/* This file is (c) 2014 Abs62
 * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */
//xapian.h must at the first in the  include header files to avoid collision with other macro definition.
#include "xapian.h"
#include <cstdlib>
#include "fulltextsearch.hh"
#include "folding.hh"
#include "ftshelpers.hh"
#include "dictfile.hh"
#include "utils.hh"

#include <vector>
#include <string>

using std::vector;
using std::string;

DEF_EX( exUserAbort, "User abort", Dictionary::Ex )

namespace FtsHelpers {
// finished  reversed   dehsinif
const static std::string finish_mark = std::string( "dehsinif" );

bool ftsIndexIsOldOrBad( BtreeIndexing::BtreeDictionary * dict )
{
  try {
    const QByteArray encodedPath = QFile::encodeName( QString::fromStdString( dict->ftsIndexName() ) );
    const Xapian::Database db( encodedPath.toStdString() );

    auto docid = db.get_lastdocid();
    if ( docid == 0 ) {
      return true; // No documents in database
    }
    auto document = db.get_document( docid );

    const string lastDoc = document.get_data();
    return lastDoc != finish_mark;
    //use a special document to mark the end of the index.
  }
  catch ( Xapian::Error & e ) {
    qWarning() << e.get_description().c_str();
    //the file is corrupted,remove it.
    Utils::Fs::removeDirectory( dict->ftsIndexName() );
    return true;
  }
  catch ( ... ) {
    return true;
  }
}

void makeFTSIndex( BtreeIndexing::BtreeDictionary * dict, QAtomicInt & isCancelled )
{
  const QMutexLocker _( &dict->getFtsMutex() );

  //check the index again.
  if ( dict->haveFTSIndex() ) {
    qDebug() << "fts index already exists";
    return;
  }

  try {
    if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) {
      qDebug() << "user abort the fts index creation";
      throw exUserAbort();
    }

    // Open the database for update, creating a new database if necessary.
    const std::string path       = dict->ftsIndexName() + "_temp";
    const QByteArray encodedPath = QFile::encodeName( QString::fromStdString( path ) );
    Xapian::WritableDatabase db( encodedPath.toStdString(), Xapian::DB_CREATE_OR_OPEN );

    Xapian::TermGenerator indexer;
    //  Xapian::Stem stemmer("english");
    //  indexer.set_stemmer(stemmer);
    //  indexer.set_stemming_strategy(indexer.STEM_SOME_FULL_POS);
    indexer.set_flags( Xapian::TermGenerator::FLAG_CJK_NGRAM );

    BtreeIndexing::IndexedWords indexedWords;

    QSet< uint32_t > setOfOffsets;
    setOfOffsets.reserve( dict->getArticleCount() );

    dict->findArticleLinks( nullptr, &setOfOffsets, nullptr, &isCancelled );

    if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) {
      throw exUserAbort();
    }

    // More efficient way to get a sorted list of offsets
    std::vector< uint32_t > offsets;
    offsets.assign( setOfOffsets.begin(), setOfOffsets.end() );
    // Sort offsets to allow for binary search resume
    std::sort( offsets.begin(), offsets.end() );

    // Free memory
    setOfOffsets.clear();

    if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) {
      throw exUserAbort();
    }

    // incremental build the index.
    // get the last address.
    bool skip            = true;
    uint32_t lastAddress = -1;
    try {
      if ( db.get_lastdocid() > 0 ) {
        Xapian::Document lastDoc = db.get_document( db.get_lastdocid() );
        lastAddress              = atoi( lastDoc.get_data().c_str() );
      }
      else {
        skip = false;
      }
    }
    catch ( Xapian::Error & e ) {
      qDebug() << "get last doc failed: " << e.get_description().c_str();
      skip = false;
    }

    long indexedDoc = 0L;

    auto start_it = offsets.cbegin();
    if ( skip ) {
      start_it         = std::upper_bound( offsets.cbegin(), offsets.cend(), lastAddress );
      long num_skipped = std::distance( offsets.cbegin(), start_it );
      qDebug() << "Resuming FTS indexing from offset" << ( start_it != offsets.cend() ? *start_it : -1 ) << "at index"
               << num_skipped;
      indexedDoc += num_skipped;
    }

    for ( auto it = start_it; it != offsets.cend(); ++it ) {
      const auto & address = *it;
      indexedDoc++;

      if ( Utils::AtomicInt::loadAcquire( isCancelled ) || !dict->canFTS() ) {
        qDebug() << "user abort the fts index creation" << isCancelled << "canFTS:" << dict->canFTS();
        throw exUserAbort();
      }

      QString headword, articleStr;

      dict->getArticleText( address, headword, articleStr );

      Xapian::Document doc;

      indexer.set_document( doc );

      indexer.index_text( Folding::applyForIndex( articleStr ) );
      indexer.index_text( Folding::applyForIndex( headword ) );

      doc.set_data( std::to_string( address ) );
      // Add the document to the database.
      db.add_document( doc );
      dict->setIndexedFtsDoc( indexedDoc );
    }

    //add a special document to mark the end of the index.
    Xapian::Document doc;
    doc.set_data( finish_mark );
    // Add the document to the database.
    db.add_document( doc );

    // Free memory
    offsets.clear();

    db.commit();

    const QByteArray encodedFtsPath = QFile::encodeName( QString::fromStdString( dict->ftsIndexName() ) );
    db.compact( encodedFtsPath.toStdString() );

    db.close();

    Utils::Fs::removeDirectory( dict->ftsIndexName() + "_temp" );
  }
  catch ( Xapian::Error & e ) {
    qWarning() << "create xapian index failed:" << QString::fromStdString( e.get_description() );
    Utils::Fs::removeDirectory( dict->ftsIndexName() );
    Utils::Fs::removeDirectory( dict->ftsIndexName() + "_temp" );
    throw;
  }
  catch ( ... ) {
    Utils::Fs::removeDirectory( dict->ftsIndexName() + "_temp" );
    throw;
  }
}

void FTSResultsRequest::run()
{
  if ( !dict.ensureInitDone().empty() ) {
    setErrorString( QString::fromUtf8( dict.ensureInitDone().c_str() ) );
    finish();
    return;
  }

  try {
    if ( dict.haveFTSIndex() ) {
      //no need to parse the search string,  use xapian directly.
      //if the search mode is wildcard, change xapian search query flag?
      // Open the database for searching.
      const QByteArray encodedPath = QFile::encodeName( QString::fromStdString( dict.ftsIndexName() ) );
      Xapian::Database db( encodedPath.toStdString() );

      // Start an enquire session.
      Xapian::Enquire enquire( db );

      // Combine the rest of the command line arguments with spaces between
      // them, so that simple queries don't have to be quoted at the shell
      // level.
      string query_string( Folding::applyForIndex( searchString ) );

      // Parse the query string to produce a Xapian::Query object.
      Xapian::QueryParser qp;
      qp.set_database( db );
      qp.set_default_op( Xapian::Query::op::OP_AND );
      int flag =
        Xapian::QueryParser::FLAG_DEFAULT | Xapian::QueryParser::FLAG_PURE_NOT | Xapian::QueryParser::FLAG_CJK_NGRAM;
      if ( searchMode == FTS::Wildcards ) {
        flag = flag | Xapian::QueryParser::FLAG_WILDCARD;
        qp.set_max_expansion( 1 );
      }
      Xapian::Query query = qp.parse_query( query_string, flag );
      qDebug() << "Parsed query is: " << query.get_description().c_str();

      // Find the top 100 results for the query.
      enquire.set_query( query );
      Xapian::MSet matches = enquire.get_mset( 0, 100 );

      emit matchCount( matches.get_matches_estimated() );
      // Display the results.
      qDebug() << matches.get_matches_estimated() << " results found.\n";
      qDebug() << "Matches " << matches.size() << ":\n\n";
      QList< uint32_t > offsetsForHeadwords;
      for ( Xapian::MSetIterator i = matches.begin(); i != matches.end(); ++i ) {
        qDebug() << i.get_rank() + 1 << ": " << i.get_weight() << " docid=" << *i << " ["
                 << i.get_document().get_data().c_str() << "]";
        if ( i.get_document().get_data() == finish_mark ) {
          continue;
        }
        offsetsForHeadwords.append( atoi( i.get_document().get_data().c_str() ) );
      }

      if ( !offsetsForHeadwords.isEmpty() ) {
        QList< QString > headwords;
        QMutexLocker _( &dataMutex );
        QString id = QString::fromUtf8( dict.getId().c_str() );
        dict.getHeadwordsFromOffsets( offsetsForHeadwords, headwords, &isCancelled );
        for ( const auto & headword : std::as_const( headwords ) ) {
          foundHeadwords->append( FTS::FtsHeadword( headword, id, QStringList(), matchCase ) );
        }
      }
    }
    else {
      //if no fulltext index,just returned.
      qWarning() << "There is no fulltext index right now.";
      finish();
      return;
    }

    if ( foundHeadwords && !foundHeadwords->empty() ) {
      QMutexLocker _( &dataMutex );
      data.resize( sizeof( foundHeadwords ) );
      memcpy( &data.front(), &foundHeadwords, sizeof( foundHeadwords ) );
      foundHeadwords = nullptr;
      hasAnyData     = true;
    }
  }
  catch ( const Xapian::Error & e ) {
    qWarning() << e.get_description().c_str();
  }
  catch ( std::exception & ex ) {
    qWarning( "FTS: Failed full-text search for \"%s\", reason: %s", dict.getName().c_str(), ex.what() );
    // Results not loaded -- we don't set the hasAnyData flag then
  }

  finish();
}

} // namespace FtsHelpers
