/* -*- 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: RKImporter.cpp,v 1.7 2003/05/11 16:13:06 rjesse Exp $
 */

#include "RKImporter.h"
#include <qapplication.h>
#include <qfileinfo.h>
#include <qregexp.h>

RKImporter::RKImporter()
   : Importer()
{
}

RecipeModel&
RKImporter::apply()
   throw( Error)
{
   if (m_type == FILE) {
      try {
         file2String();
      }
      catch( Error& e) {
         throw( e);
      }
   }

   emit start( tr( "Rezkonv 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 s( tr( "File %1 does not contain any Rezkonv recipe(s).")
            .arg( m_filename));
         throw Error( s);
      }
      else {
         QString s( tr( "Could not find any Rezkonv recipe(s)."));
         throw Error( s);
      }
   }

   return m_cookbook;
} // apply()

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

void
RKImporter::readAllRecipes()
{
   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( "^=====.*REZKONV", false);
      if ((*it).contains( rxpStart)) {
         /*
           This looks like a recipe. Copy everything up to the end of the
           recipe to a separate stringlist and pass it along.
         */
         m_curRecipeStringL.clear();
         bool end = false;
         QRegExp rxpEnd = QRegExp( "^=====$");
         while ((!m_stopProcessing) && (!end)) {
            m_curRecipeStringL.append( *it);
            if ((*it).contains( rxpEnd) || (it == m_rawStringL.end()))
               end = true;
            stepOneLine( it, &progressCounter);
         }
         readRecipe();
      }
      stepOneLine( it, &progressCounter);
   }
   
   emit done();
} // readAllRecipes()

/*! Read in a single recipe consisting of the recipe's header and
  body.  As a result, the read information is stored in m_curRecipe,
  which is added to m_cookbook.
  
  \sa readRecipeHeader()
  \sa readRecipeBody()
*/
void
RKImporter::readRecipe()
{
   if (m_stopProcessing) return;
   m_curRecipe.clear();
   readRecipeHeader();
   readRecipeBody();
   if (m_curRecipe.isValid()) m_cookbook += m_curRecipe;
} // readRecipe()

/*!
  The header contains: title, categories, yield information.
*/
void
RKImporter::readRecipeHeader()
{
   bool done = false;           // Done after title, categories, and yield.
   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( "^=====.*REZKONV", false);
      if ((*m_it).contains( rxpHead)) {
         m_it++;
         continue;
      }

      // skip empty lines
      if ((*m_it).isEmpty()) {
         m_it++;
         continue;
      }

      QRegExp rxpTitle = QRegExp( "^ *Titel: .+", false);
      QRegExp rxpCat = QRegExp( "^ *Kategorien: .+", false);
      QRegExp rxpYield = QRegExp( "^ *Menge: +\\d.*", false);
      if ((*m_it).contains( rxpTitle)) {
         int pos = (*m_it).find( "Titel: ", 0, false);
         int rest = (*m_it).length() - (pos + QString( "Titel: ").length());
         m_curRecipe.setTitle( (*m_it).right( rest));
         gotT = true;
      }
      else if ((*m_it).contains( rxpCat)) {
         int pos = (*m_it).find( "Kategorien: ", 0, false);
         int rest = (*m_it).length() - (pos + QString( "Kategorien: ").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 = (*m_it).find( "Menge: ", 0, false);
         int rest = (*m_it).length() - (pos + QString( "Menge: ").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 body contains: ingredients, references, preparation steps.
*/
void
RKImporter::readRecipeBody()
{
   readRecipeIngredients();
   readRecipeSteps();
} // readRecipeBody()

/*!
  Read in all ingredients of the current recipe.
*/
void
RKImporter::readRecipeIngredients()
{
   // remember previous ingredient in case it spans multiple lines
   Ingredient lastI;

   // the current ingredient group
   QString curGroup;

   // Skip empty lines at the beginning
   QRegExp rxpWhitesOnly = QRegExp( "^\\s+$");
   bool skip = true;
   while (skip) {
      if (!(*m_it).isEmpty() || !(*m_it).contains( rxpWhitesOnly)) skip = false;
      m_it++;
   }

   // example group line: ==========SAUCE==========
   QRegExp rxpGroup = QRegExp( "^=====.*=$");
   bool stop = false;
   while (!stop) {              // iterate all lines
      // stop at the first line that is not representing an ingredient
      if (! mightBelong2Ingredients( *m_it)) break;

      // just ignore empty lines. dont stop parsing, though.
      if ((*m_it).isEmpty() || (*m_it).contains( rxpWhitesOnly)) {
         m_it++;
         continue;
      }
      
      // open a new ingredient group at each group line
      if ((*m_it).contains( rxpGroup)) {
         m_curRecipe.addIngredient( lastI, curGroup);
         lastI.clear();
         QString s( *m_it);
         s.replace( QRegExp( "^="), ""); // remove initial "="s
         s.replace( QRegExp( "=*$"), ""); // remove "="s after group name
         s = s.simplifyWhiteSpace();
         curGroup = s;
         m_it++;
      }

      // Check whether or not the group is in reality a reference line
      if (curGroup.contains( "quelle", false)) {
         readRecipeReferences();
         curGroup.truncate( 0);
         stop = true;           // Ingredients done.
         break;
      }
      
      Ingredient curI;
      bool contLastI = readIngredientLine( *m_it, curI);
      if (contLastI) lastI += curI;
      else {
         // Make sure not to insert the first ingredient of a group twice.
         if (!lastI.isEmpty()) m_curRecipe.addIngredient( lastI, curGroup);
         lastI = curI;
      }
      m_it++;
   }
} // readRecipeIngredients()

bool
RKImporter::mightBelong2Ingredients( const QString& line) const
{
   bool ret = false;
   
   // Empty lines are valid, group lines, too.
   QRegExp rxpGroup = QRegExp( "^=====.*=$");
   QRegExp rxpWhites = QRegExp( "^\\s+$");
   if (line.isEmpty() || line.contains( rxpWhites) || line.contains( rxpGroup))
      ret = true;
   else {
      /*
        There needs to be a blank at column 7 and 18
        (right before/after the measurement).
      */
      QChar c7 = line.at( 7);
      if (c7 == QChar::null) return false; // beyond end of line.
      else if (c7.isSpace()) {  // column 7 ok => check column 18
         QChar c18 = line.at( 18);
         if (c18 == QChar::null) return false; // see above
         else if (c18.isSpace()) {
            // Starting at column 19 line must not be empty
            QString rest = line.mid( 19);
            if (!rest.isNull()) ret = true;
         }
      }
   }
   return ret;
} // mightBelong2Ingredients()

/*!
  \param line the line to be parsed.
  \param ing where to put the parsed result into.
  \return true in case the previous ingredient line was continued,
          false if a new ingredient was found/started.
*/
bool
RKImporter::readIngredientLine( const QString& line, Ingredient& ing)
{
   bool contLastLine = false;
   QRegExp rxpWhites = QRegExp( "^\\s+$");

   /*
     amount: column 0-6
     measurement: column 8-17
     name: column 19+
   */
   QString amount( line.left( 7));
   QString measurement( line.mid( 8, 10));
   QString name( line.mid( 19));
   amount = amount.simplifyWhiteSpace();
   measurement = measurement.simplifyWhiteSpace();
   /*
     check whether the measurement is RK specific and should be put
     into the name.
   */
   if (checkForRKspecificMeasurement( measurement)) {
      name.prepend( measurement);
      measurement.truncate( 0);
   } else {
      measurement = convertMeasurement( measurement);
   }
   name = name.simplifyWhiteSpace();

   if (name.startsWith( "-")) { // continue previous ingredient line
      contLastLine = true;
      name.replace( QRegExp( "^(-)+"), ""); // remove leading "-" chars
      QString s(ing.name());
      s.append( name);
      ing.setName( s);
   } else {                     // line with a new ingredient
      ing.setAmountQty( amount.toFloat());
      ing.setMeasurement( measurement);
      ing.setName( name);
   }

   return contLastLine;
} // readIngredientLine()

/*!
  \return whether or not the passed measurement string is RK specific
  and should be added to the ingredient's name instead.
*/
bool
RKImporter::checkForRKspecificMeasurement( const QString& in) const
{
   bool ret = false;
   QString ms = in.simplifyWhiteSpace();
   if (ms == "Würfel") ret = true;
   else if (ms == "Handvoll") ret = true;
   else if (ms == "Stücke") ret = true;
   else if (ms == "Stück") ret = true;
   else if (ms == "Stangen") ret = true;
   else if (ms == "Stange/n") ret = true;
   else if (ms == "Stange") ret = true;
   else if (ms == "Stängel") ret = true;
   else if (ms == "Zweige") ret = true;
   else if (ms == "Zweig/e") ret = true;
   else if (ms == "Zweig") ret = true;
   else if (ms == "Blatt") ret = true;
   else if (ms == "Blätter") ret = true;
   else if (ms == "Kopf") ret = true;
   else if (ms == "cm") ret = true;
   else if (ms == "cm-Stück") ret = true;
   else if (ms == "Einige") ret = true;
   else if (ms == "Mehrere") ret = true;
   return ret;
} // checkForRKspecificMeasurement()

/*!
  Convert an Rezkonv measurement to a MealMaster measurement. It
  is a plain and simple 'one-to-one' string conversion.
  
  \param in measurement string to convert.
  \return converted string.
*/
QString
RKImporter::convertMeasurement( const QString& in) const
{
   QString ret;
   QString ms = in.simplifyWhiteSpace();
   /*
     No need to convert these: ml, mg, x
   */
   if (ms == "Liter") ret = "l";
   else if (ms == "Gramm") ret = "g";
   else if (ms == "Kilo") ret = "kg";
   else if (ms == "Essl.") ret = "tb";
   else if (ms == "Teel.") ret = "ts";
   else if (ms == "Scheib.") ret = "sl";
   else if (ms == "Dose/n") ret = "cn";
   else if (ms == "Tasse/n") ret = "c";
   else if (ms == "Karton") ret = "ct";
   else if (ms == "Pack.") ret = "pk";
   else if (ms == "Bund") ret = "bn";
   else if (ms == "Schuss") ret = "ds";
   else if (ms == "Prise/n") ret = "pn";
   else if (ms == "Tropfen") ret = "dr";
   else if (ms == "klein.") ret = "sm";
   else if (ms == "mittl.") ret = "md";
   else if (ms == "groß.") ret = "lg";
   else if (ms == "Etwas") ret = "sm";
   return ret;
} // convertMeasurement()

/*
  References start at the references line and end with the first
  empty line that follows.
*/
void
RKImporter::readRecipeReferences()
{
   while (true) {
      // Done on the first empty line.
      QRegExp rxpWhites = QRegExp( "^\\s+$");
      if ((*m_it).isEmpty() || (*m_it).contains( rxpWhites)) {
         m_it++;
         break;
      }

      // Three possible line beginnings: '-', '- ', '-- '
      QRegExp rxpBegin = QRegExp( "^\\s+(-|- |-- )");
      if ((*m_it).contains( rxpBegin)) { // ignore line if dash(es) not found
         QString curString( *m_it);
         curString.replace( rxpBegin, "");
         /*
           The last two reference lines are special and contain the
           recipe's creation date as well as the typist.
         */
         QRegExp rxpCreationDate = QRegExp( "Erfasst \\*RK\\*", false);
         if (curString.contains( rxpCreationDate)) { // date and typist
            curString.replace( rxpCreationDate, "");
            curString.replace( QRegExp( " von\\s*$"), "");
            QString dateString( curString);
            dateString = dateString.simplifyWhiteSpace();
            /*
              The date is in format: "DD.MM.YY".
              Note: As of the "YY", Rezkonv is *not* Y2K safe.
                    Let's just try to work around this.
            */
            QString s = dateString.section( ".", 0, 0);
            int d = s.toInt();
            s = dateString.section( ".", 1, 1);
            int m = s.toInt();
            s = dateString.section( ".", 2, 2);
            int y = s.toInt();
            if (y < 100) y += 1900; // yy instead of yyyy.
            if (y < 1970) y += 100;
            QDate creationDate( y, m, d);
            m_curRecipe.setCreationDate( creationDate);
            /*
              typist is in the next line
            */
            m_it++;
            curString = *m_it;
            curString.replace( rxpBegin, "");
            m_curRecipe.setTypist( curString);
         } else {               // put references info into the recipe
            QString s( m_curRecipe.source());
            s.append( curString);
            m_curRecipe.setSource( s);
         }
      }
      m_it++;
   }
} // readRecipeReferences()

void
RKImporter::readRecipeSteps()
{
   bool skipLines = true;
   while (true) {
      QRegExp rxpEnd = QRegExp( "^(=====)");
      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;

      /*
        Make sure not to miss end of lines and empty lines. Therefore,
        use a temporary extra string here.
      */
      QString s( *m_it);
      s += "\n";
      m_curRecipe.add2Directions( s);
      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
RKImporter::stepOneLine( QStringList::const_iterator& it, int* progressCount)
{
   it++;
   (*progressCount)++;
   emit progress( *progressCount, m_rawStringL.count());
   qApp->processEvents();
} // stepOneLine()
