#pragma once

#include "common.h"
#include "exception.h"

#include <yandex/maps/wiki/configs/editor/unique_list.h>
#include <yandex/maps/wiki/configs/editor/common.h>
#include <maps/libs/log8/include/log8.h>

#include <boost/iterator/iterator_facade.hpp>

#include <map>
#include <vector>
#include <memory>

namespace maps {
namespace wiki {

class GeoObject;
class RelationObject;
class RelationsManager;

class RelationInfo
{
public:
    RelationInfo(RelationType relationType,
        GeoObject* relative,
        RelationObject* relation);

    RelationType type() const { return type_; }

    GeoObject* relative() const { return relative_; }
    RelationObject* relation() const { return relation_; }

    TOid id() const;
    const std::string& roleId() const;
    TOid relationObjectId() const;
    TRevisionId revisionId() const;
    const std::string& categoryId() const;
    std::string screenLabel() const;
    size_t seqNum() const;
    const Geom& geom() const;

    void setSeqNum(size_t seqNum);

    operator const std::string& () const;
    operator TOid() const;

private:
    RelationType type_;

    GeoObject* relative_;
    RelationObject* relation_;
};

class RelationInfoKey
{
public:
    explicit RelationInfoKey(const RelationInfo& link);

    RelationInfoKey(TOid relativeId, const std::string& roleId,
        const std::string& relativeCategoryId)
        : id_(relativeId)
        , roleId_(roleId)
        , relativeCategoryId_(relativeCategoryId)
    {}

    bool operator == (const RelationInfoKey& other) const
    {
        return
            std::tie(id_, roleId_, relativeCategoryId_) ==
            std::tie(other.id_, other.roleId_, other.relativeCategoryId_);
    }

    bool operator < (const RelationInfoKey& other) const
    {
        return
            std::tie(id_, roleId_, relativeCategoryId_) <
            std::tie(other.id_, other.roleId_, other.relativeCategoryId_);
    }

private:
    TOid id_;
    std::string roleId_;
    std::string relativeCategoryId_;
};

typedef UniqueList<RelationInfo, TOid> RelationInfosContainer;

struct RoleInfosContainer
{
    explicit RoleInfosContainer(const std::string& roleId)
        : roleId(roleId)
        , loaded(false)
    {}

    std::string roleId;
    bool loaded;
    RelationInfosContainer relations;
};

struct RoleInfosRefContainer
{
    explicit RoleInfosRefContainer(RoleInfosContainer* links)
        : links(links)
    {}

    RoleInfosContainer* links;

    operator const std::string& () const { return links->roleId; }
};

typedef UniqueList<RoleInfosRefContainer, std::string> RelationByRolesRefContainer;
typedef UniqueList<RelationInfo, RelationInfoKey> MixedRolesInfosContainer;

class RelationInfosIterator : public boost::iterator_facade<
    RelationInfosIterator, // derived
    RelationInfo, // value
    boost::bidirectional_traversal_tag,
    const RelationInfo&>
{
public:
    typedef RelationInfo value_type;
    typedef const RelationInfo& reference;

    RelationInfosIterator(const RelationInfosIterator&) = default;
    RelationInfosIterator& operator = (const RelationInfosIterator&) = default;

    static RelationInfosIterator begin(const RelationByRolesRefContainer& links);
    static RelationInfosIterator end(const RelationByRolesRefContainer& links);

private:
    friend class boost::iterator_core_access;

    typedef RelationByRolesRefContainer::const_iterator OuterIterator;
    typedef RelationInfosContainer::const_iterator InternalIterator;

    explicit RelationInfosIterator(const RelationByRolesRefContainer& links);

    RelationInfosIterator(
        const RelationByRolesRefContainer& links,
        OuterIterator outerIt,
        InternalIterator internalIt);

    void increment();
    void decrement();
    bool equal(const RelationInfosIterator& other) const;
    reference dereference() const;

    const RelationByRolesRefContainer& links_;
    OuterIterator outerIt_;
    boost::optional<InternalIterator> internalIt_;
};

struct RelativesLimit
{
    enum Type { All, PerRole };

    size_t value;
    Type type;
};

class RelationInfosRange
{
public:
    typedef RelationInfosIterator Iterator;

    explicit RelationInfosRange(const RelationByRolesRefContainer& links)
        : links_(links)
    {
        removeEmptyRoleContainers();
    }
    explicit RelationInfosRange(RelationByRolesRefContainer&& links)
        : links_(std::move(links))
    {
        removeEmptyRoleContainers();
    }

    RelationInfosRange(const RelationByRolesRefContainer& links,
            RelativesLimit limit,
            const StringSet& rolesWithExceededLimit)
        : links_(links)
        , limitsInfo_({limit, rolesWithExceededLimit})
    {
        removeEmptyRoleContainers();
    }

    RelationInfosRange(RelationByRolesRefContainer&& links,
            RelativesLimit limit,
            const StringSet& rolesWithExceededLimit)
        : links_(std::move(links))
        , limitsInfo_({limit, rolesWithExceededLimit})
    {
        removeEmptyRoleContainers();
    }

    Iterator begin() const { return Iterator::begin(links_); }
    Iterator end() const { return Iterator::end(links_); }

    bool empty() const { return links_.empty(); }

    size_t size() const
    {
        size_t res = 0;
        for (const auto& linksCont : links_) {
            res += linksCont.links->relations.size();
        }
        return res;
    }

    bool contains(TOid relativeId) const
    {
        for (const auto& linksCont : links_) {
            if (linksCont.links->relations.findByKey(relativeId) !=
                linksCont.links->relations.end())
            {
                return true;
            }
        }
        return false;
    }

    TOIds ids() const
    {
        TOIds result;
        for (const auto& linksCont : links_) {
            for (const auto& rel : linksCont.links->relations) {
                result.insert(rel.id());
            }
        }
        return result;
    }

    const StringSet& rolesWithExceededLimit() const
    {
        REQUIRE(limitsInfo_, "Limits data is not initialized");
        return (*limitsInfo_).exceedingRoleIds;
    }

    boost::optional<RelativesLimit> limit() const
    {
        if (limitsInfo_) {
            return (*limitsInfo_).limit;
        }
        return boost::none;
    }

private:
    void removeEmptyRoleContainers();

    struct LimitsInfo
    {
        RelativesLimit limit;
        StringSet exceedingRoleIds;
    };

    RelationByRolesRefContainer links_;
    boost::optional<LimitsInfo> limitsInfo_;
};

MixedRolesInfosContainer rangeToInfos(const RelationInfosRange& range);

struct RelationInfosDiff
{
    RelationInfosDiff() {}
    RelationInfosDiff(
            const MixedRolesInfosContainer& added,
            const MixedRolesInfosContainer& deleted)
        : added(added)
        , deleted(deleted)
    {}

    RelationInfosDiff(
            MixedRolesInfosContainer&& added,
            MixedRolesInfosContainer&& deleted)
        : added(std::move(added))
        , deleted(std::move(deleted))
    {}

    RelationInfosDiff(RelationInfosDiff&&) = default;
    RelationInfosDiff& operator = (RelationInfosDiff&&) = default;

    bool empty() const { return added.empty() && deleted.empty(); }

    TOIds addedIds() const
    {
        TOIds result;
        std::transform(added.begin(), added.end(), std::inserter(result, result.end()),
            [] (const RelationInfo& rel) { return rel.id(); });
        return result;
    }

    TOIds deletedIds() const
    {
        TOIds result;
        std::transform(deleted.begin(), deleted.end(), std::inserter(result, result.end()),
            [] (const RelationInfo& rel) { return rel.id(); });
        return result;
    }

    MixedRolesInfosContainer added;
    MixedRolesInfosContainer deleted;
};

class RelationInfosHolder;

/**
 * Wrapper for RelationInfosHolder, common public interface
 */
class RelationInfos
{
public:
    typedef RelationInfosRange Range;
    typedef RelationInfosDiff Diff;

    explicit RelationInfos(RelationInfosHolder& holder);

    bool areAllRolesLoaded() const;
    bool isRoleLoaded(const std::string& roleId) const;
    bool areRolesLoaded(const StringSet& roleIds) const;

    Range range() const;
    boost::optional<Range> range(RelativesLimit limit) const;

    Range range(const std::string& roleId) const;
    boost::optional<Range> range(const std::string& roleId, RelativesLimit limit) const;

    Range range(const StringSet& roleIds) const;
    boost::optional<Range> range(const StringSet& roleIds, RelativesLimit limit) const;

    Diff diff() const;
    Diff diff(const std::string& roleId, const std::string& relativeCategoryId = {}) const;
    Diff diff(const StringSet& roleIds) const;

private:
    RelationInfosHolder& holder_;
};

/**
 * Relation links storage and lazy loads implementation
 */
class RelationInfosHolder
{
public:
    typedef RelationInfosIterator Iterator;
    typedef RelationInfosRange Range;
    typedef RelationInfosDiff Diff;

    RelationInfosHolder(
        RelationType type,
        TOid id,
        RelationsManager& relationsManager);
    RelationInfosHolder(const RelationInfosHolder&);

    void setCategory(const std::string& categoryId);

    bool areAllRolesLoaded() const;
    bool isRoleLoaded(const std::string& roleId) const;
    bool areRolesLoaded(const StringSet& roleIds) const;

    StringSet loadedRoles() const;
    StringSet rolesToLoad(const StringSet& requestedRoleIds) const;

    void add(TOid relativeId, const std::string& role);
    void remove(TOid relativeId, const std::string& role);

    RoleInfosContainer* roleRelations(const std::string& roleId);
    RelationByRolesRefContainer relations();
    RelationByRolesRefContainer relations(const StringSet& roleIds);

    MixedRolesInfosContainer& added() { return added_; }
    MixedRolesInfosContainer& deleted() { return deleted_; }

    bool diffEmpty() const { return added_.empty() && deleted_.empty(); }

    void loadRelations();
    void loadRelations(RelativesLimit limit);
    void loadRelations(const std::string& roleId);
    void loadRelations(const std::string& roleId, RelativesLimit limit);
    void loadRelations(const StringSet& roleIds);
    void loadRelations(const StringSet& roleIds, RelativesLimit limit);
    const std::vector<const RelationObject*>& cachedRelations() const;

private:
    void setContainerId(TOid id) { id_ = id; }
    //friend class RelationsManager;
    friend class GeoObjectFactory;

    typedef std::map<std::string, std::unique_ptr<RoleInfosContainer>> Storage;

    TOid id_;
    std::string categoryId_;
    RelationType type_;

    Storage relatives_;
    MixedRolesInfosContainer added_;
    MixedRolesInfosContainer deleted_;

    RelationsManager& relationsManager_;
};

} // namespace wiki
} // namespace maps
