#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/stl/filesystem.h>

#include <array>
#include <filesystem>
#include <vector>

#include "BitBully.h"
#include "Board.h"
#include "OpeningBook.h"

namespace py = pybind11;
using B = BitBully::Board;

PYBIND11_MODULE(bitbully_core, m) {
  m.doc() =
      "Bitbully is a fast Connect-4 solver.";  // optional module docstring

  // Board constants (module-level; easy to discover and use from Python)
  m.attr("N_COLUMNS") = py::int_(B::N_COLUMNS);
  m.attr("N_ROWS") = py::int_(B::N_ROWS);

  // Player enum (as a proper Python enum)
  py::enum_<B::Player>(m, "Player")
      .value("P_EMPTY", B::Player::P_EMPTY)
      .value("P_YELLOW", B::Player::P_YELLOW)
      .value("P_RED", B::Player::P_RED)
      .export_values();

  py::class_<BitBully::BitBully>(m, "BitBullyCore")
      .def(py::init<>())  // Expose the default constructor
      .def(py::init<std::filesystem::path>(), py::arg("openingBookPath"))
      .def("mtdf", &BitBully::BitBully::mtdf, "MTD(f) algorithm",
           py::arg("board"), py::arg("first_guess"))
      .def("nullWindow", &BitBully::BitBully::nullWindow, "Null-window search",
           py::arg("board"))
      .def("negamax", &BitBully::BitBully::negamax, "negamax search",
           py::arg("board"), py::arg("alpha"), py::arg("beta"),
           py::arg("depth"))
      .def("scoreMove", &BitBully::BitBully::scoreMove,
           "evaluate a single move", py::arg("board"), py::arg("column"),
           py::arg("first_guess"))
      .def("scoreMoves", &BitBully::BitBully::scoreMoves, "evaluate all moves",
           py::arg("board"))
      .def("resetTranspositionTable",
           &BitBully::BitBully::resetTranspositionTable,
           "Reset the transposition table")
      .def("getNodeCounter", &BitBully::BitBully::getNodeCounter,
           "Get the current node counter")
      .def("resetNodeCounter", &BitBully::BitBully::resetNodeCounter,
           "Reset the node counter")
      .def("isBookLoaded", &BitBully::BitBully::isBookLoaded,
           "Check, if opening book is loaded")
      .def("loadBook", &BitBully::BitBully::loadBook,
           "Load an opening book from a file path. Returns True if loaded.",
           py::arg("bookPath") = std::filesystem::path{})
      .def("resetBook", &BitBully::BitBully::resetBook,
           "Unload the currently loaded opening book (if any).");

  // Expose the Board class
  // TODO: Check functions.... Many not necessary and some might be missing
  py::class_<B>(m, "BoardCore")
      .def(py::init<>())              // Default constructor
      .def(py::init<const B&>())      // Copy-Konstruktor
      .def("__str__", &B::toString)   // Override __str__ in Python
      .def("__repr__", &B::toString)  // Override __repr__ in Python
      .def("canWin", py::overload_cast<int>(&B::canWin, py::const_),
           "Check, if current player can win by moving into column.",
           py::arg("column"))
      .def("copy", &B::copy, "Create a deep copy of the board.")
      .def("canWin", py::overload_cast<>(&B::canWin, py::const_),
           "Check, if current player can win with the next move.")
      .def("hash", py::overload_cast<>(&B::hash, py::const_),
           "Hash the current position and return hash value.")
      .def("hasWin", &B::hasWin,
           "Check, if the player who performed the last move has a winning "
           "position (4 in a row).")
      .def("play", py::overload_cast<int>(&B::play),
           "Play a move by column index", py::arg("column"))
      .def("play", py::overload_cast<const std::vector<int>&>(&B::play),
           "Play a sequence of moves by column index", py::arg("moveSequence"))
      .def("play", py::overload_cast<const std::string&>(&B::play),
           "Play a sequence of moves by column index", py::arg("moveSequence"))
      .def("playMoveOnCopy", &B::playMoveOnCopy,
           "Play a move on a copy of the board and return the new board",
           py::arg("mv"))
      .def("popCountBoard", py::overload_cast<>(&B::popCountBoard, py::const_),
           "Popcount of all tokens/bits in the bitboard (for debugging).")
      .def("legalMovesMask", &B::legalMovesMask, "Generate possible moves")
      .def("generateNonLosingMoves", &B::generateNonLosingMoves,
           "Generate non-losing moves")
      .def("legalMoves", &B::legalMoves,
           "Generate possible moves as a vector of column indices",
           py::arg("nonLosing"), py::arg("orderMoves"))
      .def("isLegalMove", &B::isLegalMove, "Check if a move is legal",
           py::arg("column"))
      .def("toString", &B::toString,
           "Return a string representation of the board")
      .def("movesLeft", &B::movesLeft, "Get the number of moves left")
      .def("countTokens", &B::countTokens,
           "Get the number of Tokens on the board")
      .def("mirror", &B::mirror,
           "Get the mirrored board (mirror around center column)")
      .def("allPositions", &B::allPositions,
           "Generate all positions that can be reached from the current board "
           "with n tokens.",
           py::arg("upToNPly"), py::arg("exactlyN"))
      .def("findThreats", &B::findThreats, "Find threats on the board",
           py::arg("moves"))
      .def("doubleThreat", &B::doubleThreat, "Find double threats",
           py::arg("moves"))
      .def("toArray", &B::toArray,
           "Convert the board to a 2D array representation")
      .def("setBoard", py::overload_cast<const std::vector<int>&>(&B::setBoard),
           "Set the board using a list", py::arg("moveSequence"))
      .def("setBoard", py::overload_cast<const B::TBoardArray&>(&B::setBoard),
           "Set the board using a 2D array", py::arg("array"))
      .def("setBoard", py::overload_cast<const B::TBoardArrayT&>(&B::setBoard),
           "Set the board using a 2D array", py::arg("array"))
      .def("setBoard", py::overload_cast<const std::string&>(&B::setBoard),
           "Play a sequence of moves by column index", py::arg("moveSequence"))
      .def_static("isValid", &B::isValid, "Check, if a board is a valid one.",
                  py::arg("board"))
      .def_static("randomBoard", &B::randomBoard,
                  "Create a random board with n tokens.", py::arg("nPly"),
                  py::arg("forbidDirectWin"))
      .def("toHuffman", &B::toHuffman,
           "Encode position into a huffman-code compressed sequence.")
      .def("uid", &B::uid, "Get the unique identifier for the board")
      .def("__eq__", &B::operator==, "Check if two boards are equal")
      .def("__ne__", &B::operator!=, "Check if two boards are not equal")
      .def("getColumnHeight", &B::getColumnHeight,
           "Get the current height of a column", py::arg("column"))
      .def(
          "rawState",
          [](const B& b) {
            const auto s = b.rawState();
            // Return as Python ints (uint64 fits in Python int)
            return py::make_tuple(
                py::int_(static_cast<unsigned long long>(s.all_tokens)),
                py::int_(static_cast<unsigned long long>(s.active_tokens)),
                py::int_(s.moves_left));
          },
          "Return raw internal state: (all_tokens, active_tokens, "
          "moves_left).")
      .def(
          "setRawState",
          [](B& b, unsigned long long all_tokens,
             unsigned long long active_tokens, int moves_left) {
            B::RawState s{
                static_cast<B::TBitBoard>(all_tokens),
                static_cast<B::TBitBoard>(active_tokens),
                static_cast<B::TMovesCounter>(moves_left),
            };
            b.setRawState(s);
          },
          py::arg("all_tokens"), py::arg("active_tokens"),
          py::arg("moves_left"),
          "Set raw internal state from (all_tokens, active_tokens, "
          "moves_left). DANGER: No validity checks are performed!");

  // Expose OpeningBook:
  py::class_<BitBully::OpeningBook>(m, "OpeningBookCore")
      // Constructors
      .def(py::init<const std::filesystem::path&, bool, bool>(),
           py::arg("bookPath"), py::arg("is_8ply"), py::arg("with_distances"),
           "Initialize an OpeningBook with explicit settings.")
      .def(py::init<const std::filesystem::path&>(), py::arg("bookPath"),
           "Initialize an OpeningBook by inferring database type from file "
           "size.")

      // Member functions
      .def("init", &BitBully::OpeningBook::init, py::arg("bookPath"),
           py::arg("is_8ply"), py::arg("with_distances"),
           "Reinitialize the OpeningBook with new settings.")
      .def("getEntry", &BitBully::OpeningBook::getEntry, py::arg("entryIdx"),
           "Get an entry from the book by index.")
      .def("getBook", &BitBully::OpeningBook::getBook,
           "Return the raw book table.")
      .def("getBookSize", &BitBully::OpeningBook::getBookSize,
           "Get the size of the book.")
      .def("getBoardValue", &BitBully::OpeningBook::getBoardValue,
           py::arg("board"), "Get the value of a given board.")
      .def("isInBook", &BitBully::OpeningBook::isInBook, py::arg("board"),
           "Check, if the given board is in the opening book. Note, that "
           "usually boards are only present in one mirrored variant.")
      .def("convertValue", &BitBully::OpeningBook::convertValue,
           py::arg("value"), py::arg("board"),
           "Convert a value to the internal scoring system.")
      .def("getNPly", &BitBully::OpeningBook::getNPly,
           "Get the ply depth of the book.")

      // Static functions
      .def_static("readBook", &BitBully::OpeningBook::readBook,
                  py::arg("filename"), py::arg("with_distances") = true,
                  py::arg("is_8ply") = false, "Read a book from a file.");
}
