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

#ifndef EAGLE0_COORDSSET_HPP
#define EAGLE0_COORDSSET_HPP

#include <bit>
#include <functional>
#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 {

#define COORDS_SET_INDEX_CHECKS false

constexpr size_t STANDARD_SIZE = 12 * 14;

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

class CoordsSet {
private:
    BackingBitset store;

    int mapWidth;
    int mapHeight;

    uint32_t indexCount;

    [[nodiscard]] auto Index(const int row, const int column) const -> int {
        return row * mapWidth + 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) {
#if COORDS_SET_INDEX_CHECKS
        assert(idx < indexCount);
#endif  // __COORDS_SET_INDEX_CHECKS
        store.Unset(idx);
    }
    [[nodiscard]] auto Contains(uint32_t idx) const -> bool {
#if COORDS_SET_INDEX_CHECKS
        assert(idx < indexCount);
#endif  // __COORDS_SET_INDEX_CHECKS
        return store.IsSet(idx);
    }

public:
    CoordsSet(int w, int h) : mapWidth(w), mapHeight(h), indexCount(w * h) {
#if COORDS_SET_INDEX_CHECKS
        assert(w * h < STANDARD_SIZE);
#endif
        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;

    auto operator==(const CoordsSet& rhs) const -> bool {
        if (mapWidth != rhs.mapWidth) return false;
        if (mapHeight != rhs.mapHeight) return false;
        return store == rhs.store;
    }

    void Add(const Coords& c) { AddIndex(Index(c)); }
    void Add(const int row, const int column) { AddIndex(Index(row, column)); }
    void AddIndex(const uint32_t 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(); }

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

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

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

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

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

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

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

    void Filter(const std::function<bool(int, int)>& f) {
        for (uint32_t i = 0; i < indexCount; i++) {
            if (Contains(i) && !f(i / mapWidth, i % mapWidth)) { Remove(i); }
        }
    }

    class Iter {
    public:
        using iterator_category = std::forward_iterator_tag;
        using value_type = Coords;
        using difference_type = int;
        using pointer = Coords*;
        using reference = Coords&;

    private:
        BackingBitset::Iter iterator;

        const CoordsSet& cs;

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

        auto operator==(const Iter& rhs) const -> bool { return iterator == rhs.iterator; }
        auto operator!=(const Iter& rhs) const -> bool { return iterator != rhs.iterator; }
        void operator++() { ++iterator; }

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

            const int row = index / cs.mapWidth;
            const int column = index % cs.mapWidth;
            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); }

    [[nodiscard]] auto ToString() const -> std::string {
        return std::string(reinterpret_cast<char const*>(&store), STANDARD_SIZE);
    }
};

}  // namespace shardok

namespace common {
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) {
    for (const shardok::Coords& c : container) {
        if (!fn(c)) container.Remove(c);
    }
}
}  // namespace common

#endif  // EAGLE0_COORDSSET_HPP
