#ifndef PORYGONLANG_BASICLIBRARY_HPP
#define PORYGONLANG_BASICLIBRARY_HPP

#include <string>
#include <map>
#include "../Evaluator/EvaluationException.hpp"
#include "../Evaluator/EvalValues/EvalValue.hpp"
#include "../Evaluator/EvalValues/NumericEvalValue.hpp"
#include "../Evaluator/EvalValues/NilEvalValue.hpp"
#include "../Utilities/StringUtils.hpp"
#include "../Utilities/Random.hpp"
#include "../Binder/BoundVariables/BoundVariable.hpp"
#include "../UserData/UserDataFunction.hpp"
#include "../UserData/UserDataFunctionType.hpp"
#include "../ScriptOptions.hpp"
#include "../Evaluator/EvalValues/StringEvalValue.hpp"

namespace Porygon::StandardLibraries{
    class BasicLibrary{
        //region Assert

        static const Evaluation::EvalValue* _assert(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount){
            auto assertion = parameters[0]->EvaluateBool();
            if (!assertion){
                if (parameterCount >= 2){
                    auto error = parameters[1]->EvaluateString();
                    auto conv = Utilities::StringUtils::FromUTF8(error);
                    throw Evaluation::EvaluationException(conv);
                }
                throw Evaluation::EvaluationException("assertion failed!");
            }
            return new Evaluation::BooleanEvalValue(true);
        }

        static shared_ptr<GenericFunctionScriptType> GetAssertFuncType(){
            return GetFuncType(make_shared<ScriptType>(TypeClass::Bool),
                               {{make_shared<ScriptType>(TypeClass::Bool)},
                                {make_shared<ScriptType>(TypeClass::Bool), make_shared<StringScriptType>(false, 0)}});
        }
        //endregion
        //region Error

        static const Evaluation::EvalValue* _error(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount){
            auto message = parameters[0]->EvaluateString();
            auto conv = Utilities::StringUtils::FromUTF8(message);
            throw Evaluation::EvaluationException(conv);
        }
        static shared_ptr<GenericFunctionScriptType> GetErrorFuncType(){
            return GetFuncType(make_shared<ScriptType>(TypeClass::Nil),
                    {{make_shared<StringScriptType>(false, 0)}});
        }
        //endregion
        //region Print

        static const Evaluation::EvalValue* _print(void*, const ScriptOptions* options, const Evaluation::EvalValue* parameters[], int parameterCount){
            auto message = parameters[0]->EvaluateString();
            options->Print(message.c_str());
            return new Evaluation::NilEvalValue();
        }
        static shared_ptr<GenericFunctionScriptType> GetPrintFuncType(){
            return GetFuncType(make_shared<ScriptType>(TypeClass::Nil), {{make_shared<StringScriptType>(false, 0)}});
        }
        //endregion
        //region ToInt

        static const Evaluation::EvalValue* _toInt(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount){
            auto parameter = parameters[0]->EvaluateString();
            auto parsed = Utilities::StringUtils::ParseInteger(parameter);
            return new Evaluation::IntegerEvalValue(parsed);
        }
        static shared_ptr<GenericFunctionScriptType> GetToIntFuncType(){
            return GetFuncType(make_shared<NumericScriptType>(true, false), {{make_shared<StringScriptType>(false, 0)}});
        }
        //endregion
        //region ToFloat

        static const Evaluation::EvalValue* _toFloat(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount){
            auto parameter = parameters[0]->EvaluateString();
            auto parsed = Utilities::StringUtils::ParseFloat(parameter);
            return new Evaluation::FloatEvalValue(parsed);
        }
        static shared_ptr<GenericFunctionScriptType> GetToFloatFuncType(){
            return GetFuncType(make_shared<NumericScriptType>(true, true),
                    {{make_shared<StringScriptType>(false, 0)}});
        }
        //endregion
        //region ToString

        static const Evaluation::EvalValue* _toString(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount){
            auto parameter = parameters[0]->EvaluateString();
            return new Evaluation::StringEvalValue(parameter);
        }
        static shared_ptr<GenericFunctionScriptType> GetToStringFuncType(){
            return GetFuncType(make_shared<StringScriptType>(false, 0),
                               {{make_shared<ScriptType>(TypeClass::All)}});
        }
        //endregion
        //region Type

        static const Evaluation::EvalValue* _type(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount){
            auto parameter = parameters[0]->GetTypeClass();
            switch (parameter){

                case TypeClass::Error: return new Evaluation::StringEvalValue(u"error");
                case TypeClass::Nil: return new Evaluation::StringEvalValue(u"nil");
                case TypeClass::Number: return new Evaluation::StringEvalValue(u"number");
                case TypeClass::Bool: return new Evaluation::StringEvalValue(u"bool");
                case TypeClass::String: return new Evaluation::StringEvalValue(u"string");
                case TypeClass::Function: return new Evaluation::StringEvalValue(u"function");
                case TypeClass::UserData: return new Evaluation::StringEvalValue(u"userdata");
                case TypeClass::Table: return new Evaluation::StringEvalValue(u"table");
                case TypeClass::All: return new Evaluation::StringEvalValue(u"all");
            }
        }
        static shared_ptr<GenericFunctionScriptType> GetTypeFuncType(){
            return GetFuncType(make_shared<StringScriptType>(false, 0),
                               {{make_shared<ScriptType>(TypeClass::All)}});
        }
        //endregion
        //region IsFloat

        static const Evaluation::EvalValue* _isFloat(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount){
            auto parameter = dynamic_cast<const Evaluation::NumericEvalValue*>(parameters[0]);
            return new Evaluation::BooleanEvalValue(parameter->IsFloat());
        }
        static shared_ptr<GenericFunctionScriptType> GetIsFloatFuncType(){
            return GetFuncType(make_shared<ScriptType>(TypeClass::Bool),
                               {{make_shared<NumericScriptType>(false, false)}});
        }
        //endregion

        //region Generic handling

        static shared_ptr<GenericFunctionScriptType> GetFuncType(const shared_ptr<ScriptType>& result, const vector<vector<shared_ptr<ScriptType>>>& options){
            auto funcType = make_shared<GenericFunctionScriptType>();
            for (const auto& o: options){
                auto option = new UserData::UserDataFunctionOption(result, o);
                funcType->RegisterFunctionOption(option);
            }
            return funcType;
        }

        static Evaluation::EvalValue* GetFuncEvalValue(
            const Evaluation::EvalValue* (*func)(void* obj, const ScriptOptions*,
                    const Evaluation::EvalValue* parameters[], int parameterCount),
            const shared_ptr<GenericFunctionScriptType>& type, size_t optionLength){
            auto f = new Evaluation::GenericFunctionEvalValue(type, Utilities::Random::Get());
            for (size_t i = 0; i < optionLength; i++){
                auto funcOption = new UserData::UserDataFunction(func, nullptr);
                f->RegisterOption(funcOption);
            }
            return f;
        }
        //endregion


    public:
        static void RegisterVariables(std::map<Utilities::HashedString, Binder::BoundVariable *>* bound,
                               std::map<Utilities::HashedString, Evaluation::EvalValue*>* values){
            // Register error function
            auto errorFuncType = BasicLibrary::GetErrorFuncType();
            auto errorLookup = Utilities::HashedString::CreateLookup(u"error");
            auto errorFunc = BasicLibrary::GetFuncEvalValue(_error, errorFuncType, 1);
            bound->insert({errorLookup, new Binder::BoundVariable(errorFuncType)});
            values->insert({errorLookup, errorFunc});

            // Register assert function
            auto assertFuncType = BasicLibrary::GetAssertFuncType();
            auto assertLookup = Utilities::HashedString::CreateLookup(u"assert");
            auto assertFunc = BasicLibrary::GetFuncEvalValue(_assert, assertFuncType, 2);
            bound->insert({assertLookup, new Binder::BoundVariable(assertFuncType)});
            values->insert({assertLookup, assertFunc});

            // Register print function
            auto printFuncType = BasicLibrary::GetPrintFuncType();
            auto printLookup = Utilities::HashedString::CreateLookup(u"print");
            auto printFunc = BasicLibrary::GetFuncEvalValue(_print, printFuncType, 1);
            bound->insert({printLookup, new Binder::BoundVariable(printFuncType)});
            values->insert({printLookup, printFunc});

            // Register toInt function
            auto toIntFuncType = BasicLibrary::GetToIntFuncType();
            auto toIntLookup = Utilities::HashedString::CreateLookup(u"toint");
            auto toIntFunc = BasicLibrary::GetFuncEvalValue(_toInt, toIntFuncType, 1);
            bound->insert({toIntLookup, new Binder::BoundVariable(toIntFuncType)});
            values->insert({toIntLookup, toIntFunc});

            // Register toFloat function
            auto toFloatFuncType = BasicLibrary::GetToFloatFuncType();
            auto toFloatLookup = Utilities::HashedString::CreateLookup(u"tofloat");
            auto toFloatFunc = BasicLibrary::GetFuncEvalValue(_toFloat, toFloatFuncType, 1);
            bound->insert({toFloatLookup, new Binder::BoundVariable(toFloatFuncType)});
            values->insert({toFloatLookup, toFloatFunc});

            // Register ToString function
            auto toStringFuncType = BasicLibrary::GetToStringFuncType();
            auto toStringLookup = Utilities::HashedString::CreateLookup(u"tostring");
            auto toStringFunc = BasicLibrary::GetFuncEvalValue(_toString, toStringFuncType, 1);
            bound->insert({toStringLookup, new Binder::BoundVariable(toStringFuncType)});
            values->insert({toStringLookup, toStringFunc});

            // Register Type function
            auto typeFuncType = BasicLibrary::GetTypeFuncType();
            auto typeLookup = Utilities::HashedString::CreateLookup(u"type");
            auto typeFunc = BasicLibrary::GetFuncEvalValue(_type, typeFuncType, 1);
            bound->insert({typeLookup, new Binder::BoundVariable(typeFuncType)});
            values->insert({typeLookup, typeFunc});

            // Register IsFloat function
            auto isFloatFuncType = BasicLibrary::GetIsFloatFuncType();
            auto isFloatLookup = Utilities::HashedString::CreateLookup(u"isfloat");
            auto isFloatFunc = BasicLibrary::GetFuncEvalValue(_isFloat, isFloatFuncType, 1);
            bound->insert({isFloatLookup, new Binder::BoundVariable(isFloatFuncType)});
            values->insert({isFloatLookup, isFloatFunc});
        }
    };
}

#endif //PORYGONLANG_BASICLIBRARY_HPP
