#pragma once

#include <cassert>
#include <compare>
#include <concepts>
#include <cstdint>
#include <cstring>
#include <memory>
#include <vector>

#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Allocator.h"

namespace clice {

class StringSet {
public:
    using ID = std::uint32_t;

    explicit StringSet(llvm::BumpPtrAllocator* allocator) : allocator(allocator) {
        strings.emplace_back();
    }

    StringSet(const StringSet&) = delete;
    StringSet& operator=(const StringSet&) = delete;
    StringSet(StringSet&&) = default;
    StringSet& operator=(StringSet&&) = default;
    ~StringSet() = default;

    ID get(llvm::StringRef s) {
        if(s.empty()) {
            return ID(0);
        }

        auto [it, success] = cache.try_emplace(s, ID(0));
        if(!success) {
            return it->second;
        }

        const auto size = s.size();
        auto* p = allocator->Allocate<char>(size + 1);
        std::memcpy(p, s.data(), size);
        p[size] = '\0';

        it->first = llvm::StringRef(p, size);
        it->second = ID(strings.size());
        strings.emplace_back(it->first);
        return it->second;
    }

    llvm::StringRef get(ID id) {
        assert(id < strings.size());
        return strings[id];
    }

    llvm::StringRef save(llvm::StringRef s) {
        return get(get(s));
    }

private:
    llvm::BumpPtrAllocator* allocator;
    std::vector<llvm::StringRef> strings;
    llvm::DenseMap<llvm::StringRef, ID> cache;
};

template <typename T>
struct object_ptr {
    T* ptr = nullptr;

    object_ptr() noexcept = default;

    object_ptr(std::nullptr_t) noexcept : ptr(nullptr) {}

    explicit object_ptr(T* p) noexcept : ptr(p) {}

    T& operator*() const noexcept {
        return *ptr;
    }

    T* operator->() const noexcept {
        return ptr;
    }

    explicit operator bool() const noexcept {
        return ptr != nullptr;
    }

    std::strong_ordering operator<=>(const object_ptr&) const = default;
};

template <typename T>
object_ptr(T*) -> object_ptr<T>;

template <typename T>
class ObjectSet {
public:
    using ID = std::uint32_t;

    explicit ObjectSet(llvm::BumpPtrAllocator* allocator) : allocator(allocator) {}

    ObjectSet(const ObjectSet&) = delete;
    ObjectSet& operator=(const ObjectSet&) = delete;
    ObjectSet(ObjectSet&&) = default;
    ObjectSet& operator=(ObjectSet&&) = default;

    ~ObjectSet() {
        if constexpr(!std::is_trivially_destructible_v<T>) {
            for(auto object: objects) {
                if(object) {
                    std::destroy_at(object.ptr);
                }
            }

            for(auto& [object, _]: removed) {
                if(object) {
                    std::destroy_at(object.ptr);
                }
            }
        }
    }

    ID get(const T& object) {
        auto [it, success] = cache.try_emplace(object_ptr(const_cast<T*>(&object)), ID(0));
        if(!success) {
            return it->second;
        }

        if(!removed.empty()) [[unlikely]] {
            auto [o, id] = removed.pop_back_val();

            std::destroy_at(o.ptr);
            new (o.ptr) T(object);

            it->first = o;
            it->second = id;
            objects[id] = o;
        } else {
            auto* p = allocator->Allocate<T>(1);
            p = new (p) T(object);

            it->first = object_ptr<T>{p};
            it->second = ID(objects.size());
            objects.emplace_back(p);
        }

        return it->second;
    }

    object_ptr<T> get(ID id) {
        assert(id < objects.size());
        return objects[id];
    }

    object_ptr<T> save(const T& object) {
        return this->get(this->get(object));
    }

    void remove(object_ptr<T> object) {
        auto it = cache.find(object_ptr<T>(object.ptr));
        if(it == cache.end()) {
            return;
        }

        auto id = it->second;
        removed.emplace_back(object, id);
        cache.erase(it);
        objects[id] = nullptr;
    }

private:
    llvm::BumpPtrAllocator* allocator;
    std::vector<object_ptr<T>> objects;
    llvm::SmallVector<std::pair<object_ptr<T>, ID>> removed;
    llvm::DenseMap<object_ptr<T>, ID> cache;
};

}  // namespace clice

namespace llvm {

template <typename T>
struct DenseMapInfo<clice::object_ptr<T>> {
    using U = std::remove_cvref_t<T>;
    using O = clice::object_ptr<T>;

    inline static O getEmptyKey() {
        return O(DenseMapInfo<U*>::getEmptyKey());
    }

    inline static O getTombstoneKey() {
        return O(DenseMapInfo<U*>::getTombstoneKey());
    }

    inline static unsigned getHashValue(O value) {
        return DenseMapInfo<U>::getHashValue(*value);
    }

    inline static bool isEqual(O lhs, O rhs) {
        if(lhs == rhs) {
            return true;
        }

        const O Empty = getEmptyKey();
        const O Tombstone = getTombstoneKey();

        if(lhs == Empty || rhs == Empty || lhs == Tombstone || rhs == Tombstone) {
            return false;
        }

        return DenseMapInfo<U>::isEqual(*lhs, *rhs);
    }
};

}  // namespace llvm
