#include "thread_stop_diff.h"

#include "maps/wikimap/mapspro/services/editor/src/objects/object.h"

#include "maps/wikimap/mapspro/services/editor/src/commit.h"
#include "maps/wikimap/mapspro/services/editor/src/objects_cache.h"
#include "maps/wikimap/mapspro/services/editor/src/srv_attrs/calc.h"
#include "maps/wikimap/mapspro/services/editor/src/configs/categories_strings.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/category_traits.h"
#include "maps/wikimap/mapspro/services/editor/src/configs/config.h"

#include <yandex/maps/wiki/configs/editor/categories.h>

namespace maps {
namespace wiki {
namespace {

const std::string STR_ADDED = "added";
const std::string STR_REMOVED = "removed";
const std::string STR_MODIFIED = "modified";
const std::string STR_CONTEXT = "context";

struct ThreadStopRecord
{
    TOid id;
    std::string title;
    revision::Attributes attrs;
};
typedef std::vector<ThreadStopRecord> ThreadStopRecords;

ThreadStopRecord
extractRecord(const GeoObject* threadStop, ObjectsCache* cache) {
    ASSERT(threadStop && threadStop->categoryId() == CATEGORY_TRANSPORT_THREAD_STOP);
    StringMap attrs;
    for (const auto& attr : threadStop->attributes()) {
        const auto& value  = attr.value();
        if (!value.empty()) {
            attrs.insert({attr.id(), value});
        }
    }
    const auto& stationsRange = threadStop->masterRelations().range(ROLE_ASSIGNED_THREAD_STOP);
    ASSERT(stationsRange.size() == 1);
    std::string  stationName =
        cache
            ? srv_attrs::objectNameByType(stationsRange.begin()->id(), NAME_TYPE_OFFICIAL, *cache)
            : s_emptyString;
    return ThreadStopRecord{
        threadStop->id(),
        stationName,
        attrs
    };
}

class BrokenSequence : public InternalErrorException
{
};

ThreadStopRecords
extractRecords(ObjectPtr thread, ObjectsCache* cache)
{
    if (!thread || thread->isDeleted()) {
        return ThreadStopRecords{};
    }
    TOIds previousStops;
    const auto& stopsRange = thread->slaveRelations().range(ROLE_PART);
    if (stopsRange.empty()) {
        return {};
    }
    const GeoObject* head = nullptr;
    head = stopsRange.begin()->relative();
    while (head) {
        auto previousRange = head->slaveRelations().range(ROLE_PREVIOUS);
        if (previousRange.size() >= 2) {
            WARN() << "Two or more previous thread_stops found for:" << head->id();
            throw BrokenSequence();
        }
        if (previousRange.empty()) {
            break;
        }
        head = previousRange.begin()->relative();
    }
    ASSERT(head);
    ThreadStopRecords orderedStopRecords;
    while (head) {
        orderedStopRecords.push_back(extractRecord(head, cache));
        auto nextRange = head->masterRelations().range(ROLE_PREVIOUS);
        if (nextRange.size() >= 2) {
            WARN() << "Two or more next thread_stops found for:" << head->id();
            throw BrokenSequence();
        }
        if (!nextRange.empty()) {
            head = nextRange.begin()->relative();
        } else {
            head = nullptr;
        }
    }
    return orderedStopRecords;
}

std::optional<revision::AttributesDiff>
createAttributesDiff(
        const revision::Attributes& oldAttrs,
        const revision::Attributes& newAttrs)
{
    auto diff = revision::createAttributesDiff(oldAttrs, newAttrs);

    const auto& editorCfg = cfg()->editor();
    auto isDisplayableAttr = [&](const revision::AttributesDiff::value_type& attrDiff)
    {
        if (!editorCfg->isAttributeDefined(attrDiff.first)) {
            return false;
        }
        return !editorCfg->attribute(attrDiff.first)->system();
    };
    if (diff && std::none_of(diff->begin(), diff->end(), isDisplayableAttr)) {
        return std::nullopt;
    }

    return diff;
}

ThreadStopDiffRecord
rightRec(const ThreadStopRecord& rec)
{
    return {
        ThreadStopDiffRecord::Type::Removed,
        rec.id,
        rec.title,
        std::nullopt
    };
}

ThreadStopDiffRecord
leftRec(const ThreadStopRecord& rec)
{
    return {
        ThreadStopDiffRecord::Type::Added,
        rec.id,
        rec.title,
        createAttributesDiff({}, rec.attrs)
    };
}

ThreadStopDiffRecord
sameRec(const ThreadStopRecord& recL, const ThreadStopRecord& recR)
{
    return {
        recL.attrs == recR.attrs
            ? ThreadStopDiffRecord::Type::Context
            : ThreadStopDiffRecord::Type::Modified,
        recL.id,
        recL.title,
        createAttributesDiff(recR.attrs, recL.attrs)
    };
}

bool
operator ==(const ThreadStopRecord& recL, const ThreadStopRecord& recR)
{
    return recL.id == recR.id;
}

template <typename InputIterator1, typename InputIterator2, typename OutputIterator>
void
diff(InputIterator1 firstLeft, InputIterator1 lastLeft,
     InputIterator2 firstRight, InputIterator2 lastRight,
     OutputIterator output)
{
    typedef typename std::iterator_traits<InputIterator1>::value_type LeftType;
    typedef typename std::iterator_traits<InputIterator2>::value_type RigthType;
    while (firstLeft != lastLeft || firstRight != lastRight) {
        if (firstLeft == lastLeft) {
            *output = rightRec(*firstRight);
            ++firstRight;
        } else if (firstRight == lastRight) {
            *output = leftRec(*firstLeft);
            ++firstLeft;
        } else if (*firstRight == *firstLeft) {
            *output = sameRec(*firstLeft, *firstRight);
            ++firstRight;
            ++firstLeft;
        } else {
            auto itRight = std::find_if(firstRight, lastRight,
                [&firstLeft](const RigthType& x) { return x == *firstLeft;});
            if (itRight == lastRight) {
                *output = leftRec(*firstLeft);
                ++firstLeft;
            } else {
                auto itLeft = std::find_if(firstLeft, lastLeft,
                    [&firstRight](const LeftType& x) { return x == *firstRight;});
                if (itLeft == lastLeft) {
                    *output = rightRec(*firstRight);
                    ++firstRight;
                } else {
                    auto dl = std::distance(itLeft, firstLeft);
                    auto dr = std::distance(itRight, firstRight);
                    if (dl < dr) {
                        *output = rightRec(*firstRight);
                        ++firstRight;
                    } else {
                        *output = leftRec(*firstLeft);
                        ++firstLeft;
                    }
                }
            }
        }
        ++output;
    }
}
}//namespace

const std::string& toString(ThreadStopDiffRecord::Type type)
{
    switch (type) {
    case ThreadStopDiffRecord::Type::Added :
        return STR_ADDED;
    case ThreadStopDiffRecord::Type::Removed :
        return STR_REMOVED;
    case ThreadStopDiffRecord::Type::Modified :
        return STR_MODIFIED;
    case ThreadStopDiffRecord::Type::Context :
        return STR_CONTEXT;
    }
    throw maps::LogicError();
};

ThreadStopDiff
threadsDiff(ObjectPtr after, ObjectPtr before, ObjectsCache* cacheAfter, ObjectsCache* cacheBefore)
{
    if (!after || after->isDeleted()) {
        return ThreadStopDiff{};
    }
    ASSERT(isTransportThread(after->categoryId()));
    ASSERT(!before || isTransportThread(before->categoryId()));

    ThreadStopDiff diffResult;
    try {
        auto recordsAfter = extractRecords(after, cacheAfter);
        auto recordsBefore = extractRecords(before, cacheBefore);
        diff(
            recordsAfter.begin(), recordsAfter.end(),
            recordsBefore.begin(), recordsBefore.end(),
            std::back_insert_iterator<std::vector<ThreadStopDiffRecord>>(diffResult.diffs)
            );
        for (auto& diffRec : diffResult.diffs) {
            if (diffRec.title.empty()) {
                diffRec.title = cfg()->editor()->categories()[stationCategory(after->categoryId())].label();
            }
        }
    } catch (const BrokenSequence& ex) {
        diffResult.brokenSequence = true;
    }
    return diffResult;
}

} // namespace wiki
} // namespace maps
