#include "tile/base/exposed_var.h"

#include "tile/base/internal/logging.h"
#include "tile/base/internal/move_on_copy.h"
#include "tile/base/string.h"

namespace tile {
// namespace exposed_var {

namespace {
#define CHECK_PATH(path)                                                                                               \
    TILE_CHECK((path).size() <= 1 || (path).back() != '/', "Invalid path: [{}].", path);                               \
    TILE_CHECK((path).find("//") == tile::Slice::npos, "Invalid path: [{}].", path);

#define CHECK_RELATIVE_PATH(path)                                                                                      \
    CHECK_PATH(path);                                                                                                  \
    TILE_CHECK((path).empty() || (path).front() != '/', "Invalid path: [{}].", path);

#define CHECK_ABSOLUTE_PATH(path)                                                                                      \
    CHECK_PATH(path);                                                                                                  \
    TILE_CHECK((path).empty() || (path).front() == '/', "Invalid path: [{}]", path);

std::pair<Slice, Slice>
SplitFirstPart(Slice path)
{
    auto pos = path.find_first_of('/');
    if (pos == tile::Slice::npos) {
        return std::make_pair(path, "");
    } else {
        return std::make_pair(path.substr(0, pos), path.substr(pos + 1));
    }
}

std::pair<Slice, Slice>
SplitLastPart(Slice path)
{
    auto pos = path.find_last_of('/');
    if (pos == tile::Slice::npos) { return std::make_pair("", path); }

    return std::make_pair(path.substr(0, pos), path.substr(pos + 1));
}

std::string
JoinPath(Slice a, Slice b)
{
    if (EndsWith(b, "/")) { b.RemoveSuffix(1); }

    if (EndsWith(a, "/")) { a.RemoveSuffix(1); }

    if (b.empty()) { return a.ToString(); }
    if (a.empty()) { return b.ToString(); }

    return a.ToString() + "/" + b.ToString();
}

std::string
SubstituteEscapedSlashForZero(Slice path)
{
    TILE_CHECK(path.find('\0') == tile::Slice::npos);
    return Replace(path, "\\/", '\0');
}

std::string
SubstituteZeroForEscapedSlash(Slice path)
{
    return Replace(path, '\0', "\\/");
}

std::string
UnescapeZeroToPlainSlash(Slice path)
{
    return Replace(path, '\0', '/');
}

}// namespace

// } // namespace exposed_var
ExposedVarGroup::Handle
ExposedVarGroup::Add(Slice rel_path, std::function<Json::Value()> value)
{

    auto real_path = SubstituteEscapedSlashForZero(rel_path);
    CHECK_RELATIVE_PATH(rel_path);

    auto path_and_name = SplitLastPart(real_path);
    auto name          = path_and_name.second.ToString();

    auto moved_func    = tile::MakeMoveOnCopy(value);
    return CreateUpto(path_and_name.first)
        ->AddDirect(name, [name, moved_func](Slice expected) -> std::optional<Json::Value> {
            auto jsv = moved_func.Ref()();
            if (expected.empty()) { return jsv; }

            auto real_path   = SubstituteEscapedSlashForZero(expected);
            Json::Value *ptr = &jsv;
            auto pieces      = Split(real_path, '/');

            for (auto &&e : pieces) {
                auto unescaped = UnescapeZeroToPlainSlash({e.data(), e.size()});
                if (ptr->isObject()) {
                    if (ptr->isMember(unescaped)) {
                        ptr = &(*ptr)[unescaped];
                    } else {
                        return {};
                    }
                } else if (ptr->isArray()) {
                    auto index = TryParse<std::size_t>(unescaped);
                    if (index && *index < ptr->size()) {
                        ptr = &(*ptr)[static_cast<Json::ArrayIndex>(*index)];
                    } else {
                        return {};
                    }
                } else {
                    return {};
                }
            }

            return *ptr;
        });
}

ExposedVarGroup::Handle
ExposedVarGroup::Add(Slice rel_path, ExposedVarDynamicTree *dynamic_tree)
{
    auto real_path = SubstituteEscapedSlashForZero(rel_path);
    CHECK_RELATIVE_PATH(real_path);
    auto path_and_name = SplitLastPart(real_path);
    auto name          = path_and_name.second.ToString();

    return CreateUpto(path_and_name.first)->AddDirect(name, [dynamic_tree](Slice inner_path) {
        return dynamic_tree->TryGet(inner_path);
    });
}

ExposedVarGroup *
ExposedVarGroup::FindOrCreate(Slice abs_path)
{
    auto real_path = SubstituteEscapedSlashForZero(abs_path);
    CHECK_ABSOLUTE_PATH(real_path);

    return Root()->CreateUpto(real_path.substr(1));
}

std::optional<Json::Value>
ExposedVarGroup::TryGet(Slice abs_path)
{
    auto real_path = SubstituteEscapedSlashForZero(abs_path);
    TILE_CHECK(!real_path.empty());
    CHECK_ABSOLUTE_PATH(real_path);

    if (real_path == "/") { return Root()->Dump(); }

    Slice left_path;
    auto rel_path      = real_path.substr(1);
    auto parent        = Root()->FindLowest(rel_path, &left_path);

    auto name_and_rest = SplitFirstPart(left_path);
    if (name_and_rest.first.empty()) { return parent->Dump(); }

    auto s = name_and_rest.first.ToString();
    std::lock_guard<std::mutex> lk(parent->lock_);
    auto iter = parent->leaves_.find(s);
    if (iter != parent->leaves_.end()) {
        return iter->second(SubstituteZeroForEscapedSlash(name_and_rest.second));
    } else {
        auto iter = parent->nodes_.find(s);
        if (iter != parent->nodes_.end()) {
            return iter->second->Dump();
        } else {
            return {};
        }
    }
}

ExposedVarGroup::ExposedVarGroup(std::string abs_path) : abs_path_(std::move(abs_path))
{
    CHECK_ABSOLUTE_PATH(abs_path);
}

ExposedVarGroup *
ExposedVarGroup::Root()
{
    static ExposedVarGroup evg("/");
    return &evg;
}

const std::string &
ExposedVarGroup::AbsolutePath() const
{
    return abs_path_;
}

ExposedVarGroup *
ExposedVarGroup::FindLowest(Slice rel_path, Slice *left)
{
    CHECK_RELATIVE_PATH(rel_path);
    auto current = this;

    while (!rel_path.empty()) {
        auto name_and_rest = SplitFirstPart(rel_path);

        std::lock_guard<std::mutex> lk(current->lock_);
        auto iter = current->nodes_.find(name_and_rest.first.ToString());
        if (iter == current->nodes_.end()) {
            break;
        } else {
            current = &*iter->second;
        }

        rel_path = name_and_rest.second;
    }

    if (left) { *left = rel_path; }
    return current;
}

ExposedVarGroup *
ExposedVarGroup::CreateUpto(Slice rel_path)
{
    CHECK_RELATIVE_PATH(rel_path);

    Slice left_path;
    auto current = FindLowest(rel_path, &left_path);

    auto pieces  = Split(left_path, '/');

    for (auto &&e : pieces) {
        auto s = e.ToString();

        std::lock_guard<std::mutex> lk(current->lock_);
        auto p = JoinPath(current->AbsolutePath(), s);

        TILE_CHECK(current->leaves_.find(s) == current->leaves_.end(),
                   "Path [{}] has already been used: A value is registered at [{}].", rel_path, p);

        TILE_CHECK(current->nodes_.find(s) == current->nodes_.end());
        auto evg           = std::unique_ptr<ExposedVarGroup>(new ExposedVarGroup(p));
        current->nodes_[s] = std::move(evg);
        current            = &*(current->nodes_[s]);
    }

    TILE_CHECK(EndsWith(current->AbsolutePath(), rel_path), "[{}] vs [{}]", current->AbsolutePath(), rel_path);
    return current;
}

ExposedVarGroup::Handle
ExposedVarGroup::AddDirect(Slice name, Getter value)
{
    auto s = name.ToString();
    std::lock_guard<std::mutex> lk(lock_);

    TILE_CHECK(leaves_.find(s) == leaves_.end(), "Value [{}] has already been registered at [{}].", name,
               AbsolutePath());
    TILE_CHECK(nodes_.find(s) == nodes_.end(), "Path [{}] has already been used.", JoinPath(AbsolutePath(), name));
    leaves_[s] = std::move(value);

    return Deferred([this, s] {
        std::lock_guard<std::mutex> lk(lock_);
        TILE_CHECK_EQ(leaves_.erase(s), 1);
    });
}

Json::Value
ExposedVarGroup::Dump() const
{
    std::lock_guard<std::mutex> lk(lock_);
    Json::Value jsv;
    for (auto &&node : nodes_) { jsv[UnescapeZeroToPlainSlash(node.first)] = node.second->Dump(); }

    for (auto &&leave : leaves_) { jsv[UnescapeZeroToPlainSlash(leave.first)] = *leave.second(""); }

    return jsv;
}

ExposedVarDynamicTree::ExposedVarDynamicTree(Slice rel_path,
                                             std::function<Json::Value()> getter,
                                             ExposedVarGroup *parent)
    : getter_(std::move(getter))
{
    handle_ = parent->Add(rel_path, this);
}

std::optional<Json::Value>
ExposedVarDynamicTree::TryGet(Slice rel_path) const
{
    auto real_path   = SubstituteEscapedSlashForZero(rel_path);
    auto jsv         = getter_();
    Json::Value *ptr = &jsv;

    auto pieces      = Split(real_path, '/');
    for (auto &&e : pieces) {
        auto unescaped = UnescapeZeroToPlainSlash({e.data(), e.size()});
        ptr            = &(*ptr)[unescaped];
    }

    if (ptr->isNull()) { return {}; }

    return *ptr;
}

}// namespace tile
