#include "BookShelf.hpp"

#include <algorithm>
#include <cctype>
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include <gtkmm/gridlayout.h>
#include <gtkmm/listitem.h>
#include <gdkmm/enums.h>
#include <glibmm/quark.h>
#include <sigc++/connection.h>

namespace {

const Glib::Quark kSelectionConnQuark("bibliotheca-booktile-selection-conn");

std::string ascii_lower(const std::string& input) {
  std::string out;
  out.reserve(input.size());
  for (unsigned char ch : input)
    out.push_back(static_cast<char>(std::tolower(ch)));
  return out;
}

double substring_score(const std::string& haystack, const std::string& needle) {
  if (needle.empty())
    return 0.0;
  auto pos = haystack.find(needle);
  if (pos == std::string::npos)
    return -1.0;
  return 200.0 - static_cast<double>(pos);
}

double subsequence_score(const std::string& haystack, const std::string& needle) {
  if (needle.empty())
    return 0.0;
  std::size_t first = std::string::npos;
  std::size_t last = 0;
  std::size_t cursor = 0;
  for (char c : needle) {
    auto pos = haystack.find(c, cursor);
    if (pos == std::string::npos)
      return -1.0;
    if (first == std::string::npos)
      first = pos;
    last = pos;
    cursor = pos + 1;
  }
  const auto span = static_cast<double>(last - first + 1);
  const auto density = static_cast<double>(needle.size()) / span;
  return 80.0 * density - static_cast<double>(first);
}

double field_score(const std::string& field, const std::string& pattern) {
  if (pattern.empty())
    return 0.0;
  auto lower = ascii_lower(field);
  auto score = substring_score(lower, pattern);
  if (score >= 0.0)
    return score;
  return subsequence_score(lower, pattern);
}

// Check if pattern starts with "tag:" prefix for exact tag filtering
bool has_tag_prefix(const std::string& pattern, std::string& tag_query) {
  const std::string prefix = "tag:";
  if (pattern.size() > prefix.size() &&
      ascii_lower(pattern.substr(0, prefix.size())) == prefix) {
    tag_query = ascii_lower(pattern.substr(prefix.size()));
    return true;
  }
  return false;
}

double book_score(const Book& book, const std::string& pattern) {
  if (pattern.empty())
    return 0.0;

  // Handle "tag:tagname" syntax for exact tag filtering
  std::string tag_query;
  if (has_tag_prefix(pattern, tag_query)) {
    if (tag_query.empty())
      return 0.0;
    for (const auto& tag : book.tags()) {
      auto lower_tag = ascii_lower(tag);
      // Exact match or substring match within tag
      if (lower_tag == tag_query || lower_tag.find(tag_query) != std::string::npos)
        return 100.0; // High score for tag match
    }
    return -1.0; // No matching tag
  }

  // Regular fuzzy search across all fields
  double best = -1.0;
  best = std::max(best, field_score(book.title(), pattern));
  best = std::max(best, field_score(book.author(), pattern));
  best = std::max(best, field_score(book.filePath(), pattern));
  best = std::max(best, field_score(book.id(), pattern));
  // Also search tags
  for (const auto& tag : book.tags()) {
    best = std::max(best, field_score(tag, pattern));
  }
  return best;
}

} // namespace

class BookShelf::BookObject : public Glib::Object {
public:
  using Ptr = Glib::RefPtr<BookObject>;

  static Ptr create(const Book& book) {
    return Glib::make_refptr_for_instance<BookObject>(new BookObject(book));
  }

  const Book& book() const noexcept { return m_book; }
  void set_book(const Book& book) { m_book = book; }

private:
  explicit BookObject(const Book& book)
  : Glib::ObjectBase("BookObject"),
    Glib::Object(),
    m_book(book) {}

  Book m_book;
};

BookShelf::BookShelf(BookList& bookList, int coverSize)
: m_bookList(bookList), m_coverSize(coverSize) {
  set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
  set_hexpand(true);
  set_vexpand(true);

  m_store = Gio::ListStore<BookObject>::create();

  auto factory = Gtk::SignalListItemFactory::create();
  factory->signal_setup().connect(sigc::mem_fun(*this, &BookShelf::onFactorySetup));
  factory->signal_bind().connect(sigc::mem_fun(*this, &BookShelf::onFactoryBind));
  factory->signal_unbind().connect(sigc::mem_fun(*this, &BookShelf::onFactoryUnbind));

  m_selection = Gtk::SingleSelection::create(m_store);
  m_selection->set_can_unselect(true);

  m_grid.set_factory(factory);
  m_grid.set_model(m_selection);
  m_grid.set_single_click_activate(false);
  m_grid.set_enable_rubberband(false);
  m_grid.set_margin(8);
  m_grid.set_min_columns(1);

  if (auto layout = std::dynamic_pointer_cast<Gtk::GridLayout>(m_grid.get_layout_manager())) {
    layout->set_column_spacing(12);
    layout->set_row_spacing(12);
  }

  m_grid.signal_activate().connect([this](guint position) {
    m_selection->select_item(position, true);
    if (auto obj = m_store->get_item(position))
      m_signalBookActivated.emit(obj->book());
  });

  set_child(m_grid);

  m_backgroundClick = Gtk::GestureClick::create();
  m_backgroundClick->set_button(GDK_BUTTON_PRIMARY);
  m_backgroundClick->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
  m_backgroundClick->signal_pressed().connect([this](int, double x, double y) {
    if (auto widget = m_grid.pick(x, y, Gtk::PickFlags::DEFAULT); !widget || widget == &m_grid)
      m_selection->unselect_all();
  });
  m_grid.add_controller(m_backgroundClick);

  m_motion = Gtk::EventControllerMotion::create();
  m_motion->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
  m_motion->signal_motion().connect([this](double x, double y) {
    if (auto widget = m_grid.pick(x, y, Gtk::PickFlags::DEFAULT); !widget || widget == &m_grid)
      m_selection->unselect_all();
  });
  m_grid.add_controller(m_motion);

  m_bookList.signalBookAdded().connect(sigc::mem_fun(*this, &BookShelf::onBookAdded));
  m_bookList.signalBookUpdated().connect(sigc::mem_fun(*this, &BookShelf::onBookUpdated));
  m_bookList.signalBookRemoved().connect(sigc::mem_fun(*this, &BookShelf::onBookRemoved));
  m_bookList.signalReset().connect(sigc::mem_fun(*this, &BookShelf::onReset));
  m_bookList.signalBulkChanged().connect(sigc::mem_fun(*this, &BookShelf::reload));

  reload();
}

void BookShelf::reload() {
  refreshVisible();
}

void BookShelf::addTile(const Book&) {
  refreshVisible();
}

void BookShelf::updateTile(const Book&) {
  refreshVisible();
}

void BookShelf::removeTile(const std::string&) {
  refreshVisible();
}

void BookShelf::setFilter(const std::string& query) {
  auto lowered = ascii_lower(query);
  if (lowered == m_filter)
    return;
  m_filter = std::move(lowered);
  refreshVisible();
}

bool BookShelf::empty() const noexcept {
  return m_store->get_n_items() == 0;
}

void BookShelf::refreshVisible() {
  m_selection->unselect_all();
  m_store->remove_all();

  const auto& books = m_bookList.getBooks();
  if (m_filter.empty()) {
    for (const auto& book : books)
      m_store->append(BookObject::create(book));
    return;
  }

  struct Candidate {
    const Book* book;
    double score;
  };

  std::vector<Candidate> matches;
  matches.reserve(books.size());
  for (const auto& book : books) {
    const double score = book_score(book, m_filter);
    if (score >= 0.0)
      matches.push_back({&book, score});
  }

  std::stable_sort(matches.begin(), matches.end(), [](const Candidate& a, const Candidate& b) {
    if (a.score == b.score)
      return a.book->title() < b.book->title();
    return a.score > b.score;
  });

  for (const auto& match : matches)
    m_store->append(BookObject::create(*match.book));
}

void BookShelf::onFactorySetup(const Glib::RefPtr<Gtk::ListItem>& item) {
  auto tile = Gtk::make_managed<BookTile>(Book{}, m_coverSize);
  item->set_child(*tile);
}

void BookShelf::onFactoryBind(const Glib::RefPtr<Gtk::ListItem>& item) {
  auto* tile = dynamic_cast<BookTile*>(item->get_child());
  if (!tile) return;

  const int pos = item->get_position();
  if (pos < 0) return;

  auto obj = m_store->get_item(static_cast<guint>(pos));
  if (!obj) {
    tile->setBook(Book{});
    tile->setSelected(false);
    if (auto data = static_cast<sigc::connection*>(item->steal_data(kSelectionConnQuark))) {
      data->disconnect();
      delete data;
    }
    return;
  }

  tile->setBook(obj->book());
  tile->setSelected(item->get_selected());

  if (auto data = static_cast<sigc::connection*>(item->steal_data(kSelectionConnQuark))) {
    data->disconnect();
    delete data;
  }

  Gtk::ListItem* rawItem = item.get();
  auto connection = new sigc::connection(
    item->property_selected().signal_changed().connect([tile, rawItem]() {
      if (rawItem)
        tile->setSelected(rawItem->get_selected());
      else
        tile->setSelected(false);
    })
  );

  item->set_data(kSelectionConnQuark, connection, [](void* data) {
    auto conn = static_cast<sigc::connection*>(data);
    if (!conn) return;
    conn->disconnect();
    delete conn;
  });
}

void BookShelf::onFactoryUnbind(const Glib::RefPtr<Gtk::ListItem>& item) {
  if (auto* tile = dynamic_cast<BookTile*>(item->get_child())) {
    tile->setBook(Book{});
    tile->setSelected(false);
  }

  if (auto data = static_cast<sigc::connection*>(item->steal_data(kSelectionConnQuark))) {
    data->disconnect();
    delete data;
  }
}

void BookShelf::onBookAdded(const Book& b)   { addTile(b); }
void BookShelf::onBookUpdated(const Book& b) { updateTile(b); }
void BookShelf::onBookRemoved(const std::string& id) { removeTile(id); }
void BookShelf::onReset() { reload(); }
