#pragma once

#include "value.hpp"
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <unordered_map>
#include <variant>
#include <vector>

namespace sliger::heap {

struct Array {
    std::vector<Value> values;

    auto at(int32_t index) & -> Value&;
};

struct Struct {
    std::unordered_map<std::string, Value> fields;
};

enum class AllocType {
    Value,
    Array,
    Struct,
};

// clang-format off
template <AllocType type> struct AllocTypeType {};
template <> struct AllocTypeType<AllocType::Value> { using Type = Value; };
template <> struct AllocTypeType<AllocType::Array> { using Type = Array; };
template <> struct AllocTypeType<AllocType::Struct> { using Type = Struct; };
// clang-format on

struct AllocItem {
    AllocItem(Value&& val)
        : type(AllocType::Value)
        , value(val)
    {
    }
    AllocItem(Array&& val)
        : type(AllocType::Array)
        , value(val)
    {
    }
    AllocItem(Struct&& val)
        : type(AllocType::Struct)
        , value(val)
    {
    }

    template <AllocType AT> inline auto as() & -> AllocTypeType<AT>::Type&
    {
        return std::get<typename AllocTypeType<AT>::Type>(this->value);
    }
    template <AllocType AT>
    inline auto as() const& -> const AllocTypeType<AT>::Type&
    {
        return std::get<typename AllocTypeType<AT>::Type>(this->value);
    }
    template <AllocType AT> inline auto as() && -> AllocTypeType<AT>::Type&&
    {
        return std::move(
            std::get<typename AllocTypeType<AT>::Type>(this->value));
    }
    template <AllocType AT>
    inline auto as() const&& -> const AllocTypeType<AT>::Type&&
    {
        return std::move(
            std::get<typename AllocTypeType<AT>::Type>(this->value));
    }

    inline auto as_value() & -> Value& { return std::get<Value>(this->value); }
    inline auto as_value() const& -> const Value&
    {
        return std::get<Value>(this->value);
    }
    inline auto as_array() & -> Array& { return std::get<Array>(this->value); }
    inline auto as_array() const& -> const Array&
    {
        return std::get<Array>(this->value);
    }
    inline auto as_struct() & -> Struct&
    {
        return std::get<Struct>(this->value);
    }
    inline auto as_struct() const& -> const Struct&
    {
        return std::get<Struct>(this->value);
    }

    AllocType type;
    std::variant<Value, Array, Struct> value;
};

enum class ErrType {
    InvalidPtrAccess,
    CannotAllocate,
};

template <typename T> struct Res {
    Res(T val)
        : m_val(std::forward<T>(val))
    {
    }
    Res(ErrType err)
        : m_err(err)
    {
    }

    auto ok() const -> bool { return m_val.has_value(); }
    auto val() & -> T& { return m_val.value(); }
    auto val() const& -> const T& { return m_val.value(); }
    auto val() && -> T&& { return std::move(m_val.value()); }
    auto val() const&& -> const T&& { return std::move(m_val.value()); }
    auto err() -> ErrType { return m_err.value(); }

    std::optional<T> m_val;
    std::optional<ErrType> m_err;
};

class Heap {
public:
    inline auto can_allocate() const -> bool
    {
        return this->sel->size() < this->max_size;
    }

    inline void collect(const std::vector<uint32_t>& ptr_stack_values)
    {
        this->other->reserve(this->max_size);
        for (auto ptr : ptr_stack_values) {
            move_item_to_other(ptr);
        }
        this->sel->clear();
        std::swap(this->sel, this->other);
        if (this->sel->size() + 1 >= this->max_size) {
            this->max_size *= 2;
            this->sel->reserve(this->max_size);
        } else if (this->sel->size() * 2 < this->max_size) {
            this->max_size /= 2;
            this->sel->shrink_to_fit();
            this->sel->reserve(this->max_size);
        }
    }

    inline auto at(uint32_t ptr) -> Res<AllocItem*>
    {
        if (ptr >= this->sel->size()) {
            return ErrType::InvalidPtrAccess;
        }
        return &this->sel->at(ptr);
    }

    template <AllocType type> auto alloc() -> Res<uint32_t>
    {
        if (not can_allocate()) {
            return ErrType::CannotAllocate;
        }
        auto ptr = static_cast<uint32_t>(this->sel->size());
        this->sel->push_back(typename AllocTypeType<type>::Type {});
        return ptr;
    }

private:
    inline void move_item_to_other(uint32_t ptr)
    {
        auto res = at(ptr);
        if (!res.ok())
            return;
        auto val = res.val();
        switch (val->type) {
            case AllocType::Value: {
                auto& v = val->as<AllocType::Value>();
                if (v.type() == ValueType::Ptr) {
                    move_item_to_other(v.template as<ValueType::Ptr>().value);
                }
                break;
            }
            case AllocType::Array: {
                auto& vs = val->as<AllocType::Array>();
                for (auto& v : vs.values) {
                    if (v.type() == ValueType::Ptr) {
                        move_item_to_other(
                            v.template as<ValueType::Ptr>().value);
                    }
                }
                break;
            }
            case AllocType::Struct: {
                auto& vs = val->as<AllocType::Struct>();
                for (auto& [key, v] : vs.fields) {
                    if (v.type() == ValueType::Ptr) {
                        move_item_to_other(
                            v.template as<ValueType::Ptr>().value);
                    }
                }
                break;
            }
        }
    }

    size_t max_size = 4;

    std::vector<AllocItem> heap_1;
    std::vector<AllocItem> heap_2;

    std::vector<AllocItem>* sel = &heap_1;
    std::vector<AllocItem>* other = &heap_2;
};

}
