/***************************************************************************
    copyright            : (C) 2001-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 "entry.h"
#include "collection.h"
#include "field.h"
#include "translators/bibtexhandler.h" // needed for BibtexHandler::cleanText()
#include "document.h"
#include "tellico_debug.h"
#include "tellico_utils.h"
#include "tellico_debug.h"
#include "latin1literal.h"
#include "../isbnvalidator.h"
#include "../lccnvalidator.h"

#include <tdelocale.h>

#include <tqregexp.h>

using Tellico::Data::Entry;
using Tellico::Data::EntryGroup;

EntryGroup::EntryGroup(const TQString& group, const TQString& field)
   : TQObject(), EntryVec(), m_group(Tellico::shareString(group)), m_field(Tellico::shareString(field)) {
}

EntryGroup::~EntryGroup() {
  // need a copy since we remove ourselves
  EntryVec vec = *this;
  for(Data::EntryVecIt entry = vec.begin(); entry != vec.end(); ++entry) {
    entry->removeFromGroup(this);
  }
}

bool Entry::operator==(const Entry& e1) {
// special case for file catalog, just check the url
  if(m_coll && m_coll->type() == Collection::File &&
     e1.m_coll && e1.m_coll->type() == Collection::File) {
    // don't forget case where both could have empty urls
    // but different values for other fields
    TQString u = field(TQString::fromLatin1("url"));
    if(!u.isEmpty()) {
      // versions before 1.2.7 could have saved the url without the protocol
      bool b = KURL::fromPathOrURL(u) == KURL::fromPathOrURL(e1.field(TQString::fromLatin1("url")));
      if(b) {
        return true;
      } else {
        Data::FieldPtr f = m_coll->fieldByName(TQString::fromLatin1("url"));
        if(f && f->property(TQString::fromLatin1("relative")) == Latin1Literal("true")) {
          return KURL(Document::self()->URL(), u) == KURL::fromPathOrURL(e1.field(TQString::fromLatin1("url")));
        }
      }
    }
  }
  if(e1.m_fields.count() != m_fields.count()) {
    return false;
  }
  for(StringMap::ConstIterator it = e1.m_fields.begin(); it != e1.m_fields.end(); ++it) {
    if(!m_fields.contains(it.key()) || m_fields[it.key()] != it.data()) {
      return false;
    }
  }
  return true;
}

Entry::Entry(CollPtr coll_) : TDEShared(), m_coll(coll_), m_id(-1) {
#ifndef NDEBUG
  if(!coll_) {
    kdWarning() << "Entry() - null collection pointer!" << endl;
  }
#endif
}

Entry::Entry(CollPtr coll_, int id_) : TDEShared(), m_coll(coll_), m_id(id_) {
#ifndef NDEBUG
  if(!coll_) {
    kdWarning() << "Entry() - null collection pointer!" << endl;
  }
#endif
}

Entry::Entry(const Entry& entry_) :
    TDEShared(entry_),
    m_coll(entry_.m_coll),
    m_id(-1),
    m_fields(entry_.m_fields),
    m_formattedFields(entry_.m_formattedFields) {
}

Entry& Entry::operator=(const Entry& other_) {
  if(this == &other_) return *this;

//  myDebug() << "Entry::operator=()" << endl;
  static_cast<TDEShared&>(*this) = static_cast<const TDEShared&>(other_);
  m_coll = other_.m_coll;
  m_id = other_.m_id;
  m_fields = other_.m_fields;
  m_formattedFields = other_.m_formattedFields;
  return *this;
}

Entry::~Entry() {
}

Tellico::Data::CollPtr Entry::collection() const {
  return m_coll;
}

void Entry::setCollection(CollPtr coll_) {
  if(coll_ == m_coll) {
    myDebug() << "Entry::setCollection() - already belongs to collection!" << endl;
    return;
  }
  // special case adding a book to a bibtex collection
  // it would be better to do this in a real OOO way, but this should work
  const bool addEntryType = m_coll->type() == Collection::Book &&
                            coll_->type() == Collection::Bibtex &&
                            !m_coll->hasField(TQString::fromLatin1("entry-type"));
  m_coll = coll_;
  m_id = -1;
  // set this after changing the m_coll pointer since setField() checks field validity
  if(addEntryType) {
    setField(TQString::fromLatin1("entry-type"), TQString::fromLatin1("book"));
  }
}

TQString Entry::title() const {
  return formattedField(TQString::fromLatin1("title"));
}

TQString Entry::field(Data::FieldPtr field_, bool formatted_/*=false*/) const {
  return field(field_->name(), formatted_);
}

TQString Entry::field(const TQString& fieldName_, bool formatted_/*=false*/) const {
  if(formatted_) {
    return formattedField(fieldName_);
  }

  FieldPtr f = m_coll->fieldByName(fieldName_);
  if(!f) {
    return TQString();
  }
  if(f->type() == Field::Dependent) {
    return dependentValue(this, f->description(), false);
  }

  if(!m_fields.isEmpty() && m_fields.contains(fieldName_)) {
    return m_fields[fieldName_];
  }
  return TQString();
}

TQString Entry::formattedField(Data::FieldPtr field_) const {
  return formattedField(field_->name());
}

TQString Entry::formattedField(const TQString& fieldName_) const {
  FieldPtr f = m_coll->fieldByName(fieldName_);
  if(!f) {
    return TQString();
  }

  Field::FormatFlag flag = f->formatFlag();
  if(f->type() == Field::Dependent) {
    if(flag == Field::FormatNone) {
      return dependentValue(this, f->description(), false);
    } else {
      // format sub fields and whole string
      return Field::format(dependentValue(this, f->description(), true), flag);
    }
  }

  // if auto format is not set or FormatNone, then just return the value
  if(flag == Field::FormatNone) {
    return field(fieldName_);
  }

  if(m_formattedFields.isEmpty() || !m_formattedFields.contains(fieldName_)) {
    TQString value = field(fieldName_);
    if(!value.isEmpty()) {
      // special for Bibtex collections
      if(m_coll->type() == Collection::Bibtex) {
        BibtexHandler::cleanText(value);
      }
      value = Field::format(value, flag);
      m_formattedFields.insert(fieldName_, value);
    }
    return value;
  }
  // otherwise, just look it up
  return m_formattedFields[fieldName_];
}

TQStringList Entry::fields(Data::FieldPtr field_, bool formatted_) const {
  return fields(field_->name(), formatted_);
}

TQStringList Entry::fields(const TQString& field_, bool formatted_) const {
  TQString s = formatted_ ? formattedField(field_) : field(field_);
  if(s.isEmpty()) {
    return TQStringList();
  }
  return Field::split(s, true);
}

bool Entry::setField(Data::FieldPtr field_, const TQString& value_) {
  return setField(field_->name(), value_);
}

bool Entry::setField(const TQString& name_, const TQString& value_) {
  if(name_.isEmpty()) {
    kdWarning() << "Entry::setField() - empty field name for value: " << value_ << endl;
    return false;
  }
  // an empty value means remove the field
  if(value_.isEmpty()) {
    if(!m_fields.isEmpty() && m_fields.contains(name_)) {
      m_fields.remove(name_);
    }
    invalidateFormattedFieldValue(name_);
    return true;
  }

#ifndef NDEBUG
  if(m_coll && (m_coll->fields().count() == 0 || !m_coll->hasField(name_))) {
    myDebug() << "Entry::setField() - unknown collection entry field - "
              << name_ << endl;
    return false;
  }
#endif

  if(m_coll && !m_coll->isAllowed(name_, value_)) {
    myDebug() << "Entry::setField() - for " << name_
              << ", value is not allowed - " << value_ << endl;
    return false;
  }

  Data::FieldPtr f = m_coll->fieldByName(name_);
  if(!f) {
    return false;
  }

  // the string store is probable only useful for fields with auto-completion or choice/number/bool
  bool shareType = f->type() == Field::Choice ||
                   f->type() == Field::Bool ||
                   f->type() == Field::Image ||
                   f->type() == Field::Rating ||
                   f->type() == Field::Number;
  if(!(f->flags() & Field::AllowMultiple) &&
     (shareType ||
      (f->type() == Field::Line && (f->flags() & Field::AllowCompletion)))) {
    m_fields.insert(Tellico::shareString(name_), Tellico::shareString(value_));
  } else {
    m_fields.insert(Tellico::shareString(name_), value_);
  }
  invalidateFormattedFieldValue(name_);
  return true;
}

bool Entry::addToGroup(EntryGroup* group_) {
  if(!group_ || m_groups.contains(group_)) {
    return false;
  }

  m_groups.push_back(group_);
  group_->append(this);
//  m_coll->groupModified(group_);
  return true;
}

bool Entry::removeFromGroup(EntryGroup* group_) {
  // if the removal isn't successful, just return
  bool success = m_groups.remove(group_);
  success = success && group_->remove(this);
//  myDebug() << "Entry::removeFromGroup() - removing from group - "
//              << group_->fieldName() << "::" << group_->groupName() << endl;
  if(success) {
//    m_coll->groupModified(group_);
  } else {
    myDebug() << "Entry::removeFromGroup() failed! " << endl;
  }
  return success;
}

void Entry::clearGroups() {
  m_groups.clear();
}

// this function gets called before m_groups is updated. In fact, it is used to
// update that list. This is the function that actually parses the field values
// and returns the list of the group names.
TQStringList Entry::groupNamesByFieldName(const TQString& fieldName_) const {
//  myDebug() << "Entry::groupsByfieldName() - " << fieldName_ << endl;
  FieldPtr f = m_coll->fieldByName(fieldName_);

  // easy if not allowing multiple values
  if(!(f->flags() & Field::AllowMultiple)) {
    TQString value = formattedField(fieldName_);
    if(value.isEmpty()) {
      return i18n(Collection::s_emptyGroupTitle);
    } else {
      return value;
    }
  }

  TQStringList groups = fields(fieldName_, true);
  if(groups.isEmpty()) {
    return i18n(Collection::s_emptyGroupTitle);
  } else if(f->type() == Field::Table) {
      // quick hack for tables, how often will a user have "::" in their value?
      // only use first column for group
    TQStringList::Iterator it = groups.begin();
    while(it != groups.end()) {
      (*it) = (*it).section(TQString::fromLatin1("::"),  0,  0);
      if((*it).isEmpty()) {
        it = groups.remove(it); // points to next in list
      } else {
        ++it;
      }
    }
  }
  return groups;
}

bool Entry::isOwned() {
  return (m_coll && m_id > -1 && m_coll->entryCount() > 0 && m_coll->entries().contains(this));
}

// a null string means invalidate all
void Entry::invalidateFormattedFieldValue(const TQString& name_) {
  if(name_.isNull()) {
    m_formattedFields.clear();
  } else if(!m_formattedFields.isEmpty() && m_formattedFields.contains(name_)) {
    m_formattedFields.remove(name_);
  }
}

// format is something like "%{year} %{author}"
TQString Entry::dependentValue(ConstEntryPtr entry_, const TQString& format_, bool formatted_) {
  if(!entry_) {
    return format_;
  }

  TQString result, fieldName;
  FieldPtr field;

  int endPos;
  int curPos = 0;
  int pctPos = format_.find('%', curPos);
  while(pctPos != -1 && pctPos+1 < static_cast<int>(format_.length())) {
    if(format_[pctPos+1] == '{') {
      endPos = format_.find('}', pctPos+2);
      if(endPos > -1) {
        result += format_.mid(curPos, pctPos-curPos);
        fieldName = format_.mid(pctPos+2, endPos-pctPos-2);
        field = entry_->collection()->fieldByName(fieldName);
        if(!field) {
          // allow the user to also use field titles
          field = entry_->collection()->fieldByTitle(fieldName);
        }
        if(field) {
          // don't format, just capitalize
          result += entry_->field(field, formatted_);
        } else if(fieldName == Latin1Literal("id")) {
          result += TQString::number(entry_->id());
        } else {
          result += format_.mid(pctPos, endPos-pctPos+1);
        }
        curPos = endPos+1;
      } else {
        break;
      }
    } else {
      result += format_.mid(curPos, pctPos-curPos+1);
      curPos = pctPos+1;
    }
    pctPos = format_.find('%', curPos);
  }
  result += format_.mid(curPos, format_.length()-curPos);
//  myDebug() << "Entry::dependentValue() - " << format_ << " = " << result << endl;
  // sometimes field value might empty, resulting in multiple consecutive white spaces
  // so let's simplify that...
  return result.simplifyWhiteSpace();
}

int Entry::compareValues(EntryPtr e1, EntryPtr e2, const TQString& f, ConstCollPtr c) {
  return compareValues(e1, e2, c->fieldByName(f));
}

int Entry::compareValues(EntryPtr e1, EntryPtr e2, FieldPtr f) {
  if(!e1 || !e2 || !f) {
    return 0;
  }
  TQString s1 = e1->field(f).lower();
  TQString s2 = e2->field(f).lower();
  if(s1.isEmpty() || s2.isEmpty()) {
    return 0;
  }
  // complicated string matching, here are the cases I want to match
  // "bend it like beckham" == "bend it like beckham (widescreen edition)"
  // "the return of the king" == "return of the king"
  if(s1 == s2) {
    return 5;
  }
  // special case for isbn
  if(f->name() == Latin1Literal("isbn") && ISBNValidator::isbn10(s1) == ISBNValidator::isbn10(s2)) {
    return 5;
  }
  if(f->name() == Latin1Literal("lccn") && LCCNValidator::formalize(s1) == LCCNValidator::formalize(s2)) {
    return 5;
  }
  if(f->name() == Latin1Literal("arxiv")) {
    // normalize and unVersion arxiv ID
    s1.remove(TQRegExp(TQString::fromLatin1("^arxiv:"), false));
    s1.remove(TQRegExp(TQString::fromLatin1("v\\d+$")));
    s2.remove(TQRegExp(TQString::fromLatin1("^arxiv:"), false));
    s2.remove(TQRegExp(TQString::fromLatin1("v\\d+$")));
    if(s1 == s2) {
      return 5;
    }
  }
  if(f->formatFlag() == Field::FormatName) {
    s1 = e1->field(f, true).lower();
    s2 = e2->field(f, true).lower();
    if(s1 == s2) {
      return 5;
    }
  }
  // try removing punctuation
  TQRegExp notAlphaNum(TQString::fromLatin1("[^\\s\\w]"));
  TQString s1a = s1; s1a.remove(notAlphaNum);
  TQString s2a = s2; s2a.remove(notAlphaNum);
  if(!s1a.isEmpty() && s1a == s2a) {
//    myDebug() << "match without punctuation" << endl;
    return 5;
  }
  Field::stripArticles(s1);
  Field::stripArticles(s2);
  if(!s1.isEmpty() && s1 == s2) {
//    myDebug() << "match without articles" << endl;
    return 3;
  }
  // try removing everything between parentheses
  TQRegExp rx(TQString::fromLatin1("\\s*\\(.*\\)\\s*"));
  s1.remove(rx);
  s2.remove(rx);
  if(!s1.isEmpty() && s1 == s2) {
//    myDebug() << "match without parentheses" << endl;
    return 2;
  }
  if(f->flags() & Field::AllowMultiple) {
    TQStringList sl1 = e1->fields(f, false);
    TQStringList sl2 = e2->fields(f, false);
    int matches = 0;
    for(TQStringList::ConstIterator it = sl1.begin(); it != sl1.end(); ++it) {
      matches += sl2.contains(*it);
    }
    if(matches == 0 && f->formatFlag() == Field::FormatName) {
      sl1 = e1->fields(f, true);
      sl2 = e2->fields(f, true);
      for(TQStringList::ConstIterator it = sl1.begin(); it != sl1.end(); ++it) {
        matches += sl2.contains(*it);
      }
    }
    return matches;
  }
  return 0;
}

#include "entry.moc"
