/*
 * Copyright (C) 2021 Maneesh P M <manu.pm55@gmail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
 * NON-INFRINGEMENT.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 *
 */

#include <zim/error.h>
#define ZIM_PRIVATE

#include "zim/suggestion_iterator.h"
#include "suggestion_internal.h"
#include "./smartptr.h"

#include <stdexcept>

namespace zim
{

class SuggestionIterator::Impl {
#if defined(LIBZIM_WITH_XAPIAN)
    std::shared_ptr<SuggestionDataBase> mp_db;
    std::shared_ptr<Xapian::MSet> mp_mset;
    Xapian::MSetIterator iterator;

    // cached/memoized data
    mutable std::string _entryPath;
    mutable bool _entryPathValid;
    mutable ValuePtr<Entry> _entry;

public:
    Impl(std::shared_ptr<SuggestionDataBase> p_db,
         std::shared_ptr<Xapian::MSet> p_mset,
         Xapian::MSetIterator iterator) :
        mp_db(p_db),
        mp_mset(p_mset),
        iterator(iterator),
        _entryPathValid(false)
    {};

    void operator++() {
        ++iterator;
        _entry.reset();
        _entryPathValid = false;
    }

    void operator--() {
        --iterator;
        _entry.reset();
        _entryPathValid = false;
    }

    SuggestionItem get_suggestion() const {
        return SuggestionItem(getIndexTitle(),
                              getIndexPath(),
                              getIndexSnippet());
    }

    std::string get_entry_path() const {
        if ( !_entryPathValid ) {
            if (iterator == mp_mset->end()) {
                throw std::runtime_error("Cannot get entry for end iterator");
            }
            _entryPath = iterator.get_document().get_data();
            _entryPathValid = true;
        }
        return _entryPath;
    }

    Entry& get_entry() const {
        if (!_entry) {
            const auto path = get_entry_path();
            _entry.reset(new Entry(mp_db->m_archive.getEntryByPath(path)));
        }
        return *_entry.get();
    }

    bool operator==(const Impl& other) const {
        return (mp_db == other.mp_db
            &&  mp_mset == other.mp_mset
            &&  iterator == other.iterator);
    }

private:
    std::string getIndexPath() const;
    std::string getIndexTitle() const;
    std::string getIndexSnippet() const;
#endif
};

#if defined(LIBZIM_WITH_XAPIAN)
std::string SuggestionIterator::Impl::getIndexPath() const
{
    try {
        std::string path = get_entry_path();
        bool hasNewNamespaceScheme = mp_db->m_archive.hasNewNamespaceScheme();

        std::string dbDataType = mp_db->m_database.get_metadata("data");
        if (dbDataType.empty()) {
            dbDataType = "fullPath";
        }

        // If the archive has new namespace scheme and the type of its indexed
        // data is `fullPath` we return only the `path` without namespace
        if (hasNewNamespaceScheme && dbDataType == "fullPath") {
            path = path.substr(2);
        }
        return path;
    } catch ( Xapian::DatabaseError& e) {
        throw ZimFileFormatError(e.get_description());
    }
}

std::string SuggestionIterator::Impl::getIndexTitle() const {
    try {
        return get_entry().getTitle();
    } catch (...) {
        return "";
    }
}

std::string SuggestionIterator::Impl::getIndexSnippet() const {
    try {
        return mp_mset->snippet(getIndexTitle(), 500, mp_db->m_stemmer);
    } catch(...) {
        return "";
    }
}
#endif  // LIBZIM_WITH_XAPIAN

SuggestionIterator::~SuggestionIterator() = default;
SuggestionIterator::SuggestionIterator(SuggestionIterator&& it) = default;
SuggestionIterator& SuggestionIterator::operator=(SuggestionIterator&& it) = default;

SuggestionIterator::SuggestionIterator(RangeIterator rangeIterator)
  : mp_rangeIterator(new RangeIterator(rangeIterator))
{}

#if defined(LIBZIM_WITH_XAPIAN)
SuggestionIterator::SuggestionIterator(Impl* impl)
  : mp_impl(impl)
{}
#endif  // LIBZIM_WITH_XAPIAN

SuggestionIterator::SuggestionIterator(const SuggestionIterator& it)
{
#if defined(LIBZIM_WITH_XAPIAN)
    if (it.mp_impl) {
        mp_impl.reset(new Impl(*it.mp_impl));
    }
#endif  // LIBZIM_WITH_XAPIAN

    if (it.mp_rangeIterator) {
        mp_rangeIterator.reset(new RangeIterator(*it.mp_rangeIterator));
    }
}

SuggestionIterator& SuggestionIterator::operator=(const SuggestionIterator& it) {
    if ( this != &it ) {
      this->~SuggestionIterator();
      new(this) SuggestionIterator(it);
    }
    return *this;
}

bool SuggestionIterator::operator==(const SuggestionIterator& it) const {
    if (mp_rangeIterator && it.mp_rangeIterator) {
        return (*mp_rangeIterator == *it.mp_rangeIterator);
    }

#if defined(LIBZIM_WITH_XAPIAN)
    if (mp_impl && it.mp_impl) {
        return (*mp_impl == *it.mp_impl);
    }
#endif  // LIBZIM_WITH_XAPIAN

    return false;
}

bool SuggestionIterator::operator!=(const SuggestionIterator& it) const {
    return ! (*this == it);
}

SuggestionIterator& SuggestionIterator::operator++() {
#if defined(LIBZIM_WITH_XAPIAN)
    if (mp_impl) {
        ++(*mp_impl);
    }
#endif  // LIBZIM_WITH_XAPIAN

    if (mp_rangeIterator) {
        ++(*mp_rangeIterator);
    }
    m_suggestionItem.reset();
    return *this;
}

SuggestionIterator SuggestionIterator::operator++(int) {
    SuggestionIterator it = *this;
    operator++();
    return it;
}

SuggestionIterator& SuggestionIterator::operator--() {
#if defined(LIBZIM_WITH_XAPIAN)
    if (mp_impl) {
        --(*mp_impl);
    }
#endif  // LIBZIM_WITH_XAPIAN

    if (mp_rangeIterator) {
        --(*mp_rangeIterator);
    }
    m_suggestionItem.reset();
    return *this;
}

SuggestionIterator SuggestionIterator::operator--(int) {
    SuggestionIterator it = *this;
    operator--();
    return it;
}

Entry SuggestionIterator::getEntry() const {
#if defined(LIBZIM_WITH_XAPIAN)
    if (mp_impl) {
        try {
            return mp_impl->get_entry();
        } catch ( Xapian::DatabaseError& e) {
            throw ZimFileFormatError(e.get_description());
        }
    }
#endif  // LIBZIM_WITH_XAPIAN

    if (mp_rangeIterator) {
        return **mp_rangeIterator;
    }
    throw std::runtime_error("Cannot dereference iterator");
}

SuggestionItem* SuggestionIterator::instantiateSuggestion() const
{
#if defined(LIBZIM_WITH_XAPIAN)
    if (mp_impl) {
        return new SuggestionItem(mp_impl->get_suggestion());
    }
#endif  // LIBZIM_WITH_XAPIAN

    if (mp_rangeIterator) {
        return new SuggestionItem((*mp_rangeIterator)->getTitle(),
                                  (*mp_rangeIterator)->getPath());
    }

    return nullptr;
}

const SuggestionItem& SuggestionIterator::operator*() {
    if (!m_suggestionItem) {
      m_suggestionItem.reset(instantiateSuggestion());

      if (!m_suggestionItem){
          throw std::runtime_error("Cannot dereference iterator");
      }
    }
    return *m_suggestionItem;
}

const SuggestionItem* SuggestionIterator::operator->() {
    operator*();
    return m_suggestionItem.get();
}

////////////////////////////////////////////////////////////////////////////////
// SuggestionIterator related methods of SuggestionResultSet
////////////////////////////////////////////////////////////////////////////////

SuggestionResultSet::iterator SuggestionResultSet::begin() const
{
#if defined(LIBZIM_WITH_XAPIAN)
    if ( ! mp_entryRange ) {
        return iterator(new iterator::Impl(mp_internalDb, mp_mset, mp_mset->begin()));
    }
#endif  // LIBZIM_WITH_XAPIAN

    return iterator(mp_entryRange->begin());
}

SuggestionResultSet::iterator SuggestionResultSet::end() const
{
#if defined(LIBZIM_WITH_XAPIAN)
    if ( ! mp_entryRange ) {
        return iterator(new iterator::Impl(mp_internalDb, mp_mset, mp_mset->end()));
    }
#endif  // LIBZIM_WITH_XAPIAN

    return iterator(mp_entryRange->end());
}

} // namespace zim
