#include "journalimp.hpp"

#include <iterator>

#include <components/esm3/esmreader.hpp>
#include <components/esm3/esmwriter.hpp>
#include <components/esm3/journalentry.hpp>
#include <components/esm3/queststate.hpp>

#include <components/misc/strings/algorithm.hpp>

#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"

#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"

namespace MWDialogue
{
    Quest& Journal::getOrStartQuest(const ESM::RefId& id)
    {
        TQuestContainer::iterator iter = mQuests.find(id);

        if (iter == mQuests.end())
            iter = mQuests.emplace(id, Quest(id)).first;

        return iter->second;
    }

    Quest* Journal::getQuestOrNull(const ESM::RefId& id)
    {
        TQuestContainer::iterator iter = mQuests.find(id);
        if (iter == mQuests.end())
        {
            return nullptr;
        }

        return &(iter->second);
    }

    Topic& Journal::getTopic(const ESM::RefId& id)
    {
        TTopicContainer::iterator iter = mTopics.find(id);

        if (iter == mTopics.end())
        {
            std::pair<TTopicContainer::iterator, bool> result = mTopics.insert(std::make_pair(id, Topic(id)));

            iter = result.first;
        }

        return iter->second;
    }

    bool Journal::isThere(const ESM::RefId& topicId, const ESM::RefId& infoId) const
    {
        if (const ESM::Dialogue* dialogue
            = MWBase::Environment::get().getESMStore()->get<ESM::Dialogue>().search(topicId))
        {
            if (infoId.empty())
                return true;

            for (ESM::Dialogue::InfoContainer::const_iterator iter(dialogue->mInfo.begin());
                 iter != dialogue->mInfo.end(); ++iter)
                if (iter->mId == infoId)
                    return true;
        }

        return false;
    }

    Journal::Journal() {}

    void Journal::clear()
    {
        mJournal.clear();
        mQuests.clear();
        mTopics.clear();
    }

    void Journal::addEntry(const ESM::RefId& id, int index, const MWWorld::Ptr& actor)
    {
        // bail out if we already have heard this...
        const ESM::RefId& infoId = JournalEntry::idFromIndex(id, index);
        for (TEntryIter i = mJournal.begin(); i != mJournal.end(); ++i)
            if (i->mTopic == id && i->mInfoId == infoId)
            {
                if (getJournalIndex(id) < index)
                {
                    setJournalIndex(id, index);
                    MWBase::Environment::get().getWindowManager()->messageBox("#{sJournalEntry}");
                }
                return;
            }

        StampedJournalEntry entry = StampedJournalEntry::makeFromQuest(id, index, actor);

        Quest& quest = getOrStartQuest(id);
        if (quest.addEntry(entry)) // we are doing slicing on purpose here
        {
            // Restart all "other" quests with the same name as well
            std::string_view name = quest.getName();
            for (auto& it : mQuests)
            {
                if (it.second.isFinished() && Misc::StringUtils::ciEqual(it.second.getName(), name))
                    it.second.setFinished(false);
            }
        }

        // there is no need to show empty entries in journal
        if (!entry.getText().empty())
        {
            mJournal.push_back(entry);
            MWBase::Environment::get().getWindowManager()->messageBox("#{sJournalEntry}");
        }
    }

    void Journal::setJournalIndex(const ESM::RefId& id, int index)
    {
        Quest& quest = getOrStartQuest(id);

        quest.setIndex(index);
    }

    void Journal::addTopic(const ESM::RefId& topicId, const ESM::RefId& infoId, const MWWorld::Ptr& actor)
    {
        Topic& topic = getTopic(topicId);

        JournalEntry entry(topicId, infoId, actor);
        entry.mActorName = actor.getClass().getName(actor);
        topic.addEntry(entry);
    }

    void Journal::removeLastAddedTopicResponse(const ESM::RefId& topicId, std::string_view actorName)
    {
        Topic& topic = getTopic(topicId);

        topic.removeLastAddedResponse(actorName);

        if (topic.begin() == topic.end())
            mTopics.erase(mTopics.find(topicId)); // All responses removed -> remove topic
    }

    int Journal::getJournalIndex(const ESM::RefId& id) const
    {
        TQuestContainer::const_iterator iter = mQuests.find(id);

        if (iter == mQuests.end())
            return 0;

        return iter->second.getIndex();
    }

    Journal::TEntryIter Journal::begin() const
    {
        return mJournal.begin();
    }

    Journal::TEntryIter Journal::end() const
    {
        return mJournal.end();
    }

    Journal::TQuestIter Journal::questBegin() const
    {
        return mQuests.begin();
    }

    Journal::TQuestIter Journal::questEnd() const
    {
        return mQuests.end();
    }

    Journal::TTopicIter Journal::topicBegin() const
    {
        return mTopics.begin();
    }

    Journal::TTopicIter Journal::topicEnd() const
    {
        return mTopics.end();
    }

    int Journal::countSavedGameRecords() const
    {
        int count = static_cast<int>(mQuests.size());

        for (TQuestIter iter(mQuests.begin()); iter != mQuests.end(); ++iter)
            count += std::distance(iter->second.begin(), iter->second.end());

        count += std::distance(mJournal.begin(), mJournal.end());

        for (TTopicIter iter(mTopics.begin()); iter != mTopics.end(); ++iter)
            count += std::distance(iter->second.begin(), iter->second.end());

        return count;
    }

    void Journal::write(ESM::ESMWriter& writer, Loading::Listener& progress) const
    {
        for (TQuestIter iter(mQuests.begin()); iter != mQuests.end(); ++iter)
        {
            const Quest& quest = iter->second;

            ESM::QuestState state;
            quest.write(state);
            writer.startRecord(ESM::REC_QUES);
            state.save(writer);
            writer.endRecord(ESM::REC_QUES);

            for (Topic::TEntryIter entryIter(quest.begin()); entryIter != quest.end(); ++entryIter)
            {
                ESM::JournalEntry entry;
                entry.mType = ESM::JournalEntry::Type_Quest;
                entry.mTopic = quest.getTopic();
                entryIter->write(entry);
                writer.startRecord(ESM::REC_JOUR);
                entry.save(writer);
                writer.endRecord(ESM::REC_JOUR);
            }
        }

        for (TEntryIter iter(mJournal.begin()); iter != mJournal.end(); ++iter)
        {
            ESM::JournalEntry entry;
            entry.mType = ESM::JournalEntry::Type_Journal;
            iter->write(entry);
            writer.startRecord(ESM::REC_JOUR);
            entry.save(writer);
            writer.endRecord(ESM::REC_JOUR);
        }

        for (TTopicIter iter(mTopics.begin()); iter != mTopics.end(); ++iter)
        {
            const Topic& topic = iter->second;

            for (Topic::TEntryIter entryIter(topic.begin()); entryIter != topic.end(); ++entryIter)
            {
                ESM::JournalEntry entry;
                entry.mType = ESM::JournalEntry::Type_Topic;
                entry.mTopic = topic.getTopic();
                entryIter->write(entry);
                writer.startRecord(ESM::REC_JOUR);
                entry.save(writer);
                writer.endRecord(ESM::REC_JOUR);
            }
        }
    }

    void Journal::readRecord(ESM::ESMReader& reader, uint32_t type)
    {
        if (type == ESM::REC_JOUR)
        {
            ESM::JournalEntry record;
            record.load(reader);

            if (isThere(record.mTopic, record.mInfo))
                switch (record.mType)
                {
                    case ESM::JournalEntry::Type_Quest:

                        getOrStartQuest(record.mTopic).insertEntry(record);
                        break;

                    case ESM::JournalEntry::Type_Journal:

                        mJournal.push_back(record);
                        break;

                    case ESM::JournalEntry::Type_Topic:

                        getTopic(record.mTopic).insertEntry(record);
                        break;
                }
        }
        else if (type == ESM::REC_QUES)
        {
            ESM::QuestState record;
            record.load(reader);

            if (isThere(record.mTopic))
            {
                std::pair<TQuestContainer::iterator, bool> result
                    = mQuests.insert(std::make_pair(record.mTopic, record));
                // reapply quest index, this is to handle users upgrading from only
                // Morrowind.esm (no quest states) to Morrowind.esm + Tribunal.esm
                result.first->second.setIndex(record.mState);
            }
        }
    }
}
