#include "config.h"
#include "markup.h"
#include "ini.h"

#include <library/cpp/archive/yarchive.h>

#include <library/cpp/json/json_reader.h>
#include <library/cpp/json/json_writer.h>

#include <library/cpp/lua/eval.h>

#include <util/string/cast.h>
#include <util/string/strip.h>
#include <util/string/type.h>

#include <util/stream/output.h>
#include <util/stream/file.h>
#include <util/memory/blob.h>

#include <util/generic/singleton.h>
#include <util/stream/str.h>

using namespace NConfig;
using namespace NJson;

namespace {
    const unsigned char CODE[] = {
#include "code.inc"
    };

    struct TCode: public TArchiveReader {
        inline TCode()
            : TArchiveReader(TBlob::NoCopy(CODE, sizeof(CODE)))
        {
        }

        static inline TCode& Instance() {
            return *Singleton<TCode>();
        }
    };

    class TPreprocessor: public IInputStream {
    public:
        inline TPreprocessor(const TGlobals& g, IInputStream* in)
            : I_(nullptr, 0)
            , S_(in)
        {
            E_.SetVars(g);
        }

        size_t DoRead(void* ptr, size_t len) override {
            while (true) {
                const size_t read = I_.Read(ptr, len);

                if (read) {
                    return read;
                }

                do {
                    if (!S_.ReadLine(C_)) {
                        return 0;
                    }
                } while (IsComment(C_));

                C_ = E_.Preprocess(C_);
                C_.append('\n');
                I_.Reset(C_.data(), C_.size());
            }
        }

        static inline bool IsComment(TStringBuf s) {
            s = StripString(s);

            return !s.empty() && s[0] == '#';
        }

    private:
        TMemoryInput I_;
        TBufferedInput S_;
        TLuaEval E_;
        TString C_;
    };
}

void TConfig::DumpJson(IOutputStream& out) const {
    TString tmp;

    {
        TStringOutput out2(tmp);

        ToJson(out2);
    }

    {
        TJsonValue v;
        TStringInput in(tmp);

        ReadJsonTree(&in, &v);
        WriteJson(&out, &v, true, true);
    }
}

TConfig TConfig::FromJson(IInputStream& in, const TGlobals& g) {
    class TJSONReader: public TJsonCallbacks {
    public:
        inline TJSONReader()
            : Cur(nullptr)
        {
        }

        inline bool OnBoolean(bool b) override {
            *Next() = ConstructValue(b);

            return true;
        }

        inline bool OnInteger(long long i) override {
            *Next() = ConstructValue((i64)i);

            return true;
        }

        inline bool OnUInteger(unsigned long long i) override {
            *Next() = ConstructValue((ui64)i);

            return true;
        }

        inline bool OnDouble(double d) override {
            *Next() = ConstructValue(d);

            return true;
        }

        inline bool OnString(const TStringBuf& s) override {
            *Next() = ConstructValue(ToString(s));

            return true;
        }

        inline bool OnOpenMap() override {
            Push(ConstructValue(TDict()));

            return true;
        }

        inline bool OnCloseMap() override {
            Pop();

            return true;
        }

        inline bool OnOpenArray() override {
            Push(ConstructValue(TArray()));

            return true;
        }

        inline bool OnCloseArray() override {
            Pop();

            return true;
        }

        inline bool OnMapKey(const TStringBuf& k) override {
            if (S.empty()) {
                ythrow yexception() << "shit happen";
            }

            Cur = &S.back().GetNonConstant<TDict>()[ToString(k)];

            return true;
        }

        inline void Push(const TConfig& el) {
            *Next() = el;
            S.push_back(el);
        }

        inline void Pop() {
            if (S.empty()) {
                ythrow yexception() << "shit happen";
            }

            S.pop_back();
        }

        inline TConfig* Next() {
            if (S.empty()) {
                return &Root;
            }

            TConfig& top = S.back();

            if (top.IsA<TArray>()) {
                TArray& arr = top.GetNonConstant<TArray>();

                arr.emplace_back();

                return &arr.back();
            }

            if (top.IsA<TDict>()) {
                if (Cur) {
                    TConfig* ret = Cur;

                    Cur = nullptr;

                    return ret;
                }
            }

            ythrow yexception() << "shit happen";
        }

        inline void OnError(size_t off, TStringBuf reason) override {
            Y_UNUSED(off);
            if (!FirstErrorReason) {
                FirstErrorReason = reason;
            }
        }

        TConfig Root;
        TConfig* Cur;
        TVector<TConfig> S;
        TString FirstErrorReason;
    };

    TJSONReader r;
    TString data = in.ReadAll();
    TMemoryInput mi(data.data(), data.size());
    TPreprocessor p(g, &mi);

    if (!NJson::ReadJson(&p, false, true, &r)) {
        if (!!r.FirstErrorReason) {
            ythrow TConfigParseError() << "Error parsing json: " << r.FirstErrorReason;
        } else {
            ythrow TConfigParseError() << "Could not parse json " << data.Quote();
        }
    }

    return r.Root;
}

namespace {
    struct TData {
        const char* Prologue;
        const char* Epilogue;
    };

    const TData DATA[] = {
        {"", "\nassert(not (instance == nil))\nreturn instance\n"},
        {"", "\nassert(not (main == nil))\nreturn main\n"},
        {"return ", "\n"},
        {"", "\n"},
    };
}

TConfig TConfig::FromLua(IInputStream& in, const TGlobals& g) {
    const TString& data = in.ReadAll();
    TString json;

    for (size_t i = 0; i < Y_ARRAY_SIZE(DATA); ++i) {
        TStringStream ss;

        ss << TStringBuf("local function configgen()")
           << DATA[i].Prologue << data << DATA[i].Epilogue
           << TStringBuf("end\n\nreturn require('json').encode(configgen())\n");

        try {
            json = TLuaEval().SetVars(g).EvalRaw(ss.Str());

            break;
        } catch (...) {
            if (i == (Y_ARRAY_SIZE(DATA) - 1)) {
                throw;
            }
        }
    }

    TMemoryInput mi(json.data(), json.size());

    return FromJson(mi);
}

TConfig TConfig::FromMarkup(IInputStream& in, const TGlobals& g) {
    TPreprocessor pin(g, &in);

    return ParseRawMarkup(pin);
}

TConfig TConfig::FromIni(IInputStream& in, const TGlobals& g) {
    TPreprocessor pin(g, &in);

    return ParseIni(pin);
}

void TConfig::DumpLua(IOutputStream& out) const {
    TLuaEval e;
    TStringStream ss;

    ToJson(ss);

    e.SetVariable("jsonval", ss.Str());

    out << "return " << e.EvalRaw(TCode::Instance().ObjectByKey("/pp.lua")->ReadAll() + "\nreturn prettify(require('json').decode(jsonval))\n");
}

TConfig TConfig::FromStream(IInputStream& in, const TGlobals& g) {
    const TString& tmp = in.ReadAll();
    TString luaParsingError = "";

    try {
        TMemoryInput mi(tmp.data(), tmp.size());

        return FromLua(mi, g);
    } catch (const yexception& e) {
        luaParsingError = e.AsStrBuf();
    } catch (...) {
        luaParsingError = "unknown error";
    }

    TMemoryInput mi(tmp.data(), tmp.size());

    try {
        return FromJson(mi, g);
    } catch (const yexception& e) {
        const TStringBuf& jsonParsingError = e.AsStrBuf();
        ythrow yexception() << "Could not parse config:\nParsing as lua: " << luaParsingError << "\nParsing as json: " << jsonParsingError;
    }
}

TConfig TConfig::ReadJson(TStringBuf in, const TGlobals& g) {
    TMemoryInput mi(in.data(), in.size());

    return FromJson(mi, g);
}

TConfig TConfig::ReadLua(TStringBuf in, const TGlobals& g) {
    TMemoryInput mi(in.data(), in.size());

    return FromLua(mi, g);
}

TConfig TConfig::ReadMarkup(TStringBuf in, const TGlobals& g) {
    TMemoryInput mi(in.data(), in.size());

    return FromMarkup(mi, g);
}

TConfig TConfig::ReadIni(TStringBuf in, const TGlobals& g) {
    TMemoryInput mi(in.data(), in.size());

    return FromIni(mi, g);
}

void TConfig::Load(IInputStream* input) {
    TString string;
    ::Load(input, string);
    TStringInput stream(string);
    *this = FromJson(stream);
}

void TConfig::Save(IOutputStream* output) const {
    TString result;
    TStringOutput stream(result);
    DumpJson(stream);
    ::Save(output, result);
}

bool TConfig::Has(const TStringBuf& key) const {
    return !operator[](key).IsNull();
}

const TConfig& TConfig::operator[](const TStringBuf& key) const {
    return Get<TDict>().Find(key);
}

const TConfig& TConfig::At(const TStringBuf& key) const {
    return Get<TDict>().At(key);
}

const TConfig& TConfig::operator[](size_t index) const {
    return Get<TArray>().Index(index);
}

size_t TConfig::GetArraySize() const {
    return Get<TArray>().size();
}

const TConfig& TDict::Find(const TStringBuf& key) const {
    const_iterator it = find(key);

    if (it == end()) {
        return Default<TConfig>();
    }

    return it->second;
}

const TConfig& TDict::At(const TStringBuf& key) const {
    const_iterator it = find(key);

    Y_ENSURE_BT(it != end(), "missing key '" << key << "'");

    return it->second;
}

const TConfig& TArray::Index(size_t index) const {
    if (index < size()) {
        return (*this)[index];
    }

    return Default<TConfig>();
}

const TConfig& TArray::At(size_t index) const {
    Y_ENSURE_BT(index < size(), "index " << index << " is out of bounds");

    return (*this)[index];
}

THolder<IInputStream> NConfig::CreatePreprocessor(const TGlobals& g, IInputStream& in) {
    return MakeHolder<TPreprocessor>(g, &in);
}