#include <utility>


#ifndef PORYGONLANG_SCRIPTTYPE_HPP
#define PORYGONLANG_SCRIPTTYPE_HPP

#include <utility>
#include <vector>
#include <memory>
#include "../Binder/BoundVariables/BoundVariableKey.hpp"
#include "../Utilities/HashedString.hpp"

using namespace std;

namespace Porygon{
    enum class TypeClass{
        Error,
        Nil,
        Number,
        Bool,
        String,
        Function,
        UserData,
        Table,
        All,
    };

    class ScriptType{
        TypeClass _class;
    public:
        explicit ScriptType(TypeClass c){
            _class = c;
        }

        virtual ~ScriptType() = default;

        [[nodiscard]] const TypeClass GetClass() const{
            return _class;
        }

        virtual bool operator ==(const ScriptType& b) const{
            return _class == b._class;
        };

        virtual bool operator ==(const ScriptType* b) const{
            return _class == b->_class;
        };

        virtual bool operator !=(const ScriptType& b) const{
            return ! (operator==(b));
        }
        virtual bool operator !=(const ScriptType* b) const{
            return ! (operator==(b));
        }

        virtual bool CanBeIndexedWith(const ScriptType* indexer) const;
        [[nodiscard]]
        virtual bool CanBeIndexedWithIdentifier(uint32_t hash) const{
            return false;
        }

        [[nodiscard]]
        virtual shared_ptr<const ScriptType> GetIndexedType(const ScriptType* indexer) const;
        [[nodiscard]]
        virtual shared_ptr<const ScriptType> GetIndexedType(uint32_t hash) const{
            throw "This type told the binder it can be indexed, but it does not implement the resulting type.";
        }

        [[nodiscard]]
        virtual bool CanBeIterated() const{
            return false;
        }
        [[nodiscard]]
        virtual shared_ptr<const ScriptType> GetIteratorKeyType() const{
            throw "This type told the binder it can be iterated, but it does not implement the resulting type.";
        }
    };

    class NumericScriptType : public ScriptType{
        // Are we aware of whether this is a float or not?
        bool _awareOfFloat;
        // Is this value a float?
        bool _isFloat;
    public:
        explicit NumericScriptType(bool floatAware, bool isFloat) : ScriptType(TypeClass::Number){
            _awareOfFloat = floatAware;
            _isFloat = isFloat;
        }

        [[nodiscard]] inline bool IsAwareOfFloat() const{
            return _awareOfFloat;
        }

        [[nodiscard]] inline bool IsFloat() const{
            return _isFloat;
        }
    };

    class StringScriptType : public ScriptType{
        bool _isKnownAtBind;
        uint32_t _hashValue;
    public:
        explicit StringScriptType(bool knownAtBind, uint32_t hashValue): ScriptType(TypeClass::String){
            _isKnownAtBind = knownAtBind;
            _hashValue = hashValue;
        }

        [[nodiscard]]
        bool CanBeIndexedWith(const ScriptType* indexer) const final{
            if (indexer -> GetClass() != TypeClass::Number)
                return false;
            auto num = dynamic_cast<const NumericScriptType*>(indexer);
            return !(num->IsAwareOfFloat() && num->IsFloat());
        }

        inline shared_ptr<const ScriptType> GetIndexedType(const ScriptType* indexer) const final{
            return make_shared<StringScriptType>(false, 0);
        }

        [[nodiscard]]
        inline bool IsKnownAtBind() const{
            return _isKnownAtBind;
        }

        [[nodiscard]]
        inline uint32_t GetHashValue() const{
            return _hashValue;
        }
    };

    class NumericalTableScriptType : public ScriptType{
        shared_ptr<const ScriptType> _valueType;
        // Consider adding a check whether the table actually contains a type if every key is static.
    public:
        explicit NumericalTableScriptType(shared_ptr<const ScriptType> valueType)
            : ScriptType(TypeClass::Table), _valueType(std::move(valueType)){
        }

        bool CanBeIndexedWith(const ScriptType* indexer) const final{
            if (indexer -> GetClass() != TypeClass::Number)
                return false;
            auto num = dynamic_cast<const NumericScriptType*>(indexer);
            return !(num->IsAwareOfFloat() && num->IsFloat());
        }

        inline shared_ptr<const ScriptType> GetIndexedType(const ScriptType* indexer) const final{
            return _valueType;
        }

        [[nodiscard]]
        inline bool CanBeIterated() const final{
            return true;
        }
        [[nodiscard]]
        inline shared_ptr<const ScriptType> GetIteratorKeyType() const final{
            return make_shared<NumericScriptType>(true, false);
        }
    };
}


#endif //PORYGONLANG_SCRIPTTYPE_HPP
