#ifndef PORYGONLANG_MATHLIBRARY_HPP
#define PORYGONLANG_MATHLIBRARY_HPP

#include <utility>
#include <cmath>
#include <cfloat>
#include "../Evaluator/EvalValues/EvalValue.hpp"
#include "../Evaluator/EvalValues/NumericEvalValue.hpp"

#include "../UserData/UserDataScriptType.hpp"
#include "../ScriptTypes/FunctionScriptType.hpp"
#include "../ScriptTypes/ScriptType.hpp"
#include "../UserData/UserDataFunctionType.hpp"
#include "../Evaluator/EvalValues/ScriptFunctionEvalValue.hpp"
#include "../UserData/UserDataFunction.hpp"
#include "../Utilities/Random.hpp"

namespace Porygon::StandardLibraries {
    using namespace Porygon::Evaluation;
    using namespace Porygon::Utilities;
    class MathLibrary {

        //region templates

#define FLOAT_TYPE new NumericScriptType(true, true)
#define INTEGER_TYPE new NumericScriptType(true, false)
#define BOOL_TYPE new ScriptType(TypeClass::Bool)
#define FUNCTION(fieldName) \
        [](void* obj) -> const Porygon::Evaluation::EvalValue* { \
                auto t = new Porygon::Evaluation::GenericFunctionEvalValue(make_shared<GenericFunctionScriptType>(), \
                        Porygon::Utilities::Random::Get()); \
                t->RegisterOption(new Porygon::UserData::UserDataFunction(fieldName, nullptr)); \
                return t;}, \
        nullptr
        //endregion

        static const EvalValue* _abs(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount){
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            if (parameter->IsFloat()){
                return new FloatEvalValue(std::abs(parameter->EvaluateFloat()));
            } else{
                return new IntegerEvalValue(std::abs(parameter->EvaluateInteger()));
            }
        }

        static const EvalValue* _acos(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            return new FloatEvalValue(std::acos(parameter->EvaluateFloat()));
        }

        static const EvalValue* _asin(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            return new FloatEvalValue(std::asin(parameter->EvaluateFloat()));
        }

        static const EvalValue* _atan(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            if (parameterCount == 1){
                auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
                return new FloatEvalValue(std::atan(parameter->EvaluateFloat()));
            }
            else{
                auto parameter1 = dynamic_cast<const NumericEvalValue*>(parameters[0]);
                auto parameter2 = dynamic_cast<const NumericEvalValue*>(parameters[1]);
                return new FloatEvalValue(std::atan2(parameter1->EvaluateFloat(), parameter2->EvaluateFloat()));
            }
        }
        static const EvalValue* _ceil(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            auto f = parameter->EvaluateFloat();
            return new IntegerEvalValue(std::ceil(f));
        }

        static const EvalValue* _cos(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            auto f = parameter->EvaluateFloat();
            return new FloatEvalValue(std::cos(f));
        }

        static const EvalValue* _deg(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            auto f = parameter->EvaluateFloat();
            return new FloatEvalValue(f * 180 / M_PI);
        }

        static const EvalValue* _exp(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            auto f = parameter->EvaluateFloat();
            return new FloatEvalValue(std::exp(f));
        }

        static const EvalValue* _floor(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            auto f = parameter->EvaluateFloat();
            return new IntegerEvalValue(std::floor(f));
        }

        static const EvalValue* _fmod(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto f1 = dynamic_cast<const NumericEvalValue*>(parameters[0])->EvaluateFloat();
            auto f2 = dynamic_cast<const NumericEvalValue*>(parameters[0])->EvaluateFloat();
            return new FloatEvalValue(std::fmod(f1, f2));
        }

        static const EvalValue* _huge;

        static const EvalValue* _log(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount){
            if (parameterCount == 1){
                auto f = dynamic_cast<const NumericEvalValue*>(parameters[0])->EvaluateFloat();
                return new FloatEvalValue(std::log(f));
            }
            else{
                auto f1 = dynamic_cast<const NumericEvalValue*>(parameters[0])->EvaluateFloat();
                auto f2 = dynamic_cast<const NumericEvalValue*>(parameters[1])->EvaluateFloat();
                return new FloatEvalValue(std::log(f2) / std::log(f1));
            }
        }

        static const EvalValue* _maxInteger;
        static const EvalValue* _minInteger;
        static const EvalValue* _pi;

        static const EvalValue* _rad(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            auto f = parameter->EvaluateFloat();
            return new FloatEvalValue(M_PI *(f / 180));
        }

        static const EvalValue* _sin(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            return new FloatEvalValue(std::sin(parameter->EvaluateFloat()));
        }

        static const EvalValue* _sqrt(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            return new FloatEvalValue(std::sqrt(parameter->EvaluateFloat()));
        }

        static const EvalValue* _tan(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter = dynamic_cast<const NumericEvalValue*>(parameters[0]);
            return new FloatEvalValue(std::tan(parameter->EvaluateFloat()));
        }

        static const EvalValue* _ult(void*, const ScriptOptions*, const Evaluation::EvalValue* parameters[], int parameterCount) {
            auto parameter1 = dynamic_cast<const NumericEvalValue*>(parameters[0])->EvaluateInteger();
            auto parameter2 = dynamic_cast<const NumericEvalValue*>(parameters[1])->EvaluateInteger();
            return new BooleanEvalValue(parameter1 < parameter2);
        }

        static UserData::UserDataFunctionOption* CreateFunctionOption(ScriptType* returnType, std::vector<ScriptType*> params){
            return UserData::UserDataFunctionOption::FromRawPointers(returnType, std::move(params));
        }


    public:
        static UserData::UserData* CreateUserData(){
            return new UserData::UserData({
                {
                    HashedString::ConstHash("abs"),
                 new UserData::UserDataField(
                         (new GenericFunctionScriptType())
                             ->RegisterFunctionOption(0, CreateFunctionOption(INTEGER_TYPE, {INTEGER_TYPE}))
                             ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                         , FUNCTION(_abs))
                },
                {
                    HashedString::ConstHash("acos"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                            , FUNCTION(_acos))
                },
                {
                    HashedString::ConstHash("asin"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                            , FUNCTION(_asin))
                },
                {
                    HashedString::ConstHash("atan"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE, INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE, FLOAT_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE, INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE, FLOAT_TYPE}))
                            , FUNCTION(_atan))
                },
                {
                    HashedString::ConstHash("ceil"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                            , FUNCTION(_ceil))
                },
                {
                    HashedString::ConstHash("cos"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                            , FUNCTION(_cos))
                },
                {
                    HashedString::ConstHash("deg"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                            , FUNCTION(_deg))
                },
                {
                    HashedString::ConstHash("exp"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                            , FUNCTION(_exp))
                },
                {
                    HashedString::ConstHash("floor"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                            , FUNCTION(_floor))
                },
                {
                    HashedString::ConstHash("fmod"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE, INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE, INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE, FLOAT_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE, FLOAT_TYPE}))
                            , FUNCTION(_fmod))
                },
                {
                    HashedString::ConstHash("huge"),
                    new UserData::UserDataField(
                            FLOAT_TYPE
                            ,  [](void* obj) -> const Porygon::Evaluation::EvalValue* {return _huge;}, nullptr)
                },
                {
                    HashedString::ConstHash("log"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE, INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE, FLOAT_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE, INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE, FLOAT_TYPE}))
                            , FUNCTION(_log))
                },
                {
                    HashedString::ConstHash("maxinteger"),
                    new UserData::UserDataField(
                            INTEGER_TYPE
                            ,  [](void* obj) -> const Porygon::Evaluation::EvalValue* {return _maxInteger;}, nullptr)
                },
                {
                    HashedString::ConstHash("mininteger"),
                    new UserData::UserDataField(
                            INTEGER_TYPE
                            ,  [](void* obj) -> const Porygon::Evaluation::EvalValue* {return _minInteger;}, nullptr)
                },
                {
                    HashedString::ConstHash("pi"),
                    new UserData::UserDataField(
                            FLOAT_TYPE
                            ,  [](void* obj) -> const Porygon::Evaluation::EvalValue* {return _pi;}, nullptr)
                },
                {
                    HashedString::ConstHash("rad"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                            , FUNCTION(_rad))
                },
                {
                    HashedString::ConstHash("sin"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                            , FUNCTION(_sin))
                },
                {
                    HashedString::ConstHash("sqrt"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                            , FUNCTION(_sqrt))
                },
                {
                    HashedString::ConstHash("tan"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {INTEGER_TYPE}))
                                    ->RegisterFunctionOption(0, CreateFunctionOption(FLOAT_TYPE, {FLOAT_TYPE}))
                            , FUNCTION(_tan))
                },
                {
                    HashedString::ConstHash("ult"),
                    new UserData::UserDataField(
                            (new GenericFunctionScriptType())
                                    ->RegisterFunctionOption(0, CreateFunctionOption(BOOL_TYPE, {INTEGER_TYPE, INTEGER_TYPE}))
                            , FUNCTION(_ult))
                },

          });
        }
    };
}

#endif //PORYGONLANG_MATHLIBRARY_HPP
