/***************************************************************************
    copyright            : (C) 2003-2006 by Robby Stephenson
    email                : robby@periapsis.org
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of version 2 of the GNU General Public License as  *
 *   published by the Free Software Foundation;                            *
 *                                                                         *
 ***************************************************************************/

#include "tellicoimporter.h"
#include "tellico_xml.h"
#include "../collectionfactory.h"
#include "../collections/bibtexcollection.h"
#include "../entry.h"
#include "../field.h"
#include "../imagefactory.h"
#include "../image.h"
#include "../isbnvalidator.h"
#include "../latin1literal.h"
#include "../tellico_strings.h"
#include "../tellico_kernel.h"
#include "../tellico_utils.h"
#include "../tellico_debug.h"
#include "../progressmanager.h"

#include <tdelocale.h>
#include <kmdcodec.h>
#include <kzip.h>
#include <tdeapplication.h>

#include <tqdom.h>
#include <tqbuffer.h>
#include <tqfile.h>
#include <tqtimer.h>

using Tellico::Import::TellicoImporter;

TellicoImporter::TellicoImporter(const KURL& url_, bool loadAllImages_) : DataImporter(url_),
    m_coll(0), m_loadAllImages(loadAllImages_), m_format(Unknown), m_modified(false),
    m_cancelled(false), m_hasImages(false), m_buffer(0), m_zip(0), m_imgDir(0) {
}

TellicoImporter::TellicoImporter(const TQString& text_) : DataImporter(text_),
    m_coll(0), m_loadAllImages(true), m_format(Unknown), m_modified(false),
    m_cancelled(false), m_hasImages(false), m_buffer(0), m_zip(0), m_imgDir(0) {
}

TellicoImporter::~TellicoImporter() {
  if(m_zip) {
    m_zip->close();
  }
  delete m_zip;
  m_zip = 0;
  delete m_buffer;
  m_buffer = 0;
}

Tellico::Data::CollPtr TellicoImporter::collection() {
  if(m_coll) {
    return m_coll;
  }

  TQCString s; // read first 5 characters
  if(source() == URL) {
    if(!fileRef().open()) {
      return 0;
    }
    TQIODevice* f = fileRef().file();
    for(uint i = 0; i < 5; ++i) {
      s += static_cast<char>(f->getch());
    }
    f->reset();
  } else {
    if(data().size() < 5) {
      m_format = Error;
      return 0;
    }
    s = TQCString(data(), 6);
  }

  // need to decide if the data is xml text, or a zip file
  // if the first 5 characters are <?xml then treat it like text
  if(s[0] == '<' && s[1] == '?' && s[2] == 'x' && s[3] == 'm' && s[4] == 'l') {
    m_format = XML;
    loadXMLData(source() == URL ? TQByteArray(fileRef().file()->readAll()) : TQByteArray(data()), true);
  } else {
    m_format = Zip;
    loadZipData();
  }
  return m_coll;
}

void TellicoImporter::loadXMLData(const TQByteArray& data_, bool loadImages_) {
  ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true);
  item.setTotalSteps(100);
  connect(&item, TQ_SIGNAL(signalCancelled(ProgressItem*)), TQ_SLOT(slotCancel()));
  ProgressItem::Done done(this);

  TQDomDocument dom;
  TQString errorMsg;
  int errorLine, errorColumn;
  if(!dom.setContent(data_, true, &errorMsg, &errorLine, &errorColumn)) {
    TQString str = i18n(errorLoad).arg(url().fileName()) + TQChar('\n');
    str += i18n("There is an XML parsing error in line %1, column %2.").arg(errorLine).arg(errorColumn);
    str += TQString::fromLatin1("\n");
    str += i18n("The error message from TQt is:");
    str += TQString::fromLatin1("\n\t") + errorMsg;
    myDebug() << str << endl;
    setStatusMessage(str);
    m_format = Error;
    return;
  }

  TQDomElement root = dom.documentElement();

  // the syntax version field name changed from "version" to "syntaxVersion" in version 3
  uint syntaxVersion;
  if(root.hasAttribute(TQString::fromLatin1("syntaxVersion"))) {
    syntaxVersion = root.attribute(TQString::fromLatin1("syntaxVersion")).toInt();
  } else if (root.hasAttribute(TQString::fromLatin1("version"))) {
    syntaxVersion = root.attribute(TQString::fromLatin1("version")).toInt();
  } else {
    if(!url().isEmpty()) {
      setStatusMessage(i18n(errorLoad).arg(url().fileName()));
    }
    m_format = Error;
    return;
  }
//  myDebug() << "TellicoImporter::loadXMLData() - syntaxVersion = " << syntaxVersion << endl;

  if((syntaxVersion > 6 && root.tagName() != Latin1Literal("tellico"))
     || (syntaxVersion < 7 && root.tagName() != Latin1Literal("bookcase"))) {
    if(!url().isEmpty()) {
      setStatusMessage(i18n(errorLoad).arg(url().fileName()));
    }
    m_format = Error;
    return;
  }

  if(syntaxVersion > XML::syntaxVersion) {
    if(!url().isEmpty()) {
      TQString str = i18n(errorLoad).arg(url().fileName()) + TQChar('\n');
      str += i18n("It is from a future version of Tellico.");
      myDebug() << str << endl;
      setStatusMessage(str);
    } else {
      myDebug() << "Unable to load collection, from a future version (" << syntaxVersion << ")" << endl;
    }
    m_format = Error;
    return;
  } else if(XML::versionConversion(syntaxVersion, XML::syntaxVersion)) {
    // going form version 9 to 10, there's no conversion needed
    TQString str = i18n("Tellico is converting the file to a more recent document format. "
                       "Information loss may occur if an older version of Tellico is used "
                       "to read this file in the future.");
    myDebug() << str <<  endl;
//    setStatusMessage(str);
    m_modified = true; // mark as modified
  }

  m_namespace = syntaxVersion > 6 ? XML::nsTellico : XML::nsBookcase;

  // the collection item should be the first dom element child of the root
  TQDomElement collelem;
  for(TQDomNode n = root.firstChild(); !n.isNull(); n = n.nextSibling()) {
    if(n.namespaceURI() != m_namespace) {
      continue;
    }
    if(n.isElement() && n.localName() == Latin1Literal("collection")) {
      collelem = n.toElement();
      break;
    }
  }
  if(collelem.isNull()) {
    kdWarning() << "TellicoImporter::loadDomDocument() - No collection item found." << endl;
    return;
  }

  TQString title = collelem.attribute(TQString::fromLatin1("title"));

  // be careful not to have element name collision
  // for fields, each true field element is a child of a fields element
  TQDomNodeList fieldelems;
  for(TQDomNode n = collelem.firstChild(); !n.isNull(); n = n.nextSibling()) {
    if(n.namespaceURI() != m_namespace) {
      continue;
    }
    // Latin1Literal is a macro, so can't say Latin1Literal(syntaxVersion > 3 ? "fields" : "attributes")
    if((syntaxVersion > 3 && n.localName() == Latin1Literal("fields"))
       || (syntaxVersion < 4 && n.localName() == Latin1Literal("attributes"))) {
      TQDomElement e = n.toElement();
      fieldelems = e.elementsByTagNameNS(m_namespace, (syntaxVersion > 3) ? TQString::fromLatin1("field")
                                                                          : TQString::fromLatin1("attribute"));
      break;
    }
  }
//  myDebug() << "TellicoImporter::loadXMLData() - " << fieldelems.count() << " field(s)" << endl;

  // the dilemma is when to force the new collection to have all the default attributes
  // if there are no attributes or if the first one has the special name of _default
  bool addFields = (fieldelems.count() == 0);
  if(!addFields) {
    TQString name = fieldelems.item(0).toElement().attribute(TQString::fromLatin1("name"));
    addFields = (name == Latin1Literal("_default"));
    // removeChild only works for immediate children
    // remove _default field
    if(addFields) {
      fieldelems.item(0).parentNode().removeChild(fieldelems.item(0));
    }
  }

  TQString entryName;
  // in syntax 4, the element name was changed to "entry", always, rather than depending on
  // on the entryName of the collection. A type field was added to the collection element
  // to specify what type of collection it is.
  if(syntaxVersion > 3) {
    entryName = TQString::fromLatin1("entry");
    TQString typeStr = collelem.attribute(TQString::fromLatin1("type"));
    Data::Collection::Type type = static_cast<Data::Collection::Type>(typeStr.toInt());
    m_coll = CollectionFactory::collection(type, addFields);
  } else {
    entryName = collelem.attribute(TQString::fromLatin1("unit"));
    m_coll = CollectionFactory::collection(entryName, addFields);
  }

  if(!title.isEmpty()) {
    m_coll->setTitle(title);
  }

  for(uint j = 0; j < fieldelems.count(); ++j) {
    readField(syntaxVersion, fieldelems.item(j).toElement());
  }

  if(m_coll->type() == Data::Collection::Bibtex) {
    Data::BibtexCollection* c = static_cast<Data::BibtexCollection*>(m_coll.data());
    TQDomNodeList macroelems;
    for(TQDomNode n = collelem.firstChild(); !n.isNull(); n = n.nextSibling()) {
      if(n.namespaceURI() != m_namespace) {
        continue;
      }
      if(n.localName() == Latin1Literal("macros")) {
        macroelems = n.toElement().elementsByTagNameNS(m_namespace, TQString::fromLatin1("macro"));
        break;
      }
    }
//    myDebug() << "TellicoImporter::loadXMLData() - found " << macroelems.count() << " macros" << endl;
    for(uint j = 0; c && j < macroelems.count(); ++j) {
      TQDomElement elem = macroelems.item(j).toElement();
      c->addMacro(elem.attribute(TQString::fromLatin1("name")), elem.text());
    }

    for(TQDomNode n = collelem.firstChild(); !n.isNull(); n = n.nextSibling()) {
      if(n.namespaceURI() != m_namespace) {
        continue;
      }
      if(n.localName() == Latin1Literal("bibtex-preamble")) {
        c->setPreamble(n.toElement().text());
        break;
      }
    }
  }

  if(m_cancelled) {
    m_coll = 0;
    return;
  }

//  as a special case, for old book collections with a bibtex-id field, convert to Bibtex
  if(syntaxVersion < 4 && m_coll->type() == Data::Collection::Book
     && m_coll->hasField(TQString::fromLatin1("bibtex-id"))) {
    m_coll = Data::BibtexCollection::convertBookCollection(m_coll);
  }

  const uint count = collelem.childNodes().count();
  const uint stepSize = TQMAX(s_stepSize, count/100);
  const bool showProgress = options() & ImportProgress;

  item.setTotalSteps(count);

  // have to read images before entries so we can figure out if
  // linkOnly() is true
  // m_loadAllImages only pertains to zip files
  TQDomNodeList imgelems;
  for(TQDomNode n = collelem.firstChild(); !n.isNull(); n = n.nextSibling()) {
    if(n.namespaceURI() != m_namespace) {
      continue;
    }
    if(n.localName() == Latin1Literal("images")) {
      imgelems = n.toElement().elementsByTagNameNS(m_namespace, TQString::fromLatin1("image"));
      break;
    }
  }
  for(uint j = 0; j < imgelems.count(); ++j) {
    readImage(imgelems.item(j).toElement(), loadImages_);
  }

  if(m_cancelled) {
    m_coll = 0;
    return;
  }

  uint j = 0;
  for(TQDomNode n = collelem.firstChild(); !n.isNull() && !m_cancelled; n = n.nextSibling(), ++j) {
    if(n.namespaceURI() != m_namespace) {
      continue;
    }
    if(n.localName() == entryName) {
      readEntry(syntaxVersion, n.toElement());

      // not exactly right, but close enough
      if(showProgress && j%stepSize == 0) {
        ProgressManager::self()->setProgress(this, j);
        tdeApp->processEvents();
      }
    } else {
//      myDebug() << "...skipping " << n.localName() << " (" << n.namespaceURI() << ")" << endl;
    }
  } // end entry loop

  if(m_cancelled) {
    m_coll = 0;
    return;
  }

  // filters and borrowers are at document root level, not collection
  for(TQDomNode n = root.firstChild(); !n.isNull() && !m_cancelled; n = n.nextSibling()) {
    if(n.namespaceURI() != m_namespace) {
      continue;
    }
    if(n.localName() == Latin1Literal("borrowers")) {
      TQDomNodeList borrowerElems = n.toElement().elementsByTagNameNS(m_namespace, TQString::fromLatin1("borrower"));
      for(uint j = 0; j < borrowerElems.count(); ++j) {
        readBorrower(borrowerElems.item(j).toElement());
      }
    } else if(n.localName() == Latin1Literal("filters")) {
      TQDomNodeList filterElems = n.toElement().elementsByTagNameNS(m_namespace, TQString::fromLatin1("filter"));
      for(uint j = 0; j < filterElems.count(); ++j) {
        readFilter(filterElems.item(j).toElement());
      }
    }
  }

  // special for user, if using an older document format, add some nice new filters
  if(syntaxVersion < 8) {
    addDefaultFilters();
  }

  if(m_cancelled) {
    m_coll = 0;
  }
}

void TellicoImporter::readField(uint syntaxVersion_, const TQDomElement& elem_) {
  // special case: if the i18n attribute equals true, then translate the title, description, and category
  bool isI18n = elem_.attribute(TQString::fromLatin1("i18n")) == Latin1Literal("true");

  TQString name  = elem_.attribute(TQString::fromLatin1("name"), TQString::fromLatin1("unknown"));
  TQString title = elem_.attribute(TQString::fromLatin1("title"), i18n("Unknown"));
  if(isI18n) {
    title = i18n(title.utf8());
  }

  TQString typeStr = elem_.attribute(TQString::fromLatin1("type"), TQString::number(Data::Field::Line));
  Data::Field::Type type = static_cast<Data::Field::Type>(typeStr.toInt());

  Data::FieldPtr field;
  if(type == Data::Field::Choice) {
    TQStringList allowed = TQStringList::split(TQRegExp(TQString::fromLatin1("\\s*;\\s*")),
                                             elem_.attribute(TQString::fromLatin1("allowed")));
    if(isI18n) {
      for(TQStringList::Iterator it = allowed.begin(); it != allowed.end(); ++it) {
        (*it) = i18n((*it).utf8());
      }
    }
    field = new Data::Field(name, title, allowed);
  } else {
    field = new Data::Field(name, title, type);
  }

  if(elem_.hasAttribute(TQString::fromLatin1("category"))) {
    // at one point, the categories had keyboard accels
    TQString cat = elem_.attribute(TQString::fromLatin1("category"));
    if(syntaxVersion_ < 9 && cat.find('&') > -1) {
      cat.remove('&');
    }
    if(isI18n) {
      cat = i18n(cat.utf8());
    }
    field->setCategory(cat);
  }

  if(elem_.hasAttribute(TQString::fromLatin1("flags"))) {
    int flags = elem_.attribute(TQString::fromLatin1("flags")).toInt();
    // I also changed the enum values for syntax 3, but the only custom field
    // would have been bibtex-id
    if(syntaxVersion_ < 3 && field->name() == Latin1Literal("bibtex-id")) {
      flags = 0;
    }

    // in syntax version 4, added a flag to disallow deleting attributes
    // if it's a version before that and is the title, then add the flag
    if(syntaxVersion_ < 4 && field->name() == Latin1Literal("title")) {
      flags |= Data::Field::NoDelete;
    }
    field->setFlags(flags);
  }

  TQString formatStr = elem_.attribute(TQString::fromLatin1("format"), TQString::number(Data::Field::FormatNone));
  Data::Field::FormatFlag format = static_cast<Data::Field::FormatFlag>(formatStr.toInt());
  field->setFormatFlag(format);

  if(elem_.hasAttribute(TQString::fromLatin1("description"))) {
    TQString desc = elem_.attribute(TQString::fromLatin1("description"));
    if(isI18n) {
      desc = i18n(desc.utf8());
    }
    field->setDescription(desc);
  }

  if(syntaxVersion_ >= 5) {
    TQDomNodeList props = elem_.elementsByTagNameNS(m_namespace, TQString::fromLatin1("prop"));
    for(uint i = 0; i < props.count(); ++i) {
      TQDomElement e = props.item(i).toElement();
      field->setProperty(e.attribute(TQString::fromLatin1("name")), e.text());
    }
    // all track fields in music collections prior to version 9 get converted to three columns
    if(syntaxVersion_ < 9) {
      if(m_coll->type() == Data::Collection::Album && field->name() == Latin1Literal("track")) {
        field->setProperty(TQString::fromLatin1("columns"), TQChar('3'));
        field->setProperty(TQString::fromLatin1("column1"), i18n("Title"));
        field->setProperty(TQString::fromLatin1("column2"), i18n("Artist"));
        field->setProperty(TQString::fromLatin1("column3"), i18n("Length"));
      } else if(m_coll->type() == Data::Collection::Video && field->name() == Latin1Literal("cast")) {
        field->setProperty(TQString::fromLatin1("column1"), i18n("Actor/Actress"));
        field->setProperty(TQString::fromLatin1("column2"), i18n("Role"));
      }
    }
  } else if(elem_.hasAttribute(TQString::fromLatin1("bibtex-field"))) {
    field->setProperty(TQString::fromLatin1("bibtex"), elem_.attribute(TQString::fromLatin1("bibtex-field")));
  }

  // Table2 is deprecated
  if(field->type() == Data::Field::Table2) {
    field->setType(Data::Field::Table);
    field->setProperty(TQString::fromLatin1("columns"), TQChar('2'));
  }
  // for syntax 8, rating fields got their own type
  if(syntaxVersion_ < 8) {
    Data::Field::convertOldRating(field); // does all its own checking
  }
  m_coll->addField(field);
//  myDebug() << TQString("  Added field: %1, %2").arg(field->name()).arg(field->title()) << endl;
}

void TellicoImporter::readEntry(uint syntaxVersion_, const TQDomElement& entryElem_) {
  const int id = entryElem_.attribute(TQString::fromLatin1("id")).toInt();
  Data::EntryPtr entry;
  if(id > 0) {
    entry = new Data::Entry(m_coll, id);
  } else {
    entry = new Data::Entry(m_coll);
  }

  bool oldMusic = (syntaxVersion_ < 9 && m_coll->type() == Data::Collection::Album);

  // iterate over all field value children
  for(TQDomNode node = entryElem_.firstChild(); !node.isNull(); node = node.nextSibling()) {
    TQDomElement elem = node.toElement();
    if(elem.isNull()) {
      continue;
    }

    bool isI18n = elem.attribute(TQString::fromLatin1("i18n")) == Latin1Literal("true");

    // Entry::setField checks to see if an field of 'name' is allowed
    // in version 3 and prior, checkbox attributes had no text(), set it to "true" now
    if(syntaxVersion_ < 4 && elem.text().isEmpty()) {
      // "true" means checked
      entry->setField(elem.localName(), TQString::fromLatin1("true"));
      continue;
    }

    TQString name = elem.localName();
    Data::FieldPtr f = m_coll->fieldByName(name);

    // if the first child of the node is a text node, just set the attribute text
    // otherwise, recurse over the node's children
    // this is the case for <authors><author>..</author></authors>
    // but if there's nothing but white space, then it's a BaseNode for some reason
//    if(node.firstChild().nodeType() == TQDomNode::TextNode) {
    if(f) {
      // if it's a derived value, no field value is added
      if(f->type() == Data::Field::Dependent) {
        continue;
      }

      // special case for Date fields
      if(f->type() == Data::Field::Date) {
        if(elem.hasChildNodes()) {
          TQString value;
          TQDomNode yNode = elem.elementsByTagNameNS(m_namespace, TQString::fromLatin1("year")).item(0);
          if(!yNode.isNull()) {
            value += yNode.toElement().text();
          }
          value += '-';
          TQDomNode mNode = elem.elementsByTagNameNS(m_namespace, TQString::fromLatin1("month")).item(0);
          if(!mNode.isNull()) {
            value += mNode.toElement().text();
          }
          value += '-';
          TQDomNode dNode = elem.elementsByTagNameNS(m_namespace, TQString::fromLatin1("day")).item(0);
          if(!dNode.isNull()) {
            value += dNode.toElement().text();
          }
          entry->setField(name, value);
        } else {
          // if no child nodes, the code will later assume the value to be the year
          entry->setField(name, elem.text());
        }
        // go to next value in loop
        continue;
      }

      // this may be a performance hit to be stripping white space all the time
      // unfortunately, text() will include a carriage-return in cases like
      // <value>
      // text
      // </value
      // so we arbitrarily decide that only paragraphs get to have CRs?
      TQString value = elem.text();
      if(f->type() != Data::Field::Para) {
        value = value.stripWhiteSpace();
      }

      if(value.isEmpty()) {
        continue;
      }

      if(f->type() == Data::Field::Image) {
        // image info should have already been loaded
        const Data::ImageInfo& info = ImageFactory::imageInfo(value);
        // possible that value needs to be cleaned first in which case info is null
        if(info.isNull() || !info.linkOnly) {
          // for local files only, allow paths here
          KURL u = KURL::fromPathOrURL(value);
          if(u.isValid() && u.isLocalFile()) {
            TQString result = ImageFactory::addImage(u, false /* quiet */);
            if(!result.isEmpty()) {
              value = result;
            }
          }
          value = Data::Image::idClean(value);
        }
      }

      // in version 8, old rating fields get changed
      if(syntaxVersion_ < 8 && f->type() == Data::Field::Rating) {
        bool ok;
        uint i = Tellico::toUInt(value, &ok);
        if(ok) {
          value = TQString::number(i);
        }
      } else if(syntaxVersion_ < 2 && name == Latin1Literal("keywords")) {
        // in version 2, "keywords" changed to "keyword"
        name = TQString::fromLatin1("keyword");
      }
      // special case: if the i18n attribute equals true, then translate the title, description, and category
      if(isI18n) {
        entry->setField(name, i18n(value.utf8()));
      } else {
        // special case for isbn fields, go ahead and validate
        if(name == Latin1Literal("isbn")) {
          const ISBNValidator val(0);
          if(elem.attribute(TQString::fromLatin1("validate")) != Latin1Literal("no")) {
            val.fixup(value);
          }
        }
        entry->setField(name, value);
      }
    } else { // if no field by the tag name, then it has children, iterate through them
      // the field name has the final 's', so remove it
      name.truncate(name.length() - 1);
      f = m_coll->fieldByName(name);

      // if it's a derived value, no field value is added
      if(!f || f->type() == Data::Field::Dependent) {
        continue;
      }

      const bool oldTracks = (oldMusic && name == Latin1Literal("track"));

      TQStringList values;
      // concatenate values
      for(TQDomNode childNode = node.firstChild(); !childNode.isNull(); childNode = childNode.nextSibling()) {
        TQString value;
        // don't worry about i18n here, Tables are never translated
        TQDomNodeList cols = childNode.toElement().elementsByTagNameNS(m_namespace, TQString::fromLatin1("column"));
        if(cols.count() > 0) {
          for(uint i = 0; i < cols.count(); ++i) {
            // special case for old tracks
            if(oldTracks && i == 1) {
              // if the second column holds the track length, bump it to next column
              TQRegExp rx(TQString::fromLatin1("\\d+:\\d\\d"));
              if(rx.exactMatch(cols.item(i).toElement().text())) {
                value += entry->field(TQString::fromLatin1("artist"));
                value += TQString::fromLatin1("::");
              }
            }
            value += cols.item(i).toElement().text().stripWhiteSpace();
            if(i < cols.count()-1) {
              value += TQString::fromLatin1("::");
            } else if(oldTracks && cols.count() == 1) {
              value += TQString::fromLatin1("::");
              value += entry->field(TQString::fromLatin1("artist"));
            }
          }
          values += value;
        } else {
          // really loose here, we don't even check that the element name
          // is what we think it is
          TQString s = childNode.toElement().text().stripWhiteSpace();
          if(isI18n && !s.isEmpty()) {
            value += i18n(s.utf8());
          } else {
            value += s;
          }
          if(oldTracks) {
            value += TQString::fromLatin1("::");
            value += entry->field(TQString::fromLatin1("artist"));
          }
          if(values.findIndex(value) == -1) {
            values += value;
          }
        }
      }
      entry->setField(name, values.join(TQString::fromLatin1("; ")));
    }
  } // end field value loop

  m_coll->addEntries(entry);
}

void TellicoImporter::readImage(const TQDomElement& elem_, bool loadImage_) {
  TQString format = elem_.attribute(TQString::fromLatin1("format"));
  const bool link = elem_.attribute(TQString::fromLatin1("link")) == Latin1Literal("true");
  // idClean() already calls shareString()
  TQString id = link ? shareString(elem_.attribute(TQString::fromLatin1("id")))
                    : Data::Image::idClean(elem_.attribute(TQString::fromLatin1("id")));

  bool readInfo = true;
  if(loadImage_) {
    TQByteArray ba;
    KCodecs::base64Decode(TQCString(elem_.text().latin1()), ba);
    if(!ba.isEmpty()) {
      TQString result = ImageFactory::addImage(ba, format, id);
      if(result.isEmpty()) {
        myDebug() << "TellicoImporter::readImage(XML) - null image for " << id << endl;
      }
      m_hasImages = true;
      readInfo = false;
    }
  }
  if(readInfo) {
    // a width or height of 0 is ok here
    int width = elem_.attribute(TQString::fromLatin1("width")).toInt();
    int height = elem_.attribute(TQString::fromLatin1("height")).toInt();
    Data::ImageInfo info(id, format.latin1(), width, height, link);
    ImageFactory::cacheImageInfo(info);
  }
}

void TellicoImporter::readFilter(const TQDomElement& elem_) {
  FilterPtr f = new Filter(Filter::MatchAny);
  f->setName(elem_.attribute(TQString::fromLatin1("name")));

  TQString match = elem_.attribute(TQString::fromLatin1("match"));
  if(match == Latin1Literal("all")) {
    f->setMatch(Filter::MatchAll);
  }

  TQDomNodeList rules = elem_.elementsByTagNameNS(m_namespace, TQString::fromLatin1("rule"));
  for(uint i = 0; i < rules.count(); ++i) {
    TQDomElement e = rules.item(i).toElement();
    if(e.isNull()) {
      continue;
    }

    TQString field = e.attribute(TQString::fromLatin1("field"));
    // empty field means match any of them
    TQString pattern = e.attribute(TQString::fromLatin1("pattern"));
    // empty pattern is bad
    if(pattern.isEmpty()) {
      kdWarning() << "TellicoImporter::readFilter() - empty rule!" << endl;
      continue;
    }
    TQString function = e.attribute(TQString::fromLatin1("function")).lower();
    FilterRule::Function func;
    if(function == Latin1Literal("contains")) {
      func = FilterRule::FuncContains;
    } else if(function == Latin1Literal("notcontains")) {
      func = FilterRule::FuncNotContains;
    } else if(function == Latin1Literal("equals")) {
      func = FilterRule::FuncEquals;
    } else if(function == Latin1Literal("notequals")) {
      func = FilterRule::FuncNotEquals;
    } else if(function == Latin1Literal("regexp")) {
      func = FilterRule::FuncRegExp;
    } else if(function == Latin1Literal("notregexp")) {
      func = FilterRule::FuncNotRegExp;
    } else {
      kdWarning() << "TellicoImporter::readFilter() - invalid rule function: " << function << endl;
      continue;
    }
    f->append(new FilterRule(field, pattern, func));
  }

  if(!f->isEmpty()) {
    m_coll->addFilter(f);
  }
}

void TellicoImporter::readBorrower(const TQDomElement& elem_) {
  TQString name = elem_.attribute(TQString::fromLatin1("name"));
  TQString uid = elem_.attribute(TQString::fromLatin1("uid"));
  Data::BorrowerPtr b = new Data::Borrower(name, uid);

  TQDomNodeList loans = elem_.elementsByTagNameNS(m_namespace, TQString::fromLatin1("loan"));
  for(uint i = 0; i < loans.count(); ++i) {
    TQDomElement e = loans.item(i).toElement();
    if(e.isNull()) {
      continue;
    }
    long id = e.attribute(TQString::fromLatin1("entryRef")).toLong();
    Data::EntryPtr entry = m_coll->entryById(id);
    if(!entry) {
      myDebug() << "TellicoImporter::readBorrower() - no entry with id = " << id << endl;
      continue;
    }
    TQString uid = e.attribute(TQString::fromLatin1("uid"));
    TQDate loanDate, dueDate;
    TQString s = e.attribute(TQString::fromLatin1("loanDate"));
    if(!s.isEmpty()) {
      loanDate = TQDate::fromString(s, TQt::ISODate);
    }
    s = e.attribute(TQString::fromLatin1("dueDate"));
    if(!s.isEmpty()) {
      dueDate = TQDate::fromString(s, TQt::ISODate);
    }
    Data::LoanPtr loan = new Data::Loan(entry, loanDate, dueDate, e.text());
    loan->setUID(uid);
    b->addLoan(loan);
    s = e.attribute(TQString::fromLatin1("calendar"));
    loan->setInCalendar(s == Latin1Literal("true"));
  }
  if(!b->isEmpty()) {
    m_coll->addBorrower(b);
  }
}

void TellicoImporter::loadZipData() {
  delete m_buffer;
  delete m_zip;
  if(source() == URL) {
    m_buffer = 0;
    m_zip = new KZip(fileRef().fileName());
  } else {
    m_buffer = new TQBuffer(data());
    m_zip = new KZip(m_buffer);
  }
  if(!m_zip->open(IO_ReadOnly)) {
    setStatusMessage(i18n(errorLoad).arg(url().fileName()));
    m_format = Error;
    delete m_zip;
    m_zip = 0;
    delete m_buffer;
    m_buffer = 0;
    return;
  }

  const KArchiveDirectory* dir = m_zip->directory();
  if(!dir) {
    TQString str = i18n(errorLoad).arg(url().fileName()) + TQChar('\n');
    str += i18n("The file is empty.");
    setStatusMessage(str);
    m_format = Error;
    m_zip->close();
    delete m_zip;
    m_zip = 0;
    delete m_buffer;
    m_buffer = 0;
    return;
  }

  // main file was changed from bookcase.xml to tellico.xml as of version 0.13
  const KArchiveEntry* entry = dir->entry(TQString::fromLatin1("tellico.xml"));
  if(!entry) {
    entry = dir->entry(TQString::fromLatin1("bookcase.xml"));
  }
  if(!entry || !entry->isFile()) {
    TQString str = i18n(errorLoad).arg(url().fileName()) + TQChar('\n');
    str += i18n("The file contains no collection data.");
    setStatusMessage(str);
    m_format = Error;
    m_zip->close();
    delete m_zip;
    m_zip = 0;
    delete m_buffer;
    m_buffer = 0;
    return;
  }

  const TQByteArray xmlData = static_cast<const KArchiveFile*>(entry)->data();
  loadXMLData(xmlData, false);
  if(!m_coll) {
    m_format = Error;
    m_zip->close();
    delete m_zip;
    m_zip = 0;
    delete m_buffer;
    m_buffer = 0;
    return;
  }

  if(m_cancelled) {
    m_zip->close();
    delete m_zip;
    m_zip = 0;
    delete m_buffer;
    m_buffer = 0;
    return;
  }

  const KArchiveEntry* imgDirEntry = dir->entry(TQString::fromLatin1("images"));
  if(!imgDirEntry || !imgDirEntry->isDirectory()) {
    m_zip->close();
    delete m_zip;
    m_zip = 0;
    delete m_buffer;
    m_buffer = 0;
    return;
  }
  m_imgDir = static_cast<const KArchiveDirectory*>(imgDirEntry);
  m_images.clear();
  m_images.add(m_imgDir->entries());
  m_hasImages = !m_images.isEmpty();

  // if all the images are not to be loaded, then we're done
  if(!m_loadAllImages) {
//    myLog() << "TellicoImporter::loadZipData() - delayed loading for " << m_images.count() << " images" << endl;
    return;
  }

  const TQStringList images = static_cast<const KArchiveDirectory*>(imgDirEntry)->entries();
  const uint stepSize = TQMAX(s_stepSize, images.count()/100);

  uint j = 0;
  for(TQStringList::ConstIterator it = images.begin(); !m_cancelled && it != images.end(); ++it, ++j) {
    const KArchiveEntry* file = m_imgDir->entry(*it);
    if(file && file->isFile()) {
      ImageFactory::addImage(static_cast<const KArchiveFile*>(file)->data(),
                             (*it).section('.', -1).upper(), (*it));
      m_images.remove(*it);
    }
    if(j%stepSize == 0) {
      tdeApp->processEvents();
    }
  }

  if(m_images.isEmpty()) {
    // give it some time
    TQTimer::singleShot(3000, this, TQ_SLOT(deleteLater()));
  }
}

bool TellicoImporter::loadImage(const TQString& id_) {
//  myLog() << "TellicoImporter::loadImage() - id =  " << id_ << endl;
  if(m_format != Zip || !m_imgDir) {
    return false;
  }
  const KArchiveEntry* file = m_imgDir->entry(id_);
  if(!file || !file->isFile()) {
    return false;
  }
  TQString newID = ImageFactory::addImage(static_cast<const KArchiveFile*>(file)->data(),
                                         id_.section('.', -1).upper(), id_);
  m_images.remove(id_);
  if(m_images.isEmpty()) {
    // give it some time
    TQTimer::singleShot(3000, this, TQ_SLOT(deleteLater()));
  }
  return !newID.isEmpty();
}

// static
bool TellicoImporter::loadAllImages(const KURL& url_) {
  // only local files are allowed
  if(url_.isEmpty() || !url_.isValid() || !url_.isLocalFile()) {
//    myDebug() << "TellicoImporter::loadAllImages() - returning" << endl;
    return false;
  }

  // keep track of url for error reporting
  static KURL u;

  KZip zip(url_.path());
  if(!zip.open(IO_ReadOnly)) {
    if(u != url_) {
      Kernel::self()->sorry(i18n(errorImageLoad).arg(url_.fileName()));
    }
    u = url_;
    return false;
  }

  const KArchiveDirectory* dir = zip.directory();
  if(!dir) {
    if(u != url_) {
      Kernel::self()->sorry(i18n(errorImageLoad).arg(url_.fileName()));
    }
    u = url_;
    zip.close();
    return false;
  }

  const KArchiveEntry* imgDirEntry = dir->entry(TQString::fromLatin1("images"));
  if(!imgDirEntry || !imgDirEntry->isDirectory()) {
    zip.close();
    return false;
  }
  const TQStringList images = static_cast<const KArchiveDirectory*>(imgDirEntry)->entries();
  for(TQStringList::ConstIterator it = images.begin(); it != images.end(); ++it) {
    const KArchiveEntry* file = static_cast<const KArchiveDirectory*>(imgDirEntry)->entry(*it);
    if(file && file->isFile()) {
      ImageFactory::addImage(static_cast<const KArchiveFile*>(file)->data(),
                             (*it).section('.', -1).upper(), (*it));
    }
  }
  zip.close();
  return true;
}

void TellicoImporter::addDefaultFilters() {
  switch(m_coll->type()) {
    case Data::Collection::Book:
      if(m_coll->hasField(TQString::fromLatin1("read"))) {
        FilterPtr f = new Filter(Filter::MatchAny);
        f->setName(i18n("Unread Books"));
        f->append(new FilterRule(TQString::fromLatin1("read"), TQString::fromLatin1("true"), FilterRule::FuncNotContains));
        m_coll->addFilter(f);
        m_modified = true;
      }
      break;

    case Data::Collection::Video:
      if(m_coll->hasField(TQString::fromLatin1("year"))) {
        FilterPtr f = new Filter(Filter::MatchAny);
        f->setName(i18n("Old Movies"));
        // old movies from before 1960
        f->append(new FilterRule(TQString::fromLatin1("year"), TQString::fromLatin1("19[012345]\\d"), FilterRule::FuncRegExp));
        m_coll->addFilter(f);
        m_modified = true;
      }
      if(m_coll->hasField(TQString::fromLatin1("widescreen"))) {
        FilterPtr f = new Filter(Filter::MatchAny);
        f->setName(i18n("Widescreen"));
        f->append(new FilterRule(TQString::fromLatin1("widescreen"), TQString::fromLatin1("true"), FilterRule::FuncContains));
        m_coll->addFilter(f);
        m_modified = true;
      }
      break;

    case Data::Collection::Album:
      if(m_coll->hasField(TQString::fromLatin1("year"))) {
        FilterPtr f = new Filter(Filter::MatchAny);
        f->setName(i18n("80's Music"));
        f->append(new FilterRule(TQString::fromLatin1("year"), TQString::fromLatin1("198\\d"),FilterRule::FuncRegExp));
        m_coll->addFilter(f);
        m_modified = true;
      }
      break;

    default:
      break;
  }
  if(m_coll->hasField(TQString::fromLatin1("rating"))) {
    FilterPtr filter = new Filter(Filter::MatchAny);
    filter->setName(i18n("Favorites"));
    // check all the numbers, and use top 20% or so
    Data::FieldPtr field = m_coll->fieldByName(TQString::fromLatin1("rating"));
    bool ok;
    uint min = Tellico::toUInt(field->property(TQString::fromLatin1("minimum")), &ok);
    if(!ok) {
      min = 1;
    }
    uint max = Tellico::toUInt(field->property(TQString::fromLatin1("maximum")), &ok);
    if(!ok) {
      min = 5;
    }
    for(uint i = TQMAX(min, static_cast<uint>(0.8*(max-min+1))); i <= max; ++i) {
      filter->append(new FilterRule(TQString::fromLatin1("rating"), TQString::number(i), FilterRule::FuncContains));
    }
    if(!filter->isEmpty()) {
      m_coll->addFilter(filter);
      m_modified = true;
    }
  }
}

void TellicoImporter::slotCancel() {
  m_cancelled = true;
  m_format = Cancel;
}

#include "tellicoimporter.moc"
