/* -*- indent-tabs-mode: nil; -*-
 *
 * This file is part of the Mango recipe management distribution.
 * Copyright (c) 2000-2003, Roland Jesse
 * All rights reserved.
 *
 * For license and copyright information see the file COPYRIGHT or 
 * <http://www.opensource.org/licenses/bsd-license.html>.
 *
 * $Id: MMImporter.cpp,v 1.24 2003/05/11 14:45:23 rjesse Exp $
 */

#include "MMImporter.h"
#include "Ingredient.h"
#include <qapplication.h>
#include <qfileinfo.h>
#include <qregexp.h>
#include <assert.h>

MMImporter::MMImporter()
   : Importer()
{
} 

RecipeModel&
MMImporter::apply()		// public
   throw(Error)
{
   /* In case we are to import from a file, we just read it into our
	  internal string and continue from there on just as it would have
	  been pasted directly.
   */
   if (m_type == FILE) {
	  try {
		 file2String();
	  }
	  catch (Error& e) {
		 throw e;
	  }
   }
   emit start( tr( "Meal-Master import", "String used to label the progress bar."), m_rawStringL.count());
   
   readCookbookHeader();
   readAllRecipes();

   // Do not throw exception if user was interrupting the input.
   if (m_cookbook.isEmpty() && !m_stopProcessing) {
      if (m_type == FILE) {
         QString str( tr( "File %1 does not contain any Meal-Master recipe(s).")
                      .arg( m_filename));
         throw Error( str);
      }
      else {
         QString str( tr( "Could not find any Meal-Master encoded recipe(s)."));
         throw Error( str);
      }
   }

   return m_cookbook;
}

/*!
  Simply try to determine a name for the cookbook. Use file base name if
  available, otherwise mark it as unset.
*/
void
MMImporter::readCookbookHeader() // protected
{
   if (m_stopProcessing) return;
   if (m_type == FILE) {
	  QFileInfo finfo( m_filename);
	  m_cookbook.setName( finfo.baseName());
   }
   else
	  m_cookbook.setName( "no name set");
}

void
MMImporter::readAllRecipes()	// protected
{
   if (m_stopProcessing) return;
   QStringList::const_iterator it = m_rawStringL.begin();
   int progressCounter = 0;
   while (it != m_rawStringL.end()) {
      if (m_stopProcessing)     // Cancel request by the user.
         break;
      
      QRegExp rxpStart = QRegExp( "^(M{5,5}|-{5,5}).*Meal-Master", false);
      if ((*it).contains( rxpStart)) {
         /*
           We might have a recipe here. Copy everything up to the end
           of the recipe to a separate string list and pass that on
           for further examination.
         */
         m_curRecipeStringL.clear();
         bool end = false;
         QRegExp rxpEnd = QRegExp( "^(M{5,5}|-{5,5})$", false);
         // copy recipe into internal string list
         while ((!m_stopProcessing) && (!end)) {
            m_curRecipeStringL.append( *it);
            if (((*it).contains( rxpEnd)) || (it == m_rawStringL.end()))
               end = true;
            stepOneLine( it, &progressCounter);
         }
         readRecipe();
      } // recipe found
      stepOneLine( it, &progressCounter);
   } // parse whole string list
   emit done();
} // readAllRecipes();

void
MMImporter::readRecipe()		// protected
{
   if (m_stopProcessing) // stop if user pressed cancel in the progress dialog
      return;
   m_curRecipe.clear();
   readRecipeHeader();
   readRecipeBody();
   if (m_curRecipe.isValid()) {
      m_curRecipe.calculateFingerprint();
      m_cookbook += m_curRecipe;
   }
}

/*! 
  The header contains the title, catagories, and yield information.
*/
void
MMImporter::readRecipeHeader()	// protected
{
   
   // done when we got title, categories, and yield information
   bool done = false;
   bool gotT, gotC, gotY;
   gotT = gotC = gotY = false;
   
   m_it = m_curRecipeStringL.begin();
   while ((!done) && (m_it != m_curRecipeStringL.end())) {
      // skip initial header line
      QRegExp rxpHead = QRegExp( "^(M{5,5}|-{5,5}).*Meal-Master", false);
      if ((*m_it).contains( rxpHead)) {
         m_it++;
         continue;
      }

      // Ignore empty lines.
      if ((*m_it).isEmpty()) {
         m_it++;
         continue;
      }
      
      QRegExp rxpTitle = QRegExp( "^ *Title: .+", false);
      QRegExp rxpCat = QRegExp( "^ *Categories: .+", false);
      QRegExp rxpYield = QRegExp( "^ *(Yield:|Servings:) +[1-9][0-9]*", false);
      if ((*m_it).contains( rxpTitle)) {
         int pos = (*m_it).find( "Title: ", 0, false);
         int rest = (*m_it).length() - (pos + QString( "Title: ").length());
         m_curRecipe.setTitle( (*m_it).right( rest));
         gotT = true;
      }
      else if ((*m_it).contains( rxpCat)) {
         int pos = (*m_it).find( "Categories: ", 0, false);
         int rest = (*m_it).length() - (pos + QString( "Categories: ").length());
         QString cats( (*m_it).right( rest));
         QStringList catList = QStringList::split( ", ", cats);
         m_curRecipe.addCategories( catList);
         gotC = true;
      }
      else if ((*m_it).contains( rxpYield)) {
         int pos = 0;
         int rest = 0;
         if ((*m_it).startsWith( "Yield")) {
            pos = (*m_it).find( "Yield: ", 0, false);
            rest = (*m_it).length() - (pos + QString( "Yield: ").length());
         } else {
            pos = (*m_it).find( "Servings: ", 0, false);
            rest = (*m_it).length() - (pos + QString( "Servings: ").length());
         }
         QString yieldStr((*m_it).right( rest).stripWhiteSpace());
         // clean out any possible text before and after the yield number
         yieldStr.replace( QRegExp( "^\\D*"), "");
         yieldStr.replace( QRegExp( "\\D*$"), "");
         m_curRecipe.setYieldQty( yieldStr.toInt());
         // everything after the yield number is treated as the yield unit
         int unitIdx = (*m_it).find( QRegExp( "\\D*$"));
         if (unitIdx != -1) m_curRecipe.setYieldUnit( (*m_it).mid( unitIdx).simplifyWhiteSpace());
         gotY = true;
      }

      if (gotT && gotC && gotY) done = true;
      m_it++;
   }
} // readRecipeHeader()

/*!
  The recipe body contains ingredients, references, and preparation steps.

  \bug (B) Ingredients: retrieving amounts like "1 1/2", 
           possible sollution: use hardcoded string indexes (field lengths).           
*/
void
MMImporter::readRecipeBody()	// protected
{
   readRecipeIngredients();
   readRecipeSteps();
}

/*!
  Read in all ingredients.

  This function contains an internal list of strings that identify
  a references line. These strings are hardcoded and not subject to
  the normal i18n mechanism.

  \todo Provide a way for the user to manipulate the internal list of
        strings identifying a references line.
*/
void
MMImporter::readRecipeIngredients() // private
{
   // remember previous ingredient as it might span multiple lines.
   Ingredient lastI;

   // The current ingredient category
   QString curGroup;

   // Skip empty line(s) at the beginning.
   QRegExp rxpWhitespaceOnly = QRegExp( "^\\s+$", false);
   bool skip = true;
   while (skip) {
      // stop skipping if either line is not empty or contains not only whitespaces
      if (!(*m_it).isEmpty() || !(*m_it).contains( rxpWhitespaceOnly)) skip = false;
      m_it++;
   }

   // example group line: MMMMM----------------SAUCE-----------------
   QRegExp rxpGroup = QRegExp( "^(MMMMM|-----).*(-)+$", false);

   bool stop = false;
   while (!stop) {              // iterate all lines
      // Ingredients are done at the first line not representing an ingredient
      if (! mightBelong2Ingredients( *m_it)) {
         break;
      }

      /* In case we find a reference line:
         open a new ingredient category
      */
      if ((*m_it).contains( rxpGroup)) {
         m_curRecipe.addIngredient( lastI, curGroup); // New group, new ingredients.
         lastI.clear();

         QString str( *m_it);
         str.replace( QRegExp( "^M{5,5}"), ""); // remove initial "M"s
         str.replace( QRegExp( "^-*"), ""); // remove dashes up to category name
         str.replace( QRegExp( "-*$"), ""); // remove dashes after category name
         str = str.simplifyWhiteSpace(); // cut unwanted whitespaces
         curGroup = str;        // The rest is our new category.
         m_it++;                // next line
      }
      
      /* Check whether or not the "category" is in reality a references
         line.

         Note: The strings are hardcoded here and not subject to i18n.
      */
      QStringList referencesKeywords;
      referencesKeywords.append( "source");
      referencesKeywords.append( "quelle");
      referencesKeywords.append( "ref");
      bool cs = false;          // ignore case
      QStringList grepRes = referencesKeywords.grep( curGroup, cs);
      if ((! curGroup.isEmpty()) && (grepRes.count() != 0)) {
         readRecipeReferences();
         curGroup.setLength( 0); // references done. reset group name.
         stop = true;           // ingredients done.
         break;
      }

      Ingredient curI;
      bool contLastI = false;
      contLastI = readIngredientLine( *m_it, curI);
      if (contLastI) lastI += curI;
      else {
         /* The first ingredient of each group is empty and was
            already inserted above. Don't do it again.
         */
         if (! lastI.isEmpty()) m_curRecipe.addIngredient( lastI, curGroup);
         lastI = curI;
      }
      m_it++;                   // next line
   }
} // readRecipeIngredients()

/*
  \param line the line to be parsed
  \param ing That's where we put parsed information into.
  \return true if the previous line was continued, false if new ingredient started.

  \todo Convert "fl", "ga", "qt", and "pt" to the metric system.
  \todo Convert "oz" and "lb" to the metric system.
 */
bool
MMImporter::readIngredientLine( const QString& line, Ingredient& ing) // private
{
   bool contLastLine = false;
   QRegExp rxpWhitespacesOnly = QRegExp( "^\\s+$", false);
   
   /*
     Extract the three parts of a ingredient (name, amount, measurement)
     into separate strings. They are separated according to their length:
     amount: columns 0-6
     measurement: columns 8-9
     name: columns 11+
   */
   QString amount( line.left( 7)); // col 0-6
   QString measurement( line.mid( 8, 2)); // col 8-9
   QString name( line.mid( 11)); // col 11+

   amount = amount.simplifyWhiteSpace();
   measurement = measurement.simplifyWhiteSpace();
   name = name.simplifyWhiteSpace();

   /*
     Convert "cl" and "dl" to "ml"; "cg" and "dg" to "mg". That
     eases RezKonv export.
   */
   if (measurement == "cl") {
      int a = amount.toInt();
      a *=  10;
      amount = QString::number( a);
   }
   else if (measurement == "dl") {
      int a = amount.toInt();
      a *= 100;
      amount = QString::number( a);
   }
   else if (measurement == "cg") {
      int a = amount.toInt();
      a *= 10;
      amount = QString::number( a);
   }
   else if (measurement == "dg") {
      int a = amount.toInt();
      a *= 100;
      amount = QString::number( a);
   }
   
   if (name.startsWith( "-")) { // continue previous ingredient line.
      contLastLine = true;
      name.replace( QRegExp( "^(-)+"), " "); // remove all leading "-" chars
      name = name.simplifyWhiteSpace(); // in case prev. line caused
                                        // double space
      QString s( ing.name());
      s.append( name);
      ing.setName( s);
   } else {                     // just an ordinary ingredient line
      ing.setAmountQty( amount.toFloat());
      ing.setMeasurement( measurement);
      ing.setName( name);
   }

   return contLastLine;
} // bool readIngredientLine( line, ing)

/*!
  Returns whether or not the string \a line is possibly part of a
  recipe's ingredient block.
*/
bool
MMImporter::mightBelong2Ingredients( const QString& line) const
{
   bool rc = false;

   QRegExp rxpSeparator = QRegExp( "^(MMMMM|-----)", false);
   QRegExp rxpWhitespaceOnly = QRegExp( "^\\s+$", false);

   // Empty lines are valid. Group lines, too.
   if (line.isEmpty() || line.contains( rxpWhitespaceOnly) 
       || line.contains( rxpSeparator))
   {
      rc = true;
   } else {
      /*
        There needs to be a blank at columns 7 and 10
        (before/after the measurement)
      */
      QChar c7 = line.at( 7);
      if (c7 == QChar::null) return false; // beyond length of the string
      else if (c7.isSpace()) {   // col. 7 ok -> go for col. 10
         QChar c10 = line.at( 10);
         if (c10 == QChar::null) return false;
         else if (c10.isSpace()) {
            // Starting at column 11, the line must not be empty
            QString rest = line.mid( 11);
            if (! rest.isNull())
               rc = true;
         }
      }
   }

   return rc;
} // mightBelong2Ingredients() 

/*
  References start at the references line and end with the first empty
  line that follows. (Empty here means empty or full of whitespaces.)
*/
void
MMImporter::readRecipeReferences() // private
{
   while (true) {
      // Finish on the first empty line.
      QRegExp rxpWhitespacesOnly = QRegExp( "^\\s+$");
      if ((*m_it).isEmpty() || (*m_it).contains( rxpWhitespacesOnly)) {
         m_it++;
         break;
      }

      int idx = (*m_it).find( "--");
      if (idx != -1) {          // ignore line if dashes not found
         QString curString( *m_it);
         QRegExp rxpLeadingWhitespaces = QRegExp( "^\\s+");
         curString.replace( rxpLeadingWhitespaces, "");
         curString.replace( QRegExp( "^-- "), "");
         QRegExp rxpTypist = QRegExp( "\\b(erfasst|erfaßt|typed)\\b", false);
         if (curString.contains( rxpTypist)) {
            curString.replace( rxpTypist, "");
            curString.replace( rxpLeadingWhitespaces, "");
            m_curRecipe.setTypist( curString);
         }
      }
      m_it++;
   }
} // readRecipeReferences()

/*
  Even though a recipe should be ended by the MMMMM mark, check for end of
  string, too.
*/
void
MMImporter::readRecipeSteps()   // private
{
   bool skipLines = true;
   while (true) {
      QRegExp rxpEnd = QRegExp( "^(M{5,5}|-----)", true);
      if ((m_it == m_curRecipeStringL.end()) || ((*m_it).contains( rxpEnd)))
         break;

      // Skip initial empty line(s).
      QRegExp rxpWhitespacesOnly = QRegExp( "^\\s*\\s$");
      if (skipLines && ((*m_it).isEmpty() || (*m_it).contains( rxpWhitespacesOnly))) {
         m_it++;
         continue;
      }
      skipLines = false;

      // Check for eventually occuring fingerprint.
      QString fpMarker( ":Fingerprint:");
      if ((*m_it).startsWith( fpMarker)) {
         QString fpStr( (*m_it).mid( fpMarker.length()));
         m_curRecipe.setFingerprint( fpStr);
      }
      
      m_curRecipe.add2Directions( *m_it);
      m_it++;
   }
} // readRecipeSteps()

/*!
  Steps the iterator to the next position, increases the progress count by
  one and emits progress().

  \return true in case processing should be stopped, false if all is fine
          and reading can be continued.
  \sa progress()
*/
void
MMImporter::stepOneLine( QStringList::const_iterator& it, int* progressCount)
{
   it++;
   (*progressCount)++;
   emit progress( *progressCount, m_rawStringL.count());
   qApp->processEvents();
} // stepOneLine()
