#include "action-panel-model.hpp"
#include "fuzzy/fuzzy-searchable.hpp"
#include "services/navigation/list-navigation.hpp"
#include "theme.hpp"
#include <QKeyEvent>
#include <utility>

template <> struct fuzzy::FuzzySearchable<std::shared_ptr<AbstractAction>> {
  static fuzzy::Match score(const std::shared_ptr<AbstractAction> &action, const fuzzy::Query &query) {
    auto title = action->title().toStdString();
    return fuzzy::scoreWeighted({{title, 1.0}}, query);
  }
};

ActionPanelModel::ActionPanelModel(QObject *parent) : QAbstractListModel(parent) {
  connect(&ThemeService::instance(), &ThemeService::themeChanged, this, [this]() {
    if (rowCount() > 0) emit dataChanged(index(0), index(rowCount() - 1), {IconSource});
  });
}

ActionPanelModel::ActionPanelModel(std::unique_ptr<ActionPanelState> state, QObject *parent)
    : ActionPanelModel(parent) {
  setState(std::move(state));
}

ActionPanelModel::ActionPanelModel(const ActionPanelState *state, QObject *parent)
    : ActionPanelModel(parent) {
  setStateFrom(state);
}

void ActionPanelModel::setState(std::unique_ptr<ActionPanelState> state) { setStateFrom(state.get()); }

void ActionPanelModel::loadSections(const ActionPanelState *state) {
  m_allActions.clear();
  m_sections.clear();
  m_title = state ? state->title() : QString();

  if (state) {
    for (const auto &section : state->sections()) {
      SectionInfo info;
      info.name = section->name();
      for (const auto &action : section->actions()) {
        info.actions.push_back(action);
        m_allActions.push_back(action);
      }
      m_sections.push_back(std::move(info));
    }
  }
}

void ActionPanelModel::setStateFrom(const ActionPanelState *state) {
  beginResetModel();
  loadSections(state);
  m_filterQuery.clear();
  rebuildFlatList();
  endResetModel();
  emit titleChanged();
}

void ActionPanelModel::reconcile(const ActionPanelState *state) {
  loadSections(state);

  int oldCount = static_cast<int>(m_flat.size());
  std::vector<FlatItem> newFlat;
  buildFlatList(newFlat);
  int newCount = static_cast<int>(newFlat.size());

  if (oldCount == 0 || newCount == 0) {
    beginResetModel();
    m_flat = std::move(newFlat);
    endResetModel();
  } else if (newCount > oldCount) {
    beginInsertRows({}, oldCount, newCount - 1);
    m_flat = std::move(newFlat);
    endInsertRows();
    if (oldCount > 0) emit dataChanged(index(0), index(oldCount - 1));
  } else if (newCount < oldCount) {
    beginRemoveRows({}, newCount, oldCount - 1);
    m_flat = std::move(newFlat);
    endRemoveRows();
    if (newCount > 0) emit dataChanged(index(0), index(newCount - 1));
  } else {
    m_flat = std::move(newFlat);
    if (newCount > 0) emit dataChanged(index(0), index(newCount - 1));
  }

  emit titleChanged();
}

int ActionPanelModel::rowCount(const QModelIndex &parent) const {
  if (parent.isValid()) return 0;
  return static_cast<int>(m_flat.size());
}

QVariant ActionPanelModel::data(const QModelIndex &index, int role) const {
  if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(m_flat.size())) return {};

  const auto &item = m_flat[index.row()];

  if (item.kind == FlatItem::SectionHeader) {
    switch (role) {
    case ItemType:
      return QStringLiteral("section");
    case Title:
      return (item.sectionIdx >= 0 && std::cmp_less(item.sectionIdx, m_sections.size()))
                 ? m_sections[item.sectionIdx].name
                 : QString();
    case IconSource:
      return QString();
    case ShortcutTokens:
      return QVariantList();
    case IsSubmenu:
      return false;
    case IsPrimary:
      return false;
    case IsDanger:
      return false;
    default:
      return {};
    }
  }

  if (item.kind == FlatItem::Divider) {
    switch (role) {
    case ItemType:
      return QStringLiteral("divider");
    default:
      return {};
    }
  }

  if (item.sectionIdx < 0 || std::cmp_greater_equal(item.sectionIdx, m_sections.size())) return {};
  const auto &section = m_sections[item.sectionIdx];
  if (item.actionIdx < 0 || std::cmp_greater_equal(item.actionIdx, section.actions.size())) return {};
  const auto &action = section.actions[item.actionIdx];

  switch (role) {
  case ItemType:
    return QStringLiteral("action");
  case Title:
    return action->title();
  case IconSource: {
    auto icon = action->icon();
    return icon ? qml::imageSourceFor(*icon) : QString();
  }
  case ShortcutTokens: {
    auto shortcut = action->shortcut();
    return shortcut ? shortcut->toDisplayTokens() : QVariantList();
  }
  case IsSubmenu:
    return action->isSubmenu();
  case IsPrimary:
    return action->isPrimary();
  case IsDanger:
    return action->style() == AbstractAction::Style::Danger;
  default:
    return {};
  }
}

QHash<int, QByteArray> ActionPanelModel::roleNames() const {
  return {
      {ItemType, "itemType"},     {Title, "title"},
      {IconSource, "iconSource"}, {ShortcutTokens, "shortcutTokens"},
      {IsSubmenu, "isSubmenu"},   {IsPrimary, "isPrimary"},
      {IsDanger, "isDanger"},
  };
}

void ActionPanelModel::activate(int index) {
  if (index < 0 || std::cmp_greater_equal(index, m_flat.size())) return;
  const auto &item = m_flat[index];
  if (item.kind != FlatItem::ActionItem) return;

  // NOTE: we need to increment the reference counter by copying the shared ptr here,
  // as the action itself may trigger side effects that replace the current panel.
  auto action = m_sections[item.sectionIdx].actions[item.actionIdx];

  if (action->isSubmenu()) {
    auto *submenuAction = dynamic_cast<SubmenuAction *>(action.get());
    if (submenuAction) emit submenuActivated(submenuAction);
    return;
  }

  if (action->isPushView()) {
    emit actionExecuted(action.get());
    return;
  }

  emit actionExecuted(action.get());
  if (action->autoClose()) { emit closeRequested(); }
}

void ActionPanelModel::setFilter(const QString &text) {
  auto query = text.toStdString();
  if (m_filterQuery == query) return;
  m_filterQuery = std::move(query);

  beginResetModel();
  rebuildFlatList();
  endResetModel();
  emit filterChanged(text);
}

int ActionPanelModel::nextSelectableIndex(int from, int direction) const {
  int const count = static_cast<int>(m_flat.size());
  return ListNavigation::nextIndex(from, direction, count,
                                   [&](int idx) { return m_flat[idx].kind == FlatItem::ActionItem; });
}

int ActionPanelModel::nextSectionIndex(int from, int direction) const {
  int const count = static_cast<int>(m_flat.size());
  if (count == 0) return from;

  int currentSection = -1;
  if (from >= 0 && from < count) { currentSection = m_flat[from].sectionIdx; }

  if (direction > 0) {
    // Jump directly to first item of next section.
    for (int idx = from + 1; idx < count; ++idx) {
      if (m_flat[idx].kind == FlatItem::ActionItem && m_flat[idx].sectionIdx != currentSection) {
        return idx;
      }
    }
    if (!ListNavigation::wrapEnabled()) return from;
    return nextSelectableIndex(-1, 1);
  }

  // Going up: find start of current section, then go to previous.
  int currentStart = from;
  for (int idx = from - 1; idx >= 0; --idx) {
    if (m_flat[idx].kind == FlatItem::ActionItem && m_flat[idx].sectionIdx == currentSection) {
      currentStart = idx;
    } else if (m_flat[idx].kind == FlatItem::ActionItem) {
      break;
    }
  }

  if (currentStart < from) { return currentStart; }

  // Find the previous section's first ActionItem.
  int prevSection = -1;
  for (int idx = currentStart - 1; idx >= 0; --idx) {
    if (m_flat[idx].kind == FlatItem::ActionItem) {
      prevSection = m_flat[idx].sectionIdx;
      break;
    }
  }
  if (prevSection >= 0) {
    for (int i = 0; i < count; ++i) {
      if (m_flat[i].kind == FlatItem::ActionItem && m_flat[i].sectionIdx == prevSection) return i;
    }
  }

  if (!ListNavigation::wrapEnabled()) return from;

  // Wrap: last section.
  for (int i = count - 1; i >= 0; --i) {
    if (m_flat[i].kind == FlatItem::ActionItem) {
      prevSection = m_flat[i].sectionIdx;
      break;
    }
  }
  if (prevSection >= 0) {
    for (int i = 0; i < count; ++i) {
      if (m_flat[i].kind == FlatItem::ActionItem && m_flat[i].sectionIdx == prevSection) return i;
    }
  }
  return from;
}

int ActionPanelModel::scrollTargetIndex(int index, int direction) const {
  if (direction < 0 && index > 0 && std::cmp_less(index, m_flat.size())) {
    if (m_flat[index - 1].kind == FlatItem::SectionHeader) return index - 1;
  }
  return index;
}

void ActionPanelModel::buildFlatList(std::vector<FlatItem> &out) const {
  out.clear();
  out.reserve(m_allActions.size() + m_sections.size() * 2);

  bool needsDivider = false;
  std::vector<Scored<int>> scored;

  for (int s = 0; std::cmp_less(s, m_sections.size()); ++s) {
    const auto &section = m_sections[s];
    fuzzy::fuzzyFilter<std::shared_ptr<AbstractAction>>(section.actions, m_filterQuery, scored);

    if (scored.empty()) continue;

    if (needsDivider) { out.emplace_back(FlatItem::Divider); }

    if (!section.name.isEmpty()) { out.emplace_back(FlatItem::SectionHeader, s); }

    for (const auto &entry : scored) {
      out.emplace_back(FlatItem::ActionItem, s, entry.data);
    }

    needsDivider = true;
  }
}

void ActionPanelModel::rebuildFlatList() { buildFlatList(m_flat); }

bool ActionPanelModel::activateByShortcut(int key, int modifiers) {
  auto mods = static_cast<Qt::KeyboardModifiers>(modifiers);
  QKeyEvent const event(QEvent::KeyPress, key, mods);

  for (int i = 0; std::cmp_less(i, m_flat.size()); ++i) {
    const auto &item = m_flat[i];
    if (item.kind != FlatItem::ActionItem) continue;
    auto &action = m_sections[item.sectionIdx].actions[item.actionIdx];
    if (action->isBoundTo(&event)) {
      activate(i);
      return true;
    }
  }
  return false;
}
