#include <utility>
#include "BoundScope.hpp"
#include "../../StandardLibraries/StaticScope.hpp"

namespace Porygon::Binder {
    BoundScope::BoundScope(map<Utilities::HashedString, BoundVariable *> *tableScope,
            unordered_map<Utilities::HashedString, shared_ptr<const ScriptType>>* tableVariableTypes) {
        _tableScope = tableScope;
        _tableVariableTypes = tableVariableTypes;
        _currentScope = 1;
        _lastCreatedScope = 1;
        auto localUpmostScope = new map<Utilities::HashedString, BoundVariable *>();
        _localScope.push_back(localUpmostScope);
    }

    BoundScope::~BoundScope() {
        for (auto scope : _localScope) {
            for (const auto& v : *scope) {
                delete v.second;
            }
            delete scope;
        }
    }

    void BoundScope::GoInnerScope() {
        _lastCreatedScope++;
        _currentScope = _lastCreatedScope;
        if (_localScope.size() < _currentScope) {
            auto innerScope = new map<Utilities::HashedString, BoundVariable *>();
            _localScope.push_back(innerScope);
        }
    }

    void BoundScope::GoOuterScope() {
        auto scope = _localScope[_currentScope - 1];
        for (const auto& v : *scope) {
            delete v.second;
        }
        scope->clear();
        _currentScope--;
    }

    int BoundScope::Exists(const Utilities::HashedString& key) {
        for (int i = _currentScope - 1; i >= 0; i--) {
            auto scope = _localScope.at(i);
            auto found = scope->find(key);
            if (found != scope->end()) {
                return i + 1;
            }
        }

        auto found = this->_tableScope->find(key);
        if (found != _tableScope->end()) {
            return 0;
        }
        auto result = StandardLibraries::StaticScope::GetBoundVariable(key);
        if (result != nullptr){
            return -2;
        }
        return -1;
    }

    BoundVariable *BoundScope::GetVariable(uint32_t scope, const Utilities::HashedString& identifier) {
        if (scope == 0) {
            auto find = this->_tableScope->find(identifier);
            if (find != _tableScope->end()) {
                return find->second;
            }
            return nullptr;
        } else if (scope == -2){
            return StandardLibraries::StaticScope::GetBoundVariable(identifier);
        } else {
            auto s = this->_localScope.at(scope - 1);
            auto find = s->find(identifier);
            if (find != s->end()) {
                return find->second;
            }
            return nullptr;
        }
    }

    VariableAssignment BoundScope::CreateExplicitLocal(const Utilities::HashedString& identifier, std::shared_ptr<const ScriptType> type) {
        auto scope = this->_localScope.at(this->_currentScope - 1);
        if (scope->find(identifier) != scope->end()) {
            return VariableAssignment(VariableAssignmentResult::ExplicitLocalVariableExists, nullptr);
        }
        scope->insert({identifier, new BoundVariable(std::move(type))});
        return VariableAssignment(VariableAssignmentResult::Ok,
                                  new BoundVariableKey(identifier, this->_currentScope, true));
    }

    VariableAssignment BoundScope::AssignVariable(const Utilities::HashedString& identifier, const std::shared_ptr<const ScriptType> &type) {
        int exists = this->Exists(identifier);
        if (exists < 0) {
            // Creation
            _tableScope->insert({identifier, new BoundVariable(type)});
            if (_tableVariableTypes != nullptr){
                _tableVariableTypes->insert({identifier, type});
            }
            return VariableAssignment(VariableAssignmentResult::Ok, new BoundVariableKey(identifier, 0, true));
        } else {
            // Assigning
            auto var = this->GetVariable(exists, identifier);
            if (var->GetType().get()->operator!=(type.get())) {
                return VariableAssignment(VariableAssignmentResult::VariableDefinedWithDifferentType, nullptr);
            }
            return VariableAssignment(VariableAssignmentResult::Ok, new BoundVariableKey(identifier, exists, false));
        }
    }
}



