#ifndef JINJA2CPP_SRC_TEMPLATE_IMPL_H
#define JINJA2CPP_SRC_TEMPLATE_IMPL_H

#include "internal_value.h"
#include "jinja2cpp/binding/rapid_json.h"
#include "jinja2cpp/template_env.h"
#include "jinja2cpp/value.h"
#include "renderer.h"
#include "template_parser.h"
#include "value_visitors.h"

#include <boost/optional.hpp>
#include <boost/predef/other/endian.h>
#include <contrib/restricted/expected-lite/include/nonstd/expected.hpp>
#include <rapidjson/error/en.h>

#include <string>

namespace jinja2
{
namespace detail
{
template<size_t Sz>
struct RapidJsonEncodingType;

template<>
struct RapidJsonEncodingType<1>
{
    using type = rapidjson::UTF8<char>;
};

#ifdef BOOST_ENDIAN_BIG_BYTE
template<>
struct RapidJsonEncodingType<2>
{
    using type = rapidjson::UTF16BE<wchar_t>;
};

template<>
struct RapidJsonEncodingType<4>
{
    using type = rapidjson::UTF32BE<wchar_t>;
};
#else
template<>
struct RapidJsonEncodingType<2>
{
    using type = rapidjson::UTF16LE<wchar_t>;
};

template<>
struct RapidJsonEncodingType<4>
{
    using type = rapidjson::UTF32LE<wchar_t>;
};
#endif
} // namespace detail

extern void SetupGlobals(InternalValueMap& globalParams);

class ITemplateImpl
{
public:
    virtual ~ITemplateImpl() = default;
};


template<typename U>
struct TemplateLoader;

template<>
struct TemplateLoader<char>
{
    static auto Load(const std::string& fileName, TemplateEnv* env)
    {
        return env->LoadTemplate(fileName);
    }
};

template<>
struct TemplateLoader<wchar_t>
{
    static auto Load(const std::string& fileName, TemplateEnv* env)
    {
        return env->LoadTemplateW(fileName);
    }
};

template<typename CharT>
class GenericStreamWriter : public OutStream::StreamWriter
{
public:
    explicit GenericStreamWriter(std::basic_string<CharT>& os)
        : m_os(os)
    {}

    // StreamWriter interface
    void WriteBuffer(const void* ptr, size_t length) override
    {
        m_os.append(reinterpret_cast<const CharT*>(ptr), length);
    }
    void WriteValue(const InternalValue& val) override
    {
        Apply<visitors::ValueRenderer<CharT>>(val, m_os);
    }

private:
    std::basic_string<CharT>& m_os;
};

template<typename CharT>
class StringStreamWriter : public OutStream::StreamWriter
{
public:
    explicit StringStreamWriter(std::basic_string<CharT>* targetStr)
        : m_targetStr(targetStr)
    {}

    // StreamWriter interface
    void WriteBuffer(const void* ptr, size_t length) override
    {
        m_targetStr->append(reinterpret_cast<const CharT*>(ptr), length);
        // m_os.write(reinterpret_cast<const CharT*>(ptr), length);
    }
    void WriteValue(const InternalValue& val) override
    {
        Apply<visitors::ValueRenderer<CharT>>(val, *m_targetStr);
    }

private:
    std::basic_string<CharT>* m_targetStr;
};

template<typename ErrorTpl1, typename ErrorTpl2>
struct ErrorConverter;

template<typename CharT1, typename CharT2>
struct ErrorConverter<ErrorInfoTpl<CharT1>, ErrorInfoTpl<CharT2>>
{
    static ErrorInfoTpl<CharT1> Convert(const ErrorInfoTpl<CharT2>& srcError)
    {
        typename ErrorInfoTpl<CharT1>::Data errorData;
        errorData.code = srcError.GetCode();
        errorData.srcLoc = srcError.GetErrorLocation();
        errorData.locationDescr = ConvertString<std::basic_string<CharT1>>(srcError.GetLocationDescr());
        errorData.extraParams = srcError.GetExtraParams();

        return ErrorInfoTpl<CharT1>(errorData);
    }
};

template<typename CharT>
struct ErrorConverter<ErrorInfoTpl<CharT>, ErrorInfoTpl<CharT>>
{
    static const ErrorInfoTpl<CharT>& Convert(const ErrorInfoTpl<CharT>& srcError)
    {
        return srcError;
    }
};

template<typename CharT>
inline bool operator==(const MetadataInfo<CharT>& lhs, const MetadataInfo<CharT>& rhs)
{
    if (lhs.metadata != rhs.metadata)
        return false;
    if (lhs.metadataType != rhs.metadataType)
        return false;
    if (lhs.location != rhs.location)
        return false;
    return true;
}

template<typename CharT>
inline bool operator!=(const MetadataInfo<CharT>& lhs, const MetadataInfo<CharT>& rhs)
{
    return !(lhs == rhs);
}

inline bool operator==(const TemplateEnv& lhs, const TemplateEnv& rhs)
{
    return lhs.IsEqual(rhs);
}
inline bool operator!=(const TemplateEnv& lhs, const TemplateEnv& rhs)
{
    return !(lhs == rhs);
}

inline bool operator==(const SourceLocation& lhs, const SourceLocation& rhs)
{
    if (lhs.fileName != rhs.fileName)
        return false;
    if (lhs.line != rhs.line)
        return false;
    if (lhs.col != rhs.col)
        return false;
    return true;
}
inline bool operator!=(const SourceLocation& lhs, const SourceLocation& rhs)
{
    return !(lhs == rhs);
}

template<typename CharT>
class TemplateImpl : public ITemplateImpl
{
public:
    using ThisType = TemplateImpl<CharT>;

    explicit TemplateImpl(TemplateEnv* env)
        : m_env(env)
    {
        if (env)
            m_settings = env->GetSettings();
    }

    auto GetRenderer() const {return m_renderer;}
    auto GetTemplateName() const {};

    boost::optional<ErrorInfoTpl<CharT>> Load(std::basic_string<CharT> tpl, std::string tplName)
    {
        m_template = std::move(tpl);
        m_templateName = tplName.empty() ? std::string("noname.j2tpl") : std::move(tplName);
        TemplateParser<CharT> parser(&m_template, m_settings, m_env, m_templateName);

        auto parseResult = parser.Parse();
        if (!parseResult)
            return parseResult.error()[0];

        m_renderer = *parseResult;
        m_metadataInfo = parser.GetMetadataInfo();
        return boost::optional<ErrorInfoTpl<CharT>>();
    }

    boost::optional<ErrorInfoTpl<CharT>> Render(std::basic_string<CharT>& os, const ValuesMap& params)
    {
        boost::optional<ErrorInfoTpl<CharT>> normalResult;

        if (!m_renderer)
        {
            typename ErrorInfoTpl<CharT>::Data errorData;
            errorData.code = ErrorCode::TemplateNotParsed;
            errorData.srcLoc.col = 1;
            errorData.srcLoc.line = 1;
            errorData.srcLoc.fileName = "<unknown file>";

            return ErrorInfoTpl<CharT>(errorData);
        }

        try
        {
            InternalValueMap extParams;
            InternalValueMap intParams;

            auto convertFn = [&intParams](const auto& params) {
                for (auto& ip : params)
                {
                    auto valRef = &ip.second.data();
                    auto newParam = visit(visitors::InputValueConvertor(false, true), *valRef);
                    if (!newParam)
                        intParams[ip.first] = ValueRef(static_cast<const Value&>(*valRef));
                    else
                        intParams[ip.first] = newParam.get();
                }
            };

            if (m_env)
            {
                m_env->ApplyGlobals(convertFn);
                std::swap(extParams, intParams);
            }

            convertFn(params);
            SetupGlobals(extParams);

            RendererCallback callback(this);
            RenderContext context(intParams, extParams, &callback);
            InitRenderContext(context);
            OutStream outStream([writer = GenericStreamWriter<CharT>(os)]() mutable -> OutStream::StreamWriter* {return &writer;});
            m_renderer->Render(outStream, context);
        }
        catch (const ErrorInfoTpl<char>& error)
        {
            return ErrorConverter<ErrorInfoTpl<CharT>, ErrorInfoTpl<char>>::Convert(error);
        }
        catch (const ErrorInfoTpl<wchar_t>& error)
        {
            return ErrorConverter<ErrorInfoTpl<CharT>, ErrorInfoTpl<wchar_t>>::Convert(error);
        }
        catch (const std::exception& ex)
        {
            typename ErrorInfoTpl<CharT>::Data errorData;
            errorData.code = ErrorCode::UnexpectedException;
            errorData.srcLoc.col = 1;
            errorData.srcLoc.line = 1;
            errorData.srcLoc.fileName = m_templateName;
            errorData.extraParams.push_back(Value(std::string(ex.what())));

            return ErrorInfoTpl<CharT>(errorData);
        }

        return normalResult;
    }

    InternalValueMap& InitRenderContext(RenderContext& context)
    {
        auto& curScope = context.GetCurrentScope();
        return curScope;
    }

    using TplLoadResultType = std::variant<EmptyValue,
            nonstd::expected<std::shared_ptr<TemplateImpl<char>>, ErrorInfo>,
            nonstd::expected<std::shared_ptr<TemplateImpl<wchar_t>>, ErrorInfoW>>;

    using TplOrError = nonstd::expected<std::shared_ptr<TemplateImpl<CharT>>, ErrorInfoTpl<CharT>>;

    TplLoadResultType LoadTemplate(const std::string& fileName)
    {
        if (!m_env)
            return TplLoadResultType(EmptyValue());

        auto tplWrapper = TemplateLoader<CharT>::Load(fileName, m_env);
        if (!tplWrapper)
            return TplLoadResultType(TplOrError(tplWrapper.get_unexpected()));

        return TplLoadResultType(TplOrError(std::static_pointer_cast<ThisType>(tplWrapper.value().m_impl)));
    }

    TplLoadResultType LoadTemplate(const InternalValue& fileName)
    {
        auto name = GetAsSameString(std::string(), fileName);
        if (!name)
        {
            typename ErrorInfoTpl<CharT>::Data errorData;
            errorData.code = ErrorCode::InvalidTemplateName;
            errorData.srcLoc.col = 1;
            errorData.srcLoc.line = 1;
            errorData.srcLoc.fileName = m_templateName;
            errorData.extraParams.push_back(IntValue2Value(fileName));
            return TplOrError(nonstd::make_unexpected(ErrorInfoTpl<CharT>(errorData)));
        }

        return LoadTemplate(name.value());
    }

    nonstd::expected<GenericMap, ErrorInfoTpl<CharT>> GetMetadata() const
    {
        auto& metadataString = m_metadataInfo.metadata;
        if (metadataString.empty())
            return GenericMap();

        if (m_metadataInfo.metadataType == "json")
        {
            m_metadataJson = JsonDocumentType();
            rapidjson::ParseResult res = m_metadataJson.value().Parse(metadataString.data(), metadataString.size());
            if (!res)
            {
                typename ErrorInfoTpl<CharT>::Data errorData;
                errorData.code = ErrorCode::MetadataParseError;
                errorData.srcLoc = m_metadataInfo.location;
                std::string jsonError = rapidjson::GetParseError_En(res.Code());
                errorData.extraParams.push_back(Value(std::move(jsonError)));
                return nonstd::make_unexpected(ErrorInfoTpl<CharT>(errorData));
            }
            m_metadata = std::move(std::get<GenericMap>(Reflect(m_metadataJson.value()).data()));
            return m_metadata.value();
        }
        return GenericMap();
    }

    nonstd::expected<MetadataInfo<CharT>, ErrorInfoTpl<CharT>> GetMetadataRaw() const { return m_metadataInfo; }

    bool operator==(const TemplateImpl<CharT>& other) const
    {
        if (m_env && other.m_env)
        {
            if (*m_env != *other.m_env)
                return false;
        }
        if (m_settings != other.m_settings)
            return false;
        if (m_template != other.m_template)
            return false;
        if (m_renderer && other.m_renderer && !m_renderer->IsEqual(*other.m_renderer))
            return false;
        if (m_metadata != other.m_metadata)
            return false;
        if (m_metadataJson != other.m_metadataJson)
            return false;
        if (m_metadataInfo != other.m_metadataInfo)
            return false;
        return true;
    }
private:
    void ThrowRuntimeError(ErrorCode code, ValuesList extraParams)
    {
        typename ErrorInfoTpl<CharT>::Data errorData;
        errorData.code = code;
        errorData.srcLoc.col = 1;
        errorData.srcLoc.line = 1;
        errorData.srcLoc.fileName = m_templateName;
        errorData.extraParams = std::move(extraParams);

        throw ErrorInfoTpl<CharT>(std::move(errorData));
    }

    class RendererCallback : public IRendererCallback
    {
    public:
        explicit RendererCallback(ThisType* host)
            : m_host(host)
        {}

        TargetString GetAsTargetString(const InternalValue& val) override
        {
            std::basic_string<CharT> os;
            Apply<visitors::ValueRenderer<CharT>>(val, os);
            return TargetString(std::move(os));
        }

        OutStream GetStreamOnString(TargetString& str) override
        {
            using string_t = std::basic_string<CharT>;
            str = string_t();
            return OutStream([writer = StringStreamWriter<CharT>(&std::get<string_t>(str))]() mutable -> OutStream::StreamWriter* { return &writer; });
        }

        std::variant<EmptyValue,
            nonstd::expected<std::shared_ptr<TemplateImpl<char>>, ErrorInfo>,
            nonstd::expected<std::shared_ptr<TemplateImpl<wchar_t>>, ErrorInfoW>> LoadTemplate(const std::string& fileName) const override
        {
            return m_host->LoadTemplate(fileName);
        }

        std::variant<EmptyValue,
                nonstd::expected<std::shared_ptr<TemplateImpl<char>>, ErrorInfo>,
                nonstd::expected<std::shared_ptr<TemplateImpl<wchar_t>>, ErrorInfoW>> LoadTemplate(const InternalValue& fileName) const override
        {
            return m_host->LoadTemplate(fileName);
        }

        void ThrowRuntimeError(ErrorCode code, ValuesList extraParams) override
        {
            m_host->ThrowRuntimeError(code, std::move(extraParams));
        }

        bool IsEqual(const IComparable& other) const override
        {
            auto* callback = dynamic_cast<const RendererCallback*>(&other);
            if (!callback)
                return false;
            if (m_host && callback->m_host)
                return *m_host == *(callback->m_host);
            if ((!m_host && (callback->m_host)) || (m_host && !(callback->m_host)))
                return false;
            return true;
        }
        bool operator==(const IComparable& other) const
        {
            auto* callback = dynamic_cast<const RendererCallback*>(&other);
            if (!callback)
                return false;
            if (m_host && callback->m_host)
                return *m_host == *(callback->m_host);
            if ((!m_host && (callback->m_host)) || (m_host && !(callback->m_host)))
                return false;
            return true;
        }

    private:
        ThisType* m_host;
    };
private:
    using JsonDocumentType = rapidjson::GenericDocument<typename detail::RapidJsonEncodingType<sizeof(CharT)>::type>;

    TemplateEnv* m_env{};
    Settings m_settings;
    std::basic_string<CharT> m_template;
    std::string m_templateName;
    RendererPtr m_renderer;
    mutable std::optional<GenericMap> m_metadata;
    mutable std::optional<JsonDocumentType> m_metadataJson;
    MetadataInfo<CharT> m_metadataInfo;
};

} // namespace jinja2

#endif // JINJA2CPP_SRC_TEMPLATE_IMPL_H