/* kmail message dictionary */
/* Author: Ronen Tzur <rtzur@shani.net> */

#include "kmfolderindex.h"
#include "kmfolder.h"
#include "kmmsgdict.h"
#include "kmdict.h"
#include "globalsettings.h"
#include "folderstorage.h"

#include <tqfileinfo.h>

#include <kdebug.h>
#include <kstaticdeleter.h>

#include <stdio.h>
#include <unistd.h>

#include <string.h>
#include <errno.h>

#include <config.h>

#ifdef HAVE_BYTESWAP_H
#include <byteswap.h>
#endif

// We define functions as kmail_swap_NN so that we don't get compile errors
// on platforms where bswap_NN happens to be a function instead of a define.

/* Swap bytes in 32 bit value.  */
#ifdef bswap_32
#define kmail_swap_32(x) bswap_32(x)
#else
#define kmail_swap_32(x) \
     ((((x) & 0xff000000) >> 24) | (((x) & 0x00ff0000) >>  8) |		      \
      (((x) & 0x0000ff00) <<  8) | (((x) & 0x000000ff) << 24))
#endif


//-----------------------------------------------------------------------------

// Current version of the .index.ids files
#define IDS_VERSION 1002

// The asterisk at the end is important
#define IDS_HEADER "# KMail-Index-IDs V%d\n*"

/**
 * @short an entry in the global message dictionary consisting of a pointer
 * to a folder and the index of a message in the folder.
 */
class KMMsgDictEntry : public KMDictItem
{
public:
  KMMsgDictEntry(const KMFolder *aFolder, int aIndex)
  : folder( aFolder ), index( aIndex )
    {}

  const KMFolder *folder;
  int index;
};

/**
 * @short A "reverse entry", consisting of an array of DictEntry pointers.
 *
 * Each folder (storage) holds such an entry. That's useful for looking up the
 * serial number of a message at a certain index in the folder, since that is the
 * key of these entries.
 */
class KMMsgDictREntry
{
public:
  KMMsgDictREntry(int size = 0)
  {
    array.resize(size);
    memset(array.data(), 0, array.size() * sizeof(KMMsgDictEntry *));  // faster than a loop
    fp = 0;
    swapByteOrder = false;
    baseOffset = 0;
  }

  ~KMMsgDictREntry()
  {
    array.resize(0);
    if (fp)
      fclose(fp);
  }

  void set(int index, KMMsgDictEntry *entry)
  {
    if (index >= 0) {
      int size = array.size();
      if (index >= size) {
        int newsize = TQMAX(size + 25, index + 1);
        array.resize(newsize);
        for (int j = size; j < newsize; j++)
          array.at(j) = 0;
      }
      array.at(index) = entry;
    }
  }

  KMMsgDictEntry *get(int index)
  {
    if (index >= 0 && (unsigned)index < array.size())
      return array.at(index);
    return 0;
  }

  ulong getMsn(int index)
  {
    KMMsgDictEntry *entry = get(index);
    if (entry)
      return entry->key;
    return 0;
  }

  int getRealSize()
  {
    int count = array.size() - 1;
    while (count >= 0) {
      if (array.at(count))
        break;
      count--;
    }
    return count + 1;
  }

  void sync()
  {
    fflush(fp);
  }

public:
  TQMemArray<KMMsgDictEntry *> array;
  FILE *fp;
  bool swapByteOrder;
  off_t baseOffset;
};


static KStaticDeleter<KMMsgDict> msgDict_sd;
KMMsgDict * KMMsgDict::m_self = 0;

//-----------------------------------------------------------------------------

KMMsgDict::KMMsgDict()
{
  int lastSizeOfDict = GlobalSettings::self()->msgDictSizeHint();
  lastSizeOfDict = ( lastSizeOfDict * 11 ) / 10;
  GlobalSettings::self()->setMsgDictSizeHint( 0 );
  dict = new KMDict( lastSizeOfDict );
  nextMsgSerNum = 1;
  m_self = this;
}

//-----------------------------------------------------------------------------

KMMsgDict::~KMMsgDict()
{
  delete dict;
}

//-----------------------------------------------------------------------------

const KMMsgDict* KMMsgDict::instance()
{
  if ( !m_self ) {
    msgDict_sd.setObject( m_self, new KMMsgDict() );
  }
  return m_self;
}

KMMsgDict* KMMsgDict::mutableInstance()
{
  if ( !m_self ) {
    msgDict_sd.setObject( m_self, new KMMsgDict() );
  }
  return m_self;
}

//-----------------------------------------------------------------------------

unsigned long KMMsgDict::getNextMsgSerNum() {
  unsigned long msn = nextMsgSerNum;
  nextMsgSerNum++;
  return msn;
}

void KMMsgDict::deleteRentry(KMMsgDictREntry *entry)
{
  delete entry;
}

unsigned long KMMsgDict::insert(unsigned long msgSerNum,
                                const KMMsgBase *msg, int index)
{
  unsigned long msn = msgSerNum;
  if (!msn) {
    msn = getNextMsgSerNum();
  } else {
    if (msn >= nextMsgSerNum)
      nextMsgSerNum = msn + 1;
  }

  KMFolderIndex* folder = static_cast<KMFolderIndex*>( msg->storage() );
  if ( !folder ) {
    kdDebug(5006) << "KMMsgDict::insert: Cannot insert the message, "
      << "null pointer to storage. Requested serial: " << msgSerNum
      << endl;
    kdDebug(5006) << "  Message info: Subject: " << msg->subject() << ", To: "
      << msg->toStrip() << ", Date: " << msg->dateStr() << endl;
    return 0;
  }

  if (index == -1)
    index = folder->find(msg);

  // Should not happen, indicates id file corruption
  while (dict->find((long)msn)) {
    msn = getNextMsgSerNum();
    folder->setDirty( true ); // rewrite id file
  }

  // Insert into the dict. Don't use dict->replace() as we _know_
  // there is no entry with the same msn, we just made sure.
  KMMsgDictEntry *entry = new KMMsgDictEntry(folder->folder(), index);
  dict->insert((long)msn, entry);

  KMMsgDictREntry *rentry = folder->rDict();
  if (!rentry) {
    rentry = new KMMsgDictREntry();
    folder->setRDict(rentry);
  }
  rentry->set(index, entry);

  return msn;
}

unsigned long KMMsgDict::insert(const KMMsgBase *msg, int index)
{
  unsigned long msn = msg->getMsgSerNum();
  return insert(msn, msg, index);
}

//-----------------------------------------------------------------------------

void KMMsgDict::replace(unsigned long msgSerNum,
		       const KMMsgBase *msg, int index)
{
  KMFolderIndex* folder = static_cast<KMFolderIndex*>( msg->storage() );
  if ( !folder ) {
    kdDebug(5006) << "KMMsgDict::replace: Cannot replace the message serial "
      << "number, null pointer to storage. Requested serial: " << msgSerNum
      << endl;
    kdDebug(5006) << "  Message info: Subject: " << msg->subject() << ", To: "
      << msg->toStrip() << ", Date: " << msg->dateStr() << endl;
    return;
  }

  if ( index == -1 )
    index = folder->find( msg );

  remove( msgSerNum );
  KMMsgDictEntry *entry = new KMMsgDictEntry( folder->folder(), index );
  dict->insert( (long)msgSerNum, entry );

  KMMsgDictREntry *rentry = folder->rDict();
  if (!rentry) {
    rentry = new KMMsgDictREntry();
    folder->setRDict(rentry);
  }
  rentry->set(index, entry);
}

//-----------------------------------------------------------------------------

void KMMsgDict::remove(unsigned long msgSerNum)
{
  long key = (long)msgSerNum;
  KMMsgDictEntry *entry = (KMMsgDictEntry *)dict->find(key);
  if (!entry)
    return;

  if (entry->folder) {
    KMMsgDictREntry *rentry = entry->folder->storage()->rDict();
    if (rentry)
      rentry->set(entry->index, 0);
  }

  dict->remove((long)key);
}

unsigned long KMMsgDict::remove(const KMMsgBase *msg)
{
  unsigned long msn = msg->getMsgSerNum();
  remove(msn);
  return msn;
}

//-----------------------------------------------------------------------------

void KMMsgDict::update(const KMMsgBase *msg, int index, int newIndex)
{
  KMMsgDictREntry *rentry = msg->parent()->storage()->rDict();
  if (rentry) {
    KMMsgDictEntry *entry = rentry->get(index);
    if (entry) {
      entry->index = newIndex;
      rentry->set(index, 0);
      rentry->set(newIndex, entry);
    }
  }
}

//-----------------------------------------------------------------------------

void KMMsgDict::getLocation(unsigned long key,
                            KMFolder **retFolder, int *retIndex) const
{
  KMMsgDictEntry *entry = (KMMsgDictEntry *)dict->find((long)key);
  if (entry) {
    *retFolder = (KMFolder *)entry->folder;
    *retIndex = entry->index;
  } else {
    *retFolder = 0;
    *retIndex = -1;
  }
}

void KMMsgDict::getLocation(const KMMsgBase *msg,
                            KMFolder **retFolder, int *retIndex) const
{
  getLocation(msg->getMsgSerNum(), retFolder, retIndex);
}

void KMMsgDict::getLocation( const KMMessage * msg, KMFolder * *retFolder, int * retIndex ) const
{
  getLocation( msg->toMsgBase().getMsgSerNum(), retFolder, retIndex );
}

//-----------------------------------------------------------------------------

unsigned long KMMsgDict::getMsgSerNum(KMFolder *folder, int index) const
{
  unsigned long msn = 0;
  if ( folder ) {
    KMMsgDictREntry *rentry = folder->storage()->rDict();
    if (rentry)
      msn = rentry->getMsn(index);
  }
  return msn;
}

//-----------------------------------------------------------------------------

//static
TQValueList<unsigned long> KMMsgDict::serNumList(TQPtrList<KMMsgBase> msgList)
{
  TQValueList<unsigned long> ret;
  for ( unsigned int i = 0; i < msgList.count(); i++ ) {
    unsigned long serNum = msgList.at(i)->getMsgSerNum();
    assert( serNum );
    ret.append( serNum );
  }
  return ret;
}

//-----------------------------------------------------------------------------

TQString KMMsgDict::getFolderIdsLocation( const FolderStorage &storage )
{
  return storage.indexLocation() + ".ids";
}

//-----------------------------------------------------------------------------

bool KMMsgDict::isFolderIdsOutdated( const FolderStorage &storage )
{
  bool outdated = false;

  TQFileInfo indexInfo( storage.indexLocation() );
  TQFileInfo idsInfo( getFolderIdsLocation( storage ) );

  if (!indexInfo.exists() || !idsInfo.exists())
    outdated = true;
  if (indexInfo.lastModified() > idsInfo.lastModified())
    outdated = true;

  return outdated;
}

//-----------------------------------------------------------------------------

int KMMsgDict::readFolderIds( FolderStorage& storage )
{
  if ( isFolderIdsOutdated( storage ) )
    return -1;

  TQString filename = getFolderIdsLocation( storage );
  FILE *fp = fopen(TQFile::encodeName(filename), "r+");
  if (!fp)
    return -1;

  int version = 0;
  fscanf(fp, IDS_HEADER, &version);
  if (version != IDS_VERSION) {
    fclose(fp);
    return -1;
  }

  bool swapByteOrder;
  TQ_UINT32 byte_order;
  if (!fread(&byte_order, sizeof(byte_order), 1, fp)) {
    fclose(fp);
    return -1;
  }
  swapByteOrder = (byte_order == 0x78563412);

  TQ_UINT32 count;
  if (!fread(&count, sizeof(count), 1, fp)) {
    fclose(fp);
    return -1;
  }
  if (swapByteOrder)
     count = kmail_swap_32(count);

  // quick consistency check to avoid allocating huge amount of memory
  // due to reading corrupt file (#71549)
  long pos = ftell(fp);       // store current position
  fseek(fp, 0, SEEK_END);
  long fileSize = ftell(fp);  // how large is the file ?
  fseek(fp, pos, SEEK_SET);   // back to previous position

  // the file must at least contain what we try to read below
  if ( (fileSize - pos) < (long)(count * sizeof(TQ_UINT32)) ) {
    fclose(fp);
    return -1;
  }

  KMMsgDictREntry *rentry = new KMMsgDictREntry(count);

  for (unsigned int index = 0; index < count; index++) {
    TQ_UINT32 msn;

    bool readOk = fread(&msn, sizeof(msn), 1, fp);
    if (swapByteOrder)
       msn = kmail_swap_32(msn);

    if (!readOk || dict->find(msn)) {
      for (unsigned int i = 0; i < index; i++) {
        msn = rentry->getMsn(i);
        dict->remove((long)msn);
      }
      delete rentry;
      fclose(fp);
      return -1;
    }

    // We found a serial number that is zero. This is not allowed, and would
    // later cause problems like in bug 149715.
    // Therefore, use a fresh serial number instead
    if ( msn == 0 ) {
      kdWarning(5006) << "readFolderIds(): Found serial number zero at index " << index
                      << " in folder " << filename << endl;
      msn = getNextMsgSerNum();
      Q_ASSERT( msn != 0 );
    }

    // Insert into the dict. Don't use dict->replace() as we _know_
    // there is no entry with the same msn, we just made sure.
    KMMsgDictEntry *entry = new KMMsgDictEntry( storage.folder(), index);
    dict->insert((long)msn, entry);
    if (msn >= nextMsgSerNum)
      nextMsgSerNum = msn + 1;

    rentry->set(index, entry);
  }
  // Remember how many items we put into the dict this time so we can create
  // it with an appropriate size next time.
  GlobalSettings::setMsgDictSizeHint( GlobalSettings::msgDictSizeHint() + count );

  fclose(fp);
  storage.setRDict(rentry);

  return 0;
}

//-----------------------------------------------------------------------------

KMMsgDictREntry *KMMsgDict::openFolderIds( const FolderStorage& storage, bool truncate)
{
  KMMsgDictREntry *rentry = storage.rDict();
  if (!rentry) {
    rentry = new KMMsgDictREntry();
    storage.setRDict(rentry);
  }

  if (!rentry->fp) {
    TQString filename = getFolderIdsLocation( storage );
    FILE *fp = truncate ? 0 : fopen(TQFile::encodeName(filename), "r+");
    if (fp)
    {
      int version = 0;
      fscanf(fp, IDS_HEADER, &version);
      if (version == IDS_VERSION)
      {
         TQ_UINT32 byte_order = 0;
         fread(&byte_order, sizeof(byte_order), 1, fp);
         rentry->swapByteOrder = (byte_order == 0x78563412);
      }
      else
      {
         fclose(fp);
         fp = 0;
      }
    }

    if (!fp)
    {
      fp = fopen(TQFile::encodeName(filename), "w+");
      if (!fp)
      {
        kdDebug(5006) << "Dict '" << filename
                      << "' cannot open with folder " << storage.label() << ": "
                      << strerror(errno) << " (" << errno << ")" << endl;
         delete rentry;
         rentry = 0;
         return 0;
      }
      fprintf(fp, IDS_HEADER, IDS_VERSION);
      TQ_UINT32 byteOrder = 0x12345678;
      fwrite(&byteOrder, sizeof(byteOrder), 1, fp);
      rentry->swapByteOrder = false;
    }
    rentry->baseOffset = ftell(fp);
    rentry->fp = fp;
  }

  return rentry;
}

//-----------------------------------------------------------------------------

int KMMsgDict::writeFolderIds( const FolderStorage &storage )
{
  KMMsgDictREntry *rentry = openFolderIds( storage, true );
  if (!rentry)
    return 0;
  FILE *fp = rentry->fp;

  fseek(fp, rentry->baseOffset, SEEK_SET);
  // kdDebug(5006) << "Dict writing for folder " << storage.label() << endl;
  TQ_UINT32 count = rentry->getRealSize();
  if (!fwrite(&count, sizeof(count), 1, fp)) {
    kdDebug(5006) << "Dict cannot write count with folder " << storage.label() << ": "
                  << strerror(errno) << " (" << errno << ")" << endl;
    return -1;
  }

  for (unsigned int index = 0; index < count; index++) {
    TQ_UINT32 msn = rentry->getMsn(index);
    if (!fwrite(&msn, sizeof(msn), 1, fp))
      return -1;
    if ( msn == 0 ) {
      kdWarning(5006) << "writeFolderIds(): Serial number of message at index "
                      << index << " is zero in folder " << storage.label() << endl;
    }
  }

  rentry->sync();

  off_t eof = ftell(fp);
  TQString filename = getFolderIdsLocation( storage );
  truncate(TQFile::encodeName(filename), eof);
  fclose(rentry->fp);
  rentry->fp = 0;

  return 0;
}

//-----------------------------------------------------------------------------

int KMMsgDict::touchFolderIds( const FolderStorage &storage )
{
  KMMsgDictREntry *rentry = openFolderIds( storage, false);
  if (rentry) {
    rentry->sync();
    fclose(rentry->fp);
    rentry->fp = 0;
  }
  return 0;
}

//-----------------------------------------------------------------------------

int KMMsgDict::appendToFolderIds( FolderStorage& storage, int index)
{
  KMMsgDictREntry *rentry = openFolderIds( storage, false);
  if (!rentry)
    return 0;
  FILE *fp = rentry->fp;

//  kdDebug(5006) << "Dict appending for folder " << storage.label() << endl;

  fseek(fp, rentry->baseOffset, SEEK_SET);
  TQ_UINT32 count;
  if (!fread(&count, sizeof(count), 1, fp)) {
    kdDebug(5006) << "Dict cannot read count for folder " << storage.label() << ": "
                  << strerror(errno) << " (" << errno << ")" << endl;
    return 0;
  }
  if (rentry->swapByteOrder)
     count = kmail_swap_32(count);

  count++;

  if (rentry->swapByteOrder)
     count = kmail_swap_32(count);
  fseek(fp, rentry->baseOffset, SEEK_SET);
  if (!fwrite(&count, sizeof(count), 1, fp)) {
    kdDebug(5006) << "Dict cannot write count for folder " << storage.label() << ": "
                  << strerror(errno) << " (" << errno << ")" << endl;
    return 0;
  }

  long ofs = (count - 1) * sizeof(ulong);
  if (ofs > 0)
    fseek(fp, ofs, SEEK_CUR);

  TQ_UINT32 msn = rentry->getMsn(index);
  if (rentry->swapByteOrder)
     msn = kmail_swap_32(msn);
  if (!fwrite(&msn, sizeof(msn), 1, fp)) {
    kdDebug(5006) << "Dict cannot write count for folder " << storage.label() << ": "
                  << strerror(errno) << " (" << errno << ")" << endl;
    return 0;
  }

  rentry->sync();
  fclose(rentry->fp);
  rentry->fp = 0;

  return 0;
}

//-----------------------------------------------------------------------------

bool KMMsgDict::hasFolderIds( const FolderStorage& storage )
{
  return storage.rDict() != 0;
}

//-----------------------------------------------------------------------------

bool KMMsgDict::removeFolderIds( FolderStorage& storage )
{
  storage.setRDict( 0 );
  TQString filename = getFolderIdsLocation( storage );
  return unlink( TQFile::encodeName( filename) );
}