/* -*- tab-width: 4; c-basic-offset: 3; -*-
 *
 * Copyright (c) 2000-2003, Roland Jesse
 * All rights reserved.
 *
 * This file is part of the Mango recipe management distribution.
 * For license and copyright information see the file COPYRIGHT or 
 * <http://www.opensource.org/licenses/bsd-license.html>.
 *
 * $Id: Recipe.cpp,v 1.17 2003/06/19 17:54:18 rjesse Exp $
 */

#include "Recipe.h"
#include "AmbrosiaFingerprint.h"
#include "Repository.h"
#include "Mango.h"
#include <qapplication.h>
#include <qpainter.h>
#include <qsimplerichtext.h> 
#include <qstylesheet.h>

/*!
  \internal class storing a recipe's information.
*/
class RecipeData {
   friend class Recipe;

	  long refCount;

      /*! This not only names the recipe but is used for referencing the
        picture in the MimeSourceFactory. Therefore: Whenever the title
        changes we need to change the according picture reference. That
        is done in Recipe::setTitle().
      */
	  QString title;

      // Head
      AmbrosiaFingerprint fingerprint; //!< Unique Id
      QString lang;             //!< Language of the recipe
      QString typist;           //!< Who typed it in the first time?
      QString typistEmail;      //!< E-Mail address of the above person.
	  QString source;           //!< Where does the recipe come from?
      QDateTime created;        //!< When was it typed in originally?
      QDateTime modified;       //!< Last date of modification
      QString modificationUser; //!< Who did the last modification?
      QString modificationEmail; //!< E-Mail address of the above person.

      /* obsolete:
      QTime cookingTime;		//!< How long does it take to cook the beast?
      */
      QMap<QString,QTime> preparationTimes;   //!< How long does it take to cook the beast?

      int yieldQty;				//!< For how many people is the meal?
      int yieldRange;           //!< For how many people is the meal at max?
      QString yieldUnit;        //!< Unit, like "persons", "cakes" et cetera.
      
      Recipe::Complexity complexity; //!< How difficult is to to cook?
      Recipe::Categories categories; //!< Where it all belongs to
      Recipe::Keywords keywords; //!< Keywords that help to categorise the recipe.

      // Body
      Recipe::Ingredients ingredients; //!< What is needed to cook it?
      Recipe::Directions directions;	//!< How to cook the beast.
      
      /*! Setting/changing the picture needs to
        be accompanied by setting its according MimeSource which
        is done by setPicture().
      */
      QValueList<RecipeImageData> pictures; //!< How might (parts of) the recipe look like?

      /*! False if only the default picture is set, true if at least one picture is really set. */
      bool validPicture;

      //! Weight Watchers points.
      float wwPoints;

      //! Costs of the recipe. Not really meant to be used.
      QString costs;

      //! Content of a recipe according to the BLS.
      QMap<QString,QString> blsContent;
      
      /*! Additional remarks someone wants to put into the recipe.
        All remarks are associated with the user who made them.
        The key is the user name.
        The data is a list of lines. This way, a user can provide multiple remarks.
      */
      QMap<QString,QStringList> remarks;

      /*!
        Container that stores arbitrary data. This is mainly to support
        the trashy <content> element of CookML documents. Not for public access!
      */
      QValueList<RecipeCustomData> customData;
      
	  /*! \internal Let's remember to which cookbook this
		recipe belongs to.
	  */
	  QStringList cookbooks;
};

/*!
  Creates an empty RecipeCustomData object.
*/
RecipeCustomData::RecipeCustomData()
{
}

/*!
  Creates a RecipeCustomData object with key \a k, datatype \a dt, and value \a v.
*/
RecipeCustomData::RecipeCustomData( const QString& k, const QString& dt, const QString& v)
   : m_key( k), m_datatype( dt), m_value( v)
{   
}

QString
RecipeCustomData::key() const
{
   return m_key;
}

QString
RecipeCustomData::datatype() const
{
   return m_datatype;
}

QString
RecipeCustomData::value() const
{
   return m_value;
}

void
RecipeCustomData::setKey( const QString& s)
{
   m_key = s;
}

void
RecipeCustomData::setDatatype( const QString& s)
{
   m_datatype = s;
}

void
RecipeCustomData::setValue( const QString& s)
{
   m_value = s;
}
   
Recipe::Recipe()
{
   r = new RecipeData();
   r->refCount = 1;
   clear();
}

Recipe::Recipe( const Recipe& other)
{
   r = other.r;
   r->refCount++;
}

Recipe::~Recipe()
{
   if (--r->refCount == 0)
	  delete r;
}

Recipe&
Recipe::operator=( const Recipe& right)
{
   right.r->refCount++;
   if (--r->refCount == 0)
	  delete r;
   r = right.r;
   return *this;
}

/*!
  Compares the content of both recipes and returns \a true in case
  they are the same, \a false otherwise.

  \todo As soon as the fingerprint is available, it should be sufficiant to
        just compare this one instead.
*/
bool Recipe::
operator==( const Recipe& right) const
{
   bool ret = true;
   if (right.title() != title()) ret = false;
   else if (right.lang() != lang()) ret = false;
   else if (right.typist() != typist()) ret = false;
   else if (right.typistEmail() != typistEmail()) ret = false;
   else if (right.source() != source()) ret = false;
   else if (right.created() != created()) ret = false;
   else if (right.modified() != modified()) ret = false;
   else if (right.modificationUser() != modificationUser()) ret = false;
   else if (right.modificationEmail() != modificationEmail()) ret = false;
//   else if (right.preparationTime() != preparationTime()) ret = false;
   else if (right.yieldQty() != yieldQty()) ret = false;
   else if (right.yieldRange() != yieldRange()) ret = false;
   else if (right.yieldUnit() != yieldUnit()) ret = false;
   else if (right.complexity() != complexity()) ret = false;
   else if (right.categories() != categories()) ret = false;
//   else if (right.ingredients() != ingredients()) ret = false;
   else if (right.directions() != directions()) ret = false;
//   else if (right.pictures() != pictures()) ret = false;
   return ret;
}

void
Recipe::detach()
{
   if (r->refCount > 1) {
	  r->refCount--;
	  r = new RecipeData( *r);
	  r->refCount = 1;
   }
}

/*!
  Set everything back to default.
*/
void
Recipe::clear()
{
   detach();
   r->title = QString::null;
   r->typist = QString::null;
   r->typistEmail = QString::null;
   r->source = QString::null;
   r->created = QDateTime::currentDateTime();
   r->modified = QDateTime::currentDateTime();
   r->modificationUser = QString::null;
   r->modificationEmail = QString::null;
   r->preparationTimes.clear();
   r->yieldQty = 0;
   r->yieldRange = 0;
   r->yieldUnit = QString::null;
   r->complexity = UnsetComplexity;
   setDefaultPicture();
   r->wwPoints = 0.0f;
   r->blsContent.clear();
   r->remarks.clear();
   r->customData.clear();
   r->categories.clear();
   r->keywords.clear();
   r->ingredients.clear();
   r->directions = QString::null;
   r->cookbooks.clear();
}

/*! A recipe is valid when it has a title name. 
  \sa invalidate()
 */
bool
Recipe::isValid() const
{
   return (!title().isEmpty());
}

/*! Mark recipe as invalid. 

  Invalid recipes are not to be used!
  \sa isValid()
*/
void
Recipe::invalidate()
{
   detach();
   setTitle( QString::null);
}

Recipe::Categories
Recipe::categories() const
{
   return r->categories;
}

void
Recipe::setCategories( const Recipe::Categories& c)
{
   detach();
   r->categories = c;
}

void
Recipe::addCategories( const Recipe::Categories& c)
{
   detach();
   r->categories += c;
}

/*!
 * Add the given category to the categories list. Each category is stored
 * only once.
 */
void
Recipe::addCategory( const QString& name)
{
   detach();
   bool changed = false;
   Recipe::Categories::iterator it = r->categories.begin();
   while (it != r->categories.end()) {
	  if ((*it) == name)
		 break;
	  it++;
   }
   if (it == r->categories.end()) {
	  r->categories.insert( r->categories.end(), name);
	  changed = true;
   }
   if (changed) r->categories.sort();
}

/*!
  Remove the category \a cat from the list of categories.
  
  \return whether or not the removal was successull, that is whether
  or not the categories list actually contained the category \a cat.
*/
bool Recipe::
removeCategory( const QString& cat)
{
   detach();
   bool ret = false;
   if (r->categories.grep( cat).count() > 0) {
      ret = true;
      r->categories.remove( cat);
   }
   return ret;
}

Recipe::Keywords
Recipe::keywords() const
{
   return r->keywords;
}

void
Recipe::setKeywords( const Keywords& kw)
{
   detach();
   r->keywords = kw;
}

void
Recipe::addKeywords( const QString& s)
{
   detach();
   r->keywords.append( s);
}

/*!
  \param str string to search for
  \param cs whether or not to search case sensitive
  \return whether or not the given string occurs in the recipe.
*/
bool
Recipe::contains( const QString& str, bool cs) const
{
   bool rc = false;
   if (title().contains( str, cs)) rc = true;
   else if (typist().contains( str, cs)) rc = true;
   else if (typistEmail().contains( str, cs)) rc = true;
   else if (source().contains( str, cs)) rc = true;
   else if (! categories().grep( str, cs).empty()) rc = true;
   else if (directions().contains( str, cs)) rc = true;
   if (!rc) {
      Ingredients ings( ingredients());
	  Ingredients::const_iterator it = ings.begin();
	  while (it != ings.end()) {
		 if (rc) break;
		 // Search in ingredient category name.
		 if (it.key().contains( str, cs)) {
			rc = true;
		 }
		 // Search in current ingredient categories' list.
		 else {
            IngredientList curList( it.data());
			IngredientList::const_iterator itL = curList.begin();
			while (itL != curList.end()) {
			   if ((*itL).measurement().contains( str, cs)) {
				  rc = true;
			   }
               itL++;
			}
		 }
		 it++;
	  }
   }
   
   return rc;
} // contains

/*! Returns the name (title) of the recipe. */
QString
Recipe::title() const
{
   return r->title;
}

/*! 
  Set the name (title) of the recipe. As a side effect, the picture reference
  in the MimeSourceFactory is updated, too.
*/
void
Recipe::setTitle( const QString& str)
{
   detach();
   r->title = str;
   updateMimeSourceFactory();
}

/*!
  Set default image.
*/
void
Recipe::setDefaultPicture()
{
   detach();
   setPicture( r->title, createPicture( 100, 50));
   r->validPicture = false;
}

/*!
  Set picture to \a pic and associate it with \a name.
*/
void
Recipe::setPicture( const QString& name, const QPixmap& pic)
{
   detach();
   RecipeImageData idata( name);
   idata.setPixmap( pic);
   r->pictures.append( idata);
   if (name == title())
      updateMimeSourceFactory();
   setPictureAsValid( true);
}

/*!
  Remove the picture associated with \a name.
  \return true in case the picture existed and was successfully removed.
  \return false otherwise (i.e. no picture associated with such a name existed).
*/
bool
Recipe::removePicture( const QString& name)
{
   detach();
   bool ret = false;

   QValueList<RecipeImageData>::iterator it;
   for (it = r->pictures.begin(); it != r->pictures.end(); it++) {
      if ((*it).label() == name) {
         r->pictures.remove( it);
         ret = true;
         break;
      }
   }
   
   if (r->pictures.isEmpty())
      setPictureAsValid( false);

   if (name == r->title)
      setDefaultPicture();
   
   return ret;
}

/*!
  Set the picture URI and associate it with \a name.
*/
void
Recipe::setPictureUri( const QString& name, const QString& uri)
{
   detach();
   RecipeImageData idata( name);
   idata.setUri( uri);
   r->pictures.append( idata);
}

/*!
  Set all pictures at once.
*/
void
Recipe::setPictures( const QValueList<RecipeImageData>& pics)
{
    detach();
    r->pictures = pics;
}

/*!
  Returns a copy of the recipe's default picture. The default
  picture is the one associated with the recipe's title.
*/
QPixmap
Recipe::picture() const
{
   return picture( title());
}

/*!
  Returns a copy of on of the recipe's pictures. The picture is not a public
  attribute in order to make sure that setPicture() is used to set or
  change the picture. That is needed for correct setting of the according
  MimeSource. Therefore, any richtext rendering or whatever works.
*/
QPixmap
Recipe::picture( const QString& name) const
{
   QPixmap ret;
   QValueList<RecipeImageData> pix( r->pictures);
   for (QValueList<RecipeImageData>::const_iterator it = pix.begin();
        it != pix.end();
        it++)
   {
      if ((*it).label() == name) {
         ret = (*it).pixmap();
         break;
      }
   }
   return ret;
}

/*!
  \return string containing the base64 encoded data of the picture associated
          with \a label.
*/
QString
Recipe::pictureAsBase64( const QString& label) const
{
   return encodeBase64( picture( label));
}

/*!
  Return all pictures at once.
*/
QValueList<RecipeImageData>&
Recipe::pictures() const
{
    return r->pictures;
}

/*!
  Returns a string list containing all labels for pictures of this recipe.
*/
QStringList
Recipe::allPictureLabels() const
{
   QStringList ret;
   QValueList<RecipeImageData>::const_iterator it;
   for (it = r->pictures.begin(); it != r->pictures.end(); it++)
      ret.append( (*it).label());
   return ret;
}

/*!
  Create a dummy image.
  \param w width of image.
  \param h height of image.
*/
QPixmap&
Recipe::createPicture(int w, int h) const
{
   QPixmap* ret = new QPixmap();
   Mango* mango = Mango::getInstance();
   QSimpleRichText text( QObject::tr( "No image set"), QApplication::font());
   ret->resize( w, h);
   ret->fill( mango->paletteBackgroundColor());
   QPainter p( ret);
   QRect clipRect( 0, 0, w, h);
   text.draw( &p, 0, 0, clipRect, mango->colorGroup());
   return *ret;
}

/*! Declare the recipe's picture as valid, 
  that is something set by the user.
*/
void
Recipe::setPictureAsValid( bool valid)
{
   detach();
   r->validPicture = valid;
}

/*! Returns whether or not the picture has already been set
  by the user. If only the default picture is available,
  false is returned.
*/
bool
Recipe::hasValidPicture() const
{
   return r->validPicture;
}

/*!
  Simply updates the title/picture pair reference in the default
  Mime source factory. That's kind of useful for richtext rendering.

  Out of all pictures, that might belong to a recipe, the default one
  is used here. This default picture is associated with the recipe's title.
*/
void
Recipe::updateMimeSourceFactory() const
{
   QMimeSourceFactory::defaultFactory()->setPixmap( r->title, picture( r->title));
}

/*!
  Returns a unique id for the recipe.
  This id equals the Ambrosia id and therefore consists of two 32 bit parts.
*/
QString Recipe::
fingerprint() const
{
   return r->fingerprint.asString();
}

void
Recipe::setFingerprint( const QString& f)
{
   detach();
   r->fingerprint = AmbrosiaFingerprint( f);
}

void Recipe::
calculateFingerprint()
{
   // No detach() as calculating the fp does not change the recipe.
   r->fingerprint.calculateFP( *this);
}

QString
Recipe::lang() const
{
   return r->lang;
}

void
Recipe::setLang( const QString& l)
{
   detach();
   r->lang = l;
}

Recipe::Complexity
Recipe::complexity() const
{
   return r->complexity;
}

void
Recipe::setComplexity( const Complexity& c)
{
   detach();
   r->complexity = c;
}

QString
Recipe::typist() const
{
   return r->typist;
}

void
Recipe::setTypist( const QString& s)
{
   detach();
   r->typist = s;
}

QString
Recipe::typistEmail() const
{
    return r->typistEmail;
}

void
Recipe::setTypistEmail( const QString& s)
{
    detach();
    r->typistEmail = s;
}

Recipe::Ingredients
Recipe::ingredients() const
{
   return r->ingredients;
}

/*!
  Add ingredient \a ing to ingredient group \a group. Creates the
  group if necessary.
*/
void
Recipe::addIngredient( const Ingredient& ing, const QString& group)
{
   detach();
   IngredientList ingl = r->ingredients[group];
   ingl.push_back( ing);
   bool overwrite = true;
   r->ingredients.insert( group, ingl, overwrite);
}

void
Recipe::setIngredients( const Ingredients& i)
{
   detach();
   r->ingredients = i;
}

int Recipe::
yieldQty() const
{
   return r->yieldQty;
}

void
Recipe::setYieldQty( const int y)
{
   detach();
   r->yieldQty = y;
}

int Recipe::
yieldRange() const
{
   return r->yieldRange;
}

void Recipe::
setYieldRange( const int range)
{
   detach();
   r->yieldRange = range;
}

QString Recipe::
yieldUnit() const
{
   return r->yieldUnit;
}

void Recipe::
setYieldUnit( const QString& s)
{
   detach();
   r->yieldUnit = s;
}

QStringList
Recipe::cookbooks() const
{
   return r->cookbooks;
}

void
Recipe::setCookbooks( const QStringList& s)
{
   detach();
   r->cookbooks = s;
}

void
Recipe::addCookbook( const QString& s)
{
   detach();
   if (! (r->cookbooks.grep( s).count() > 0)) // don't add it twice
      r->cookbooks.append( s);
}

bool
Recipe::removeCookbook( const QString& s)
{
   detach();
   bool ret = false;
   if (r->cookbooks.grep( s).count() > 0) {
	  ret = true;
	  r->cookbooks.remove( s);
   }
   return ret;
}

QString
Recipe::source() const
{
   return r->source;
}

void
Recipe::setSource( const QString& s)
{
   detach();
   r->source = s;
}

QDateTime
Recipe::created() const
{
   return r->created;
}

void
Recipe::setCreationDate( const QDateTime& d)
{
   detach();
   r->created = d;
}

QDateTime
Recipe::modified() const
{
   return r->modified;
}

void
Recipe::setModificationDate( const QDateTime& d)
{
   detach();
   r->modified = d;
}

QString
Recipe::modificationUser() const
{
    return r->modificationUser;
}

void
Recipe::setModificationUser( const QString& s)
{
    detach();
    r->modificationUser = s;
}

QString
Recipe::modificationEmail() const
{
    return r->modificationEmail;
}

void
Recipe::setModificationEmail( const QString& s)
{
    detach();
    r->modificationEmail = s;
}

QTime
Recipe::preparationTime( const QString& name) const
{
   return r->preparationTimes.find( name).data();
}

/*!
   \return sum of all preparation times.
*/
QTime
Recipe::overallPreparationTime() const
{
   QTime ret;
   QMap<QString,QTime>::const_iterator it = r->preparationTimes.begin();
   while (it != r->preparationTimes.end()) {
      ret = ret.addMSecs( it.data().msecsTo( QTime( 0, 0, 0)));
   }
   return ret;
}

/*!
  Set all separate preparation times at once.
*/
void
Recipe::setPreparationTimes( const QMap<QString,QTime>& pt)
{
   detach();
   r->preparationTimes = pt;
}

/*!
   Store the preparation time \a t with the associated \a name.
   Exemple names would be "perparation", "rising", "marinating", "cooking" et cetera.
*/
void
Recipe::setPreparationTime( const QString& name, const QTime& t)
{
   detach();
   r->preparationTimes[name] = t;
}

QMap<QString,QTime>
Recipe::preparationTimes() const
{
   return r->preparationTimes;
}

/*!
  Returns the directions text without any richtext tags.
*/
Recipe::Directions
Recipe::directions() const
{
   return r->directions;
}

/*!
  Returns the directions text as stored internally, that is including any possible
  markup.
*/
Recipe::Directions
Recipe::directionsAsRichtext() const
{
   return r->directions;
}

/*!
  Adds one step to the directions part of the recipe.
  \param s the text describing the step.
  \param constructRichtext whether or not to try to convert the plain text to a richtext paragraph.
*/
void
Recipe::add2Directions( const QString& s, const bool constructRichtext)
{
   detach();
   QString s2;
   if (constructRichtext)
      s2 = QStyleSheet::convertFromPlainText( s);
   else s2 = s;
   r->directions += s2;
}

void
Recipe::setDirections( const Directions& d)
{
   detach();
   r->directions = d;
}

/*! \return the Weight Watchers points for this recipe.  In case the
  sum of the points of all ingredients is larger than the points given
  for this recipe, this sum is returned instead.
  \sa setWwPoints()
*/
float Recipe::
wwPoints() const
{
   float ret = 0;
   Ingredients ings( ingredients());
   Ingredients::const_iterator it = ings.begin();
   while (it != ings.end()) {
      IngredientList curList( it.data());
      IngredientList::const_iterator itL = curList.begin();
      while (itL != curList.end()) {
         ret += (*itL).wwPoints();
         itL++;
      }
      it++;
   }
   ret += r->yieldQty;

   if (r->wwPoints > ret) ret = r->wwPoints;
   return ret;
}

/*!
  Sets the Weight Watchers points for this recipe.
  The points target the recipe as it is, that is for all persons.
  \sa wwPoints()
  \sa yieldQty()
*/
void Recipe::
setWwPoints( const float p)
{
   detach();
   r->wwPoints = p;
}

/*!
  This is mainly for information purpose and not really meant to be used.
  It was added in order to support the according element in CookML documents.
  \return costs of the recipe.
*/
QString
Recipe::costs() const
{
   return r->costs;
}

/*!
  \sa costs()
*/
void
Recipe::setCosts( const QString& s)
{
   detach();
   r->costs = s;
}

/*!
  \return all BLS content data.
  \sa addBlsContent(), setBlsContents()
*/
QMap<QString,QString>&
Recipe::blsContents() const
{
   return r->blsContent;
}

/*!
  Add one entry of BLS content.
  \param key the BLS key this entry is associated with.
  \param amount amount of this entry.
  \sa blsContents(), setBlsContents()
*/
void
Recipe::addBlsContent( const QString& key, const QString& amount)
{
   detach();
   r->blsContent.insert( key, amount);
}

/*!
  Set all BLS content data at once.
  \sa blsContents(), addBlsContent()
*/
void
Recipe::setBlsContents( const QMap<QString,QString>& data)
{
   detach();
   r->blsContent = data;
}

/*!
  \return all remarks associated with this recipe.
*/
QMap<QString,QStringList>&
Recipe::remarks() const
{
   return r->remarks;
}

/*!
  Set all remarks for the recipe at once.
  Useful for import.
*/
void Recipe::
setRemarks( const QMap<QString,QStringList>& rms)
{
   detach();
   r->remarks = rms;
}

/*!
   Set the remarks for \a user.
*/
void Recipe::
setRemarksForUser( const QString& user, const QStringList& s)
{
   detach();
   r->remarks[user] = s;
}

/*!
  \return the custom data trash container.
*/
QValueList<RecipeCustomData>&
Recipe::customData() const
{
   return r->customData;
}

/*!
  Set the whole custom data block at once. Not to be used except for
  import of CookML documents!
*/
void
Recipe::setCustomData( const QValueList<RecipeCustomData>& custom)
{
   detach();
   r->customData = custom;
}

/*!
  Add a custom data element where \a data is associated with \a key.
*/
void
Recipe::addCustomData( const QString& key, const QString& type, const QString& data)
{
   detach();
   RecipeCustomData cd( key, type, data);
   r->customData.append( cd);
}

/*****************************************************************************
  static functions
 *****************************************************************************/

/*!
  Encodes data of \a dataPix into a base64 encoded string as described
  in RFC 2045. As required by the RFC, no line exceeds 76 characters.

  Implementation note (taken from RFC 2045):
  "The encoding process represents 24-bit groups of input bits as output
  strings of 4 encoded characters. Proceeding from left to right, a
  24-bit input group is formed by concatenating 3 8bit input groups.
  These 24 bits are then treated as 4 concatenated 6-bit groups, each
  of which is translated into a single digit in the base64 alphabet."

  \sa decodeBase64()
*/
QString
Recipe::encodeBase64( const QPixmap& dataPix)
{
   const char* table =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
   const unsigned int linelength = 76;
   
   QByteArray inbytes;
   QDataStream ds( inbytes, IO_WriteOnly);
   ds << dataPix;
   unsigned char* in = (unsigned char*) inbytes.data();
   const unsigned int insize = inbytes.size();
   unsigned int inCtr = 0;     // runs from 0 to insize
   
   QByteArray outbytes;
   
   unsigned char ingroup[3];
   unsigned char outidx[4];

   unsigned int curOut = 0;     // runs from 0 to final size of output
   unsigned int lineCharCtr = 0; // current position in an output line
   while (inCtr < insize) {
      // Read 24-bit group.
      for (uint i = 0; i < 3; i++) {
         if (inCtr + i < insize)
            ingroup[i] = in[inCtr + i];
         else
            ingroup[i] = 0;
      }

      /*
        Convert chars according to base64 encoding table.
        Only the indexes into the table are constructed here.
      */
      outidx[0] = (ingroup[0] & 0xFC) >> 2;
      outidx[1] = ((ingroup[0] & 0x03) << 4) | ((ingroup[1] & 0xF0) >> 4);
      outidx[2] = ((ingroup[1] & 0x0F) << 2) | ((ingroup[2] & 0xC0) >> 6);
      outidx[3] = ingroup[2] & 0x3F;

      // Prepare padding handling.
      unsigned int doCtr = 3;   // default: output whole buffer
      switch (insize - inCtr) {
      case 1:
         doCtr = 1;
         break;
      case 2:
         doCtr = 2;
         break;
      }

      // Output (part of) the buffer.
      outbytes.resize( outbytes.size() + 3, QGArray::SpeedOptim);
      for (uint i = 0; i <= doCtr; i++) {
         outbytes[curOut++] = table[outidx[i]];
      }

      /*
        Padding at the end in case fewer than 24 bits are available at the
        end of the data being encoded.
      */
      for (uint i = doCtr + 1; i <= 3; i++) {
         outbytes[curOut++] = '=';
      }

      inCtr += 3;
      lineCharCtr += 4;

      if (lineCharCtr > linelength) {
         outbytes[curOut++] = '\n';
         lineCharCtr = 0;
      }
   } // while
   
   return outbytes;
}

/*!
  Follows RFC 2045.
  
  \sa encodeBase64()

  \bug QPixmap( QByteArray) is not working as expected.
*/
QPixmap
Recipe::decodeBase64( const QString& dataStr)
{
   QByteArray outbytes;

   unsigned char buf[4];
   unsigned int bufIdx = 0;
   unsigned int bufCtr = 3;

   unsigned int insize = dataStr.length();
   unsigned int curIdx = 0;     // Runs from 0 to insize.

   unsigned int curOut = 0;     // Runs from 0 to final size of output buffer.
   bool done = false;
   while (!done) {
      unsigned char ch = dataStr.at( curIdx++);
      if (curIdx >= insize)
         done = true;
      
      if ((ch >= 'A') && (ch <= 'Z'))
         ch -= 'A';
      else if ((ch >= 'a') && (ch <= 'z'))
         ch -= 'a' + 26;
      else if ((ch >= '0') && (ch <= '9'))
         ch -= '0' + 52;
      else if (ch == '+')
         ch = 62;
      else if (ch == '/')
         ch = 63;
      else if (ch == '=') {     // padding, end marker
         done = true;
         /*
           Note from the RFC regarding the number of padding characters:
           "... only the following cases can arise:
           (1) the final quantum of encoding input is an integral multiple of 24 bits;
               here, the final unit of encoded output will be an integral multiple of 4
               characters with no "=" padding,
           (2) the final quantum of encoding input is exactly 8 bits;
               here, the final unit of encoded output will be two characters followed
               by two "=" padding characters,
           (3) the final quantum of encoding input is exactly 16 bits;
               here, the final unit of encoded output will be three characters
               followed by one "=" padding character."
         */
         if (bufIdx == 0)       // case (1), shouldn't occur.
            break;
         else if (bufIdx <= 2)  // case (2): 2 chars + 2 '='
            bufCtr = 2;
         else bufCtr = 3;
      } // '='
      else                      // "invalid" character (like EOL et cetera)
         continue;

      buf[bufIdx++] = ch;

      unsigned char tmpBuf[3];
      if (bufIdx == 4) {        // done with one 3-bit string group
         bufIdx = 0;
         tmpBuf[0] = (buf[0] << 2) | ((buf[1] & 0x30) >> 4);
         tmpBuf[1] = ((buf[1] & 0x0F) << 4) | ((buf[2] & 0x3C) >> 2);
         tmpBuf[2] = ((buf[2] & 0x03) << 6) | (buf[3] & 0x3F);

         // Now append read data to outbuf (mainly 3 bits).
         outbytes.resize( outbytes.size() + bufCtr, QGArray::SpeedOptim);
         for (uint i = 0; i < bufCtr; i++)
            outbytes[curOut++] = tmpBuf[i];
      }
   } // while

   return QPixmap( outbytes);
}

/*****************************************************************************
  Recipe stream functions
 *****************************************************************************/

/*!
  \relates RecipeCustomData
  Writes the recipe's custom \a data to the stream \a s.
*/
QDataStream&
operator<<( QDataStream& s, const RecipeCustomData& data)
{
   s << data.key();
   s << data.datatype();
   s << data.value();
   return s;
}

/*!
  \relates RecipeCustomData
  Reads the custom data of a recipe from stream \a s into \a data.
*/
QDataStream&
operator>>( QDataStream& s, RecipeCustomData& data)
{
   QString str;
   s >> str;
   data.setKey( str);
   s >> str;
   data.setDatatype( str);
   s >> str;
   data.setValue( str);
   
   return s;
}

/*!
  \relates RecipeImageData
  Writes the recipe's image \a data to the stream \a s.
*/
QDataStream&
operator<<( QDataStream& s, const RecipeImageData& data)
{
   s << data.label();
   s << data.uri();
   s << data.pixmap();

   return s;
}

/*!
  \relates RecipeImageData
  Reads the image data of a recipe from stream \a s into \a data.
*/
QDataStream&
operator>>( QDataStream& s, RecipeImageData& data)
{
   QString str;
   s >> str;
   data.setLabel( str);
   s >> str;
   data.setUri( str);
   QPixmap pix;
   s >> pix;
   data.setPixmap( pix);

   return s;
}

/*!
  \relates Recipe
  Writes the recipe \a recipe to the stream \a s.
*/
QDataStream&
operator<<( QDataStream& s, const Recipe& recipe)
{
   s << recipe.fingerprint();
   s << recipe.lang();
   s << recipe.title();
   s << recipe.typist();
   s << recipe.typistEmail();
   s << recipe.source();
   s << recipe.created();   // Changed in version 2 from QDate to QDateTime
   s << recipe.modified();  // Changed in version 2 from QDate to QDateTime
   s << recipe.modificationUser();  // Added in version 2.
   s << recipe.modificationEmail(); // Added in version 2.
   s << recipe.preparationTimes(); // Changed in version 2 to a map of multiple name/time pairs.
   s << (Q_UINT32) recipe.yieldQty();
   s << (Q_UINT32) recipe.yieldRange(); // Added in version 2.
   s << recipe.yieldUnit();     // Added in version 2.
   s << (Q_UINT32) recipe.complexity();
   s << recipe.hasValidPicture();
   s << recipe.pictures();
   s << recipe.categories();
   s << recipe.keywords();      // Added in version 2.
   s << recipe.ingredients();
   s << recipe.directions();
   s << recipe.wwPoints();      // Added in version 2.
   s << recipe.remarks();       // Added in version 2.
   s << recipe.blsContents();   // Added in version 2.
   s << recipe.customData();    // Added in version 2.
   // Don't write recipe.cookbooks()! This is only for *internal* use.
   return s;
}

/*!
  \relates Recipe
  Reads a recipe from the stream \a s into recipe \a recipe.

  Changes made for \a version 2:
  - E-Mail address of the typist added.
  - YieldRange and YieldUnit in addition to YieldQty.
  - Creation- and modification data extended to DateTime.
*/
QDataStream&
operator>>( QDataStream& s, Recipe& recipe)
{
   Repository* repo = Repository::getInstance();
   QString str;
   s >> str;
   recipe.setFingerprint( str);
   s >> str;
   recipe.setLang( str);
   s >> str;
   recipe.setTitle( str);
   
   s >> str;
   recipe.setTypist( str);
   if (repo->activeMangoFormatVersion() >= 2) {
      s >> str;
      recipe.setTypistEmail( str);
   }
   s >> str;
   recipe.setSource( str);
   
   if (repo->activeMangoFormatVersion() == 1) {
      QDate date;
      s >> date;
      recipe.setCreationDate( date);
      s >> date;
      recipe.setModificationDate( date);
   }
   else { // format version 2 and above
      QDateTime dt;
      s >> dt;
      recipe.setCreationDate( dt);
      s >> dt;
      recipe.setModificationDate( dt);
      s >> str;
      recipe.setModificationUser( str);
      s >> str;
      recipe.setModificationEmail( str);
   }
   
   if (repo->activeMangoFormatVersion() == 1) {
      QTime time;
      s >> time;
      recipe.setPreparationTime( "preptime", time);
   }
   else {                       // format version 2 introduced multiple preparation times
      QMap<QString,QTime> prepTimes;
      s >> prepTimes;
      recipe.setPreparationTimes( prepTimes);
   }
   Q_UINT32 yieldQty;
   s >> yieldQty;
   recipe.setYieldQty( yieldQty);
   if (repo->activeMangoFormatVersion() >= 2) {
      Q_UINT32 yieldRange;
      s >> yieldRange;
      recipe.setYieldRange( yieldRange);
      s >> str;
      recipe.setYieldUnit( str);
   }

   // Casting here is ok as an enum is just an int.
   Q_UINT32 complexity;
   s >> complexity;
   recipe.setComplexity( (Recipe::Complexity) complexity);

   bool hasPic;
   s >> hasPic;
   recipe.setPictureAsValid( hasPic);
   if (repo->activeMangoFormatVersion() == 1) {
      QPixmap pic;
      s >> pic;
      recipe.setPicture( recipe.title(), pic);
   }
   else {       // format version 2 and above support multiple recipes
      QValueList<RecipeImageData> pics;
      s >> pics;
      recipe.setPictures( pics);
   }

   Recipe::Categories cats;
   s >> cats;
   recipe.setCategories( cats);

   if (repo->activeMangoFormatVersion() > 1) {
      Recipe::Keywords kw;
      s >> kw;
      recipe.setKeywords( kw);
   }

   Recipe::Ingredients ings;
   s >> ings;
   recipe.setIngredients( ings);

   Recipe::Directions directs;
   s >> directs;
   recipe.setDirections( directs);

   if (repo->activeMangoFormatVersion() >= 2) {
      float f;
      s >> f;
      recipe.setWwPoints( f);

      QMap<QString,QStringList> rms;
      s >> rms;
      recipe.setRemarks( rms);

      QMap<QString,QString> blsD;
      s >> blsD;
      recipe.setBlsContents( blsD);

      QValueList<RecipeCustomData> custom;
      s >> custom;
      recipe.setCustomData( custom);
   }
   return s;
}
