//
// Created by Dan Crosby on 4/20/21.
//

#ifndef EAGLE0_SHARDOK_LIBRARY_MAP_COORDS_SET_HPP
#define EAGLE0_SHARDOK_LIBRARY_MAP_COORDS_SET_HPP

#include <bit>
#include <cassert>
#include <cstdint>
#include <iterator>
#include <limits>
#include <utility>
#include <vector>

#include "src/main/cpp/net/eagle0/common/IterableBitset.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/coords.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/hex_map.hpp"

namespace shardok {

#ifndef NDEBUG
constexpr bool kCoordsSetIndexChecks = true;
#else
constexpr bool kCoordsSetIndexChecks = false;
#endif

constexpr size_t STANDARD_SIZE = size_t{12} * 14;

using HexMap = net::eagle0::shardok::storage::fb::HexMap;
using BackingBitset = common::IterableBitset<168>;

class CoordsSet {
private:
    BackingBitset store;

    uint8_t columnCount;
    uint8_t rowCount;

    [[maybe_unused]] uint16_t indexCount;

    void AssertCoordsInBounds([[maybe_unused]] const int row, [[maybe_unused]] const int column)
            const {
        if constexpr (kCoordsSetIndexChecks) {
            assert(row >= 0);
            assert(column >= 0);
            assert(row < rowCount);
            assert(column < columnCount);
        }
    }

    void AssertIndexInBounds([[maybe_unused]] const uint32_t idx) const {
        if constexpr (kCoordsSetIndexChecks) { assert(idx < indexCount); }
    }

    [[nodiscard]] auto Index(const int row, const int column) const -> int {
        AssertCoordsInBounds(row, column);
        return (row * columnCount) + column;
    }

    [[nodiscard]] auto Index(const Coords& c) const -> int { return Index(c.row(), c.column()); }

    class MismatchException : public std::exception {
        const std::string w;

    public:
        explicit MismatchException(std::string e) : w(std::move(e)) {}

        [[nodiscard]] auto what() const noexcept -> const char* override { return w.c_str(); }
    };

    void Remove(uint32_t idx) {
        AssertIndexInBounds(idx);
        store.Unset(idx);
    }
    [[nodiscard]] auto Contains(uint32_t idx) const -> bool {
        AssertIndexInBounds(idx);
        return store.IsSet(idx);
    }

public:
    CoordsSet(const int w, const int h)
        : columnCount(static_cast<uint8_t>(w)),
          rowCount(static_cast<uint8_t>(h)),
          indexCount(static_cast<uint16_t>(w * h)) {
        if constexpr (kCoordsSetIndexChecks) {
            assert(w >= 0);
            assert(h >= 0);
            assert(w <= std::numeric_limits<uint8_t>::max());
            assert(h <= std::numeric_limits<uint8_t>::max());
            assert(static_cast<size_t>(w) * static_cast<size_t>(h) <= STANDARD_SIZE);
        }
        clear();
    }
    explicit CoordsSet(const HexMap* map) : CoordsSet(map->column_count(), map->row_count()) {}

    CoordsSet(const CoordsSet& rhs) = default;
    CoordsSet(CoordsSet&& rhs) = default;

    ~CoordsSet() = default;

    auto operator=(const CoordsSet& rhs) -> CoordsSet& = default;
    auto operator=(CoordsSet&& rhs) -> CoordsSet& = default;

    [[nodiscard]] auto operator==(const CoordsSet& rhs) const -> bool {
        if (columnCount != rhs.columnCount) return false;
        if (rowCount != rhs.rowCount) return false;
        return store == rhs.store;
    }

    void Add(const Coords& c) { AddMapIndexUnchecked(Index(c)); }
    void Add(const int row, const int column) { AddMapIndexUnchecked(Index(row, column)); }

    // For callers that already have row * column_count + column.
    void AddMapIndexUnchecked(const uint32_t idx) {
        AssertIndexInBounds(idx);
        store.Set(idx);
    }

    // Always succeeds
    void Remove(const Coords& c) { Remove(Index(c)); }

    void Remove(const int row, const int column) { Remove(Index(row, column)); }

    [[nodiscard]] auto Contains(const Coords& c) const -> bool { return Contains(Index(c)); }
    [[nodiscard]] auto Contains(const Coords* c) const -> bool { return Contains(Index(*c)); }
    [[nodiscard]] auto Contains(const int row, const int column) const -> bool {
        return Contains(Index(row, column));
    }

    void clear() { store.clear(); }

    [[nodiscard]] auto size() const -> uint32_t { return store.size(); }

    [[nodiscard]] auto empty() const -> bool { return store.empty(); }

    [[nodiscard]] auto ColumnCount() const -> int { return columnCount; }

    // Should only be used for debugging.
    [[nodiscard]] auto as_vector() const -> std::vector<Coords> {
        return std::vector<Coords>{this->begin(), this->end()};
    }

    auto operator+=(const CoordsSet& rhs) -> CoordsSet& {
        if (columnCount != rhs.columnCount || rowCount != rhs.rowCount) {
            throw MismatchException("mismatched sizes");
        }

        store += rhs.store;
        return *this;
    }

    [[nodiscard]] auto operator+(const CoordsSet& rhs) const -> CoordsSet {
        CoordsSet cs = *this;
        cs += rhs;
        return cs;
    }

    auto operator-=(const CoordsSet& rhs) -> CoordsSet& {
        if (columnCount != rhs.columnCount || rowCount != rhs.rowCount) {
            throw MismatchException("mismatched sizes");
        }

        store -= rhs.store;
        return *this;
    }

    [[nodiscard]] auto operator-(const CoordsSet& rhs) const -> CoordsSet {
        CoordsSet cs = *this;
        cs -= rhs;
        return cs;
    }

    template<class Func>
    void Filter(Func&& f) {
        if (size() * 4 > indexCount) {
            for (uint32_t i = 0; i < indexCount; ++i) {
                if (Contains(i) && !f(i / columnCount, i % columnCount)) { Remove(i); }
            }
            return;
        }

        for (auto iter = store.begin(); iter != store.end();) {
            const uint32_t i = static_cast<uint32_t>(*iter);
            ++iter;
            if (!f(i / columnCount, i % columnCount)) { Remove(i); }
        }
    }

    class Iter {
    public:
        using iterator_category = std::forward_iterator_tag;
        using value_type = Coords;
        using difference_type = std::ptrdiff_t;
        using pointer = void;
        using reference = Coords;

    private:
        BackingBitset::Iter iterator;

        const CoordsSet& cs;

    public:
        Iter(const CoordsSet& cs, BackingBitset::Iter iter) : iterator(iter), cs(cs) {}

        [[nodiscard]] auto operator==(const Iter& rhs) const -> bool {
            return iterator == rhs.iterator;
        }
        [[nodiscard]] auto operator!=(const Iter& rhs) const -> bool {
            return iterator != rhs.iterator;
        }
        auto operator++() -> Iter& {
            ++iterator;
            return *this;
        }

        auto operator++(int) -> Iter {
            Iter previous = *this;
            ++(*this);
            return previous;
        }

        [[nodiscard]] auto operator*() const -> Coords {
            const size_t index = *iterator;

            const int row = static_cast<int>(index / static_cast<size_t>(cs.columnCount));
            const int column = static_cast<int>(index % static_cast<size_t>(cs.columnCount));
            return Coords(row, column);
        }
    };

    [[nodiscard]] auto begin() const -> Iter { return Iter(*this, store.begin()); }
    [[nodiscard]] auto end() const -> Iter { return Iter(*this, store.end()); }

    class IndexIterator {
    private:
        const CoordsSet& cs;

    public:
        explicit IndexIterator(const CoordsSet& cs) : cs(cs) {}

        [[nodiscard]] auto begin() const -> BackingBitset::Iter { return cs.store.begin(); }
        [[nodiscard]] auto end() const -> BackingBitset::Iter { return cs.store.end(); }
    };
    [[nodiscard]] auto indexIterator() const -> IndexIterator { return IndexIterator(*this); }
};

}  // namespace shardok

namespace common {
[[nodiscard]] static inline auto Contains(
        const shardok::CoordsSet& container,
        const shardok::Coords& elt) -> bool {
    return container.Contains(elt);
}

template<class Func>
void FilterInPlace(shardok::CoordsSet& container, Func fn) {
    container.Filter(
            [&fn](const int row, const int column) { return fn(shardok::Coords(row, column)); });
}
}  // namespace common

#endif  // EAGLE0_SHARDOK_LIBRARY_MAP_COORDS_SET_HPP
