

#include "shortcutpopup.h"

// Tnz6 includes
#include "menubarcommandids.h"
#include "tapp.h"
#include "tenv.h"
#include "tsystem.h"
#include "toolpresetcommandmanager.h"

#include "toonz/toonzfolders.h"
// TnzQt includes
#include "toonzqt/gutil.h"
#include "toonzqt/menubarcommand.h"
#include "toonzqt/dvdialog.h"

// TnzLib includes
#include "toonz/preferences.h"

// Qt includes
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QScrollArea>
#include <QSizePolicy>
#include <QPushButton>
#include <QPainter>
#include <QAction>
#include <QMap>
#include <QKeyEvent>

//=============================================================================
// Static instance pointer for ShortcutPopup
//=============================================================================

ShortcutPopup *ShortcutPopup::s_instance = nullptr;
#include <QMainWindow>
#include <QLabel>
#include <QLineEdit>
#include <QSettings>
#include <QApplication>
#include <QTextStream>
#include <QGroupBox>
#include <QSignalBlocker>

// STD includes
#include <vector>

namespace {

// Persist Configure Shortcuts UI state in the user profile (.env).
TEnv::StringVar ShortcutPopupExpandedFolders("ShortcutPopupExpandedFolders", "");
TEnv::IntVar ShortcutPopupExpandedStateSaved("ShortcutPopupExpandedStateSaved",
                                             0);
TEnv::StringVar ShortcutPopupSearchText("ShortcutPopupSearchText", "");

const QChar kFolderListSeparator(0x1e);

QStringList expandedFoldersFromEnv() {
  const QString stored =
      QString::fromStdString((std::string)ShortcutPopupExpandedFolders);
  if (stored.isEmpty()) return QStringList();
  return stored.split(kFolderListSeparator, Qt::SkipEmptyParts);
}

void saveExpandedFoldersToEnv(const QStringList &expandedFolders) {
  ShortcutPopupExpandedFolders =
      expandedFolders.join(kFolderListSeparator).toStdString();
}

}  // namespace

//=============================================================================
// ShortcutItem
// ------------
// The ShortcutTree displays ShortcutItem (organized in folders)
// each ShortcutItem represents a QAction (and its Shortcut)
//-----------------------------------------------------------------------------

class ShortcutItem final : public QTreeWidgetItem {
  QAction *m_action;

public:
  ShortcutItem(QTreeWidgetItem *parent, QAction *action)
      : QTreeWidgetItem(parent, UserType), m_action(action) {
    setFlags(parent->flags());
    updateText();
  }
  void updateText() {
    QString text = m_action->text();
    // removing accelerator key indicator
    QRegularExpression regex("&([^& ])");
    text = text.replace(regex, "\\1");
    // removing doubled &s
    text = text.replace("&&", "&");
    setText(0, text);
    QString shortcut = m_action->shortcut().toString();
    setText(1, shortcut);
  }
  QAction *getAction() const { return m_action; }
};

//=============================================================================
// ShortcutViewer
//-----------------------------------------------------------------------------

ShortcutViewer::ShortcutViewer(QWidget *parent)
    : QKeySequenceEdit(parent), m_action(0), m_keysPressed(0) {
  setObjectName("ShortcutViewer");
  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  connect(this, &ShortcutViewer::editingFinished, this,
          &ShortcutViewer::onEditingFinished);
}

//-----------------------------------------------------------------------------

ShortcutViewer::~ShortcutViewer() {}

//-----------------------------------------------------------------------------

void ShortcutViewer::setAction(QAction *action) {
  m_action = action;
  if (m_action) {
    setEnabled(true);
    setKeySequence(m_action->shortcut());
    setFocus();
  } else {
    setEnabled(false);
    setKeySequence(QKeySequence());
  }
}

//-----------------------------------------------------------------------------

void ShortcutViewer::keyPressEvent(QKeyEvent *event) {
  int key                         = event->key();
  Qt::KeyboardModifiers modifiers = event->modifiers();

  if (m_keysPressed == 0) {
    if (key == Qt::Key_Home || key == Qt::Key_End || key == Qt::Key_PageDown ||
        key == Qt::Key_PageUp || key == Qt::Key_Escape ||
        key == Qt::Key_Print || key == Qt::Key_Pause ||
        key == Qt::Key_ScrollLock) {
      event->ignore();
      return;
    }

    // If "Use Numpad and Tab keys for Switching Styles" option is activated,
    // then prevent to assign such keys
    if (Preferences::instance()->isUseNumpadForSwitchingStylesEnabled() &&
        modifiers == 0 && (key >= Qt::Key_0 && key <= Qt::Key_9)) {
      event->ignore();
      return;
    }
  }

  m_keysPressed++;

  QKeySequenceEdit::keyPressEvent(event);
}

//-----------------------------------------------------------------------------

void ShortcutViewer::onEditingFinished() {
  // Get the current key sequence and its string representation
  QKeySequence keySeq = keySequence();
  QString keyStr      = keySeq.toString();

  // Reset the keys pressed counter (used for tracking multi-key sequences)
  m_keysPressed = 0;

  // Extract individual keys from the key sequence
  QVector<int> keys;
  for (int i = 0; i < keySeq.count(); i++) {
    keys.append(keySeq[i]);
  }

  // Check for conflicts with existing shortcuts
  if (m_action) {
    CommandManager *cm = CommandManager::instance();

    // Iterate through the key sequence to check for partial conflicts
    for (int i = 0; i < keys.size(); i++) {
      // Create a partial key sequence (e.g., k1, k1+k2, k1+k2+k3, etc.)
      QKeySequence partialSeq(keys[0], (i >= 1 ? keys[1] : 0),
                              (i >= 2 ? keys[2] : 0), (i >= 3 ? keys[3] : 0));

      // Check if the partial sequence conflicts with an existing shortcut
      QAction *conflictingAction =
          cm->getActionFromShortcut(partialSeq.toString().toStdString());

      if (conflictingAction == m_action) return;

      // If a conflict is found with another action
      if (conflictingAction) {
        QString msg;
        // Check if the conflict is with the full sequence or a partial sequence
        if (keys.size() == (i + 1)) {
          // Conflict with the full sequence
          msg = tr("'%1' is already assigned to '%2'\nAssign to '%3'?")
                    .arg(partialSeq.toString())
                    .arg(conflictingAction->iconText())
                    .arg(m_action->iconText());
        } else {
          // Conflict with a partial sequence
          msg = tr("Initial sequence '%1' is assigned to '%2' which takes "
                   "priority.\nAssign shortcut sequence anyway?")
                    .arg(partialSeq.toString())
                    .arg(conflictingAction->iconText());
        }

        // Show a warning message box to the user
        int ret = DVGui::MsgBox(msg, tr("Yes"), tr("No"), 1);
        activateWindow();

        // If the user chooses "No" or closes the dialog, reset the shortcut and
        // exit
        if (ret != 1) {  // 1 corresponds to "Yes"
          setKeySequence(
              m_action->shortcut());  // Reset to the original shortcut
          setFocus();                 // Set focus back to the widget
          return;
        }
      }
    }

    // If no conflicts are found, assign the new shortcut
    std::string shortcutString = keySeq.toString().toStdString();
    cm->setShortcut(m_action, shortcutString);
    emit shortcutChanged();
  }

  setKeySequence(keySeq);  // Update the displayed key sequence in the UI
}

//-----------------------------------------------------------------------------

void ShortcutViewer::removeShortcut() {
  if (m_action) {
    CommandManager::instance()->setShortcut(m_action, "", false);
    emit shortcutChanged();
    clear();
  }
}

//-----------------------------------------------------------------------------

void ShortcutViewer::enterEvent(QEvent *event) {
  setFocus();
  update();
}

//-----------------------------------------------------------------------------

void ShortcutViewer::leaveEvent(QEvent *event) { update(); }

//=============================================================================
// ShortcutTree
//-----------------------------------------------------------------------------

ShortcutTree::ShortcutTree(QWidget *parent) : QTreeWidget(parent) {
  setObjectName("ShortcutTree");
  setIndentation(14);
  setAlternatingRowColors(true);

  setColumnCount(2);
  header()->close();
  header()->setSectionResizeMode(0, QHeaderView::ResizeMode::Fixed);
  header()->setSectionResizeMode(1, QHeaderView::ResizeMode::ResizeToContents);
  header()->setDefaultSectionSize(300);
  // setStyleSheet("border-bottom:1px solid rgb(120,120,120); border-left:1px
  // solid rgb(120,120,120); border-top:1px solid rgb(120,120,120)");

  QTreeWidgetItem *menuCommandFolder = new QTreeWidgetItem(this);
  menuCommandFolder->setText(0, tr("Menu Commands"));
  m_folders.push_back(menuCommandFolder);

  addFolder(tr("Fill"), FillCommandType);
  addFolder(tr("File"), MenuFileCommandType, menuCommandFolder);
  addFolder(tr("Edit"), MenuEditCommandType, menuCommandFolder);
  addFolder(tr("Scan & Cleanup"), MenuScanCleanupCommandType,
            menuCommandFolder);
  addFolder(tr("Level"), MenuLevelCommandType, menuCommandFolder);
  addFolder(tr("Xsheet"), MenuXsheetCommandType, menuCommandFolder);
  addFolder(tr("Cells"), MenuCellsCommandType, menuCommandFolder);
  addFolder(tr("Play"), MenuPlayCommandType, menuCommandFolder);
  addFolder(tr("Render"), MenuRenderCommandType, menuCommandFolder);
  addFolder(tr("View"), MenuViewCommandType, menuCommandFolder);
  addFolder(tr("Windows"), MenuWindowsCommandType, menuCommandFolder);
  QTreeWidgetItem *windowsFolder = m_subFolders.back();
  addFolder(tr("Custom Panels"), CustomPanelCommandType, windowsFolder);
  addFolder(tr("Help"), MenuHelpCommandType, menuCommandFolder);

  addFolder(tr("Right-click Menu Commands"), RightClickMenuCommandType);
  QTreeWidgetItem *rcmSubFolder = m_folders.back();
  addFolder(tr("Cell Mark"), CellMarkCommandType, rcmSubFolder);

  addFolder(tr("Tools"), ToolCommandType);
  addFolder(tr("Tool Modifiers"), ToolModifierCommandType);
  QTreeWidgetItem *toolModifiersFolder = m_folders.back();
  addFolder(tr("Brush Presets"), BrushPresetCommandType, toolModifiersFolder);
  addFolder(tr("Brush Sizes"), BrushSizeCommandType, toolModifiersFolder);
  addFolder(tr("Stop Motion"), StopMotionCommandType);
  addFolder(tr("Visualization"), ZoomCommandType);
  addFolder(tr("Misc"), MiscCommandType);
  addFolder(tr("RGBA Channels"), RGBACommandType);
  addFolder(tr("Special Modifier Keys"), SpecialModifierKeyType);

  sortItems(0, Qt::AscendingOrder);

  restoreExpandedState();

  connect(this, &ShortcutTree::currentItemChanged, this,
          &ShortcutTree::onCurrentItemChanged);

  connect(this, &ShortcutTree::clicked, this, &ShortcutTree::onItemClicked);

  connect(this, &QTreeWidget::itemCollapsed, this,
          &ShortcutTree::onItemCollapsed);

  connect(this, &QTreeWidget::itemExpanded, this, [this](QTreeWidgetItem *) {
    if (m_lastSearchTerm.isEmpty()) saveExpandedState();
  });

  connect(this, &QTreeWidget::itemCollapsed, this, [this](QTreeWidgetItem *) {
    if (m_lastSearchTerm.isEmpty()) saveExpandedState();
  });
}

//-----------------------------------------------------------------------------

ShortcutTree::~ShortcutTree() {}

//-----------------------------------------------------------------------------

void ShortcutTree::saveExpandedState() {
  saveExpandedFoldersToEnv(collectExpandedState());
  ShortcutPopupExpandedStateSaved = 1;
}

//-----------------------------------------------------------------------------

void ShortcutTree::restoreExpandedState() {
  const bool hasSavedState = ShortcutPopupExpandedStateSaved != 0;
  const QStringList expandedFolders = expandedFoldersFromEnv();
  QSignalBlocker blocker(this);
  applyExpandedState(expandedFolders, !hasSavedState);
}

//-----------------------------------------------------------------------------

QStringList ShortcutTree::collectExpandedState() const {
  QStringList expandedFolders;

  // Save expanded state for all folders
  for (QTreeWidgetItem *folder : m_folders) {
    if (folder && folder->isExpanded()) {
      expandedFolders.append(folder->text(0));
    }
  }

  // Save expanded state for all subfolders (with parent path)
  for (QTreeWidgetItem *subfolder : m_subFolders) {
    if (subfolder && subfolder->isExpanded() && subfolder->parent()) {
      QString path = subfolder->parent()->text(0) + "/" + subfolder->text(0);
      expandedFolders.append(path);
    }
  }

  return expandedFolders;
}

//-----------------------------------------------------------------------------

void ShortcutTree::applyExpandedState(const QStringList &expandedFolders,
                                      bool useDefaultIfEmpty) {
  // Restore state for top-level folders
  for (QTreeWidgetItem *folder : m_folders) {
    if (folder) {
      QString folderName = folder->text(0);
      if (!expandedFolders.isEmpty()) {
        folder->setExpanded(expandedFolders.contains(folderName));
      } else if (useDefaultIfEmpty) {
        folder->setExpanded(folderName == tr("Menu Commands"));
      }
    }
  }

  // Restore state for subfolders
  for (QTreeWidgetItem *subfolder : m_subFolders) {
    if (subfolder && subfolder->parent()) {
      QString path = subfolder->parent()->text(0) + "/" + subfolder->text(0);
      if (!expandedFolders.isEmpty()) {
        subfolder->setExpanded(expandedFolders.contains(path));
      } else if (useDefaultIfEmpty) {
        subfolder->setExpanded(false);
      }
    }
  }
}

//-----------------------------------------------------------------------------

void ShortcutTree::refreshTree() {
  // === STEP 1: Save expansion state before clearing ===
  QMap<QString, bool> expandedState;

  // Save state for top-level folders
  for (QTreeWidgetItem *folder : m_folders) {
    if (folder) {
      expandedState[folder->text(0)] = folder->isExpanded();
    }
  }

  // Save state for sub-folders
  for (QTreeWidgetItem *subfolder : m_subFolders) {
    if (subfolder && subfolder->parent()) {
      QString fullPath =
          subfolder->parent()->text(0) + "/" + subfolder->text(0);
      expandedState[fullPath] = subfolder->isExpanded();
    }
  }

  // === STEP 2: Clear and rebuild ===
  m_items.clear();
  m_folders.clear();
  m_subFolders.clear();
  clear();

  // Rebuild the tree structure
  QTreeWidgetItem *menuCommandFolder = new QTreeWidgetItem(this);
  menuCommandFolder->setText(0, tr("Menu Commands"));
  m_folders.push_back(menuCommandFolder);

  addFolder(tr("Fill"), FillCommandType);
  addFolder(tr("File"), MenuFileCommandType, menuCommandFolder);
  addFolder(tr("Edit"), MenuEditCommandType, menuCommandFolder);
  addFolder(tr("Scan & Cleanup"), MenuScanCleanupCommandType,
            menuCommandFolder);
  addFolder(tr("Level"), MenuLevelCommandType, menuCommandFolder);
  addFolder(tr("Xsheet"), MenuXsheetCommandType, menuCommandFolder);
  addFolder(tr("Cells"), MenuCellsCommandType, menuCommandFolder);
  addFolder(tr("Play"), MenuPlayCommandType, menuCommandFolder);
  addFolder(tr("Render"), MenuRenderCommandType, menuCommandFolder);
  addFolder(tr("View"), MenuViewCommandType, menuCommandFolder);
  addFolder(tr("Windows"), MenuWindowsCommandType, menuCommandFolder);
  QTreeWidgetItem *windowsFolder = m_subFolders.back();
  addFolder(tr("Custom Panels"), CustomPanelCommandType, windowsFolder);
  addFolder(tr("Help"), MenuHelpCommandType, menuCommandFolder);

  addFolder(tr("Right-click Menu Commands"), RightClickMenuCommandType);
  QTreeWidgetItem *rcmSubFolder = m_folders.back();
  addFolder(tr("Cell Mark"), CellMarkCommandType, rcmSubFolder);

  addFolder(tr("Tools"), ToolCommandType);
  addFolder(tr("Tool Modifiers"), ToolModifierCommandType);
  QTreeWidgetItem *toolModifiersFolder = m_folders.back();
  addFolder(tr("Brush Presets"), BrushPresetCommandType, toolModifiersFolder);
  addFolder(tr("Brush Sizes"), BrushSizeCommandType, toolModifiersFolder);
  addFolder(tr("Stop Motion"), StopMotionCommandType);
  addFolder(tr("Visualization"), ZoomCommandType);
  addFolder(tr("Misc"), MiscCommandType);
  addFolder(tr("RGBA Channels"), RGBACommandType);
  addFolder(tr("Special Modifier Keys"), SpecialModifierKeyType);

  sortItems(0, Qt::AscendingOrder);

  // === STEP 3: Restore expansion state ===
  // Restore state for top-level folders (only if state exists)
  for (QTreeWidgetItem *folder : m_folders) {
    if (folder) {
      QString folderName = folder->text(0);
      if (expandedState.contains(folderName)) {
        folder->setExpanded(expandedState[folderName]);
      }
      // No default behavior - keep folders collapsed if no state exists
    }
  }

  // Restore state for sub-folders (only if state exists)
  for (QTreeWidgetItem *subfolder : m_subFolders) {
    if (subfolder && subfolder->parent()) {
      QString fullPath =
          subfolder->parent()->text(0) + "/" + subfolder->text(0);
      if (expandedState.contains(fullPath)) {
        subfolder->setExpanded(expandedState[fullPath]);
      }
      // No default behavior - keep folders collapsed if no state exists
    }
  }

  // === STEP 4: Restore search filter if one was active ===
  if (!m_lastSearchTerm.isEmpty()) {
    searchItems(m_lastSearchTerm);
  }

  update();
}

//-----------------------------------------------------------------------------

void ShortcutTree::addFolder(const QString &title, int commandType,
                             QTreeWidgetItem *parentFolder) {
  QTreeWidgetItem *folder;
  if (!parentFolder) {
    folder = new QTreeWidgetItem(this);
    m_folders.push_back(folder);
  } else {
    folder = new QTreeWidgetItem(parentFolder);
    m_subFolders.push_back(folder);
  }
  assert(folder);
  folder->setText(0, title);

  std::vector<QAction *> actions;
  CommandManager::instance()->getActions((CommandType)commandType, actions);
  for (int i = 0; i < (int)actions.size(); i++) {
    ShortcutItem *item = new ShortcutItem(folder, actions[i]);
    m_items.push_back(item);
  }
}

//-----------------------------------------------------------------------------

void ShortcutTree::searchItems(const QString &searchWord) {
  m_lastSearchTerm = searchWord;

  if (searchWord.isEmpty()) {
    // Reset: show all items
    for (int i = 0; i < (int)m_items.size(); i++) m_items[i]->setHidden(false);
    for (int f = 0; f < m_subFolders.size(); f++) {
      m_subFolders[f]->setHidden(false);
      m_subFolders[f]->setExpanded(false);  // Close all subfolders
    }
    for (int f = 0; f < m_folders.size(); f++) {
      m_folders[f]->setHidden(false);
      // Open only "Menu Commands"
      m_folders[f]->setExpanded(m_folders[f]->text(0) == tr("Menu Commands"));
    }

    // Clear user collapse tracking
    m_userCollapsedDuringSearch.clear();

    show();
    emit searched(true);
    update();
    return;
  }

  // Starting a new search: clear user collapse tracking
  if (m_lastSearchTerm.isEmpty()) {
    m_userCollapsedDuringSearch.clear();
  }

  // Multi-word search: split search term by spaces and match all words
  QStringList searchWords = searchWord.split(' ', Qt::SkipEmptyParts);
  QList<QTreeWidgetItem *> foundItems;

  if (searchWords.isEmpty()) {
    // No valid search words after splitting
    hide();
    emit searched(false);
    update();
    return;
  }

  // Find items that contain ALL search words (case-insensitive)
  // Search in item name AND parent folder names
  for (int i = 0; i < (int)m_items.size(); i++) {
    QTreeWidgetItem *item = m_items[i];
    QString itemText      = item->text(0);  // Get item name

    // Build full search text: item name + parent folder name + grandparent
    // folder name
    QString fullSearchText  = itemText;
    QTreeWidgetItem *parent = item->parent();
    if (parent) {
      fullSearchText += " " + parent->text(0);
      QTreeWidgetItem *grandparent = parent->parent();
      if (grandparent) {
        fullSearchText += " " + grandparent->text(0);
      }
    }

    bool matchesAllWords = true;
    for (const QString &word : searchWords) {
      if (!fullSearchText.contains(word, Qt::CaseInsensitive)) {
        matchesAllWords = false;
        break;
      }
    }

    if (matchesAllWords) {
      foundItems.append(item);
    }
  }

  if (foundItems.isEmpty()) {
    hide();
    emit searched(false);
    update();
    return;
  }

  // show all matched items, hide all unmatched items
  for (int i = 0; i < (int)m_items.size(); i++)
    m_items[i]->setHidden(!foundItems.contains(m_items[i]));

  // hide folders which does not contain matched items
  // show AND expand folders containing matched items (unless user manually
  // closed them)
  bool found;
  for (int f = 0; f < m_subFolders.size(); f++) {
    QTreeWidgetItem *sf = m_subFolders.at(f);
    found               = false;
    for (int i = 0; i < sf->childCount(); i++) {
      if (!sf->child(i)->isHidden()) {
        found = true;
        break;
      }
    }
    sf->setHidden(!found);

    // Expand only if: has results AND user hasn't manually closed it
    if (found) {
      QString path = sf->parent() ? sf->parent()->text(0) + "/" + sf->text(0)
                                  : sf->text(0);
      if (!m_userCollapsedDuringSearch.contains(path)) {
        sf->setExpanded(true);
      }
    }
  }
  for (int f = 0; f < m_folders.size(); f++) {
    QTreeWidgetItem *fol = m_folders.at(f);
    found                = false;
    for (int i = 0; i < fol->childCount(); i++) {
      if (!fol->child(i)->isHidden()) {
        found = true;
        break;
      }
    }
    fol->setHidden(!found);

    // Expand only if: has results AND user hasn't manually closed it
    if (found) {
      QString folderName = fol->text(0);
      if (!m_userCollapsedDuringSearch.contains(folderName)) {
        fol->setExpanded(true);
      }
    }
  }

  show();
  emit searched(true);
  update();
}

//-----------------------------------------------------------------------------

void ShortcutTree::onCurrentItemChanged(QTreeWidgetItem *current,
                                        QTreeWidgetItem *previous) {
  ShortcutItem *item = dynamic_cast<ShortcutItem *>(current);
  emit actionSelected(item ? item->getAction() : 0);
}

//-----------------------------------------------------------------------------

void ShortcutTree::onShortcutChanged() {
  int i;
  for (i = 0; i < (int)m_items.size(); i++) m_items[i]->updateText();
}

//-----------------------------------------------------------------------------

void ShortcutTree::onItemClicked(const QModelIndex &index) {
  isExpanded(index) ? collapse(index) : expand(index);
}

//-----------------------------------------------------------------------------

void ShortcutTree::onItemCollapsed(QTreeWidgetItem *item) {
  // If search is active and user manually collapses a folder, remember it
  if (!m_lastSearchTerm.isEmpty()) {
    // Determine the path for this item
    QString path;
    if (item->parent()) {
      path = item->parent()->text(0) + "/" + item->text(0);
    } else {
      path = item->text(0);
    }
    m_userCollapsedDuringSearch.insert(path);
  }
}

//=============================================================================
// ShortcutPopup
//-----------------------------------------------------------------------------

ShortcutPopup::ShortcutPopup()
    : Dialog(TApp::instance()->getMainWindow(), false, false, "Shortcut") {
  // Register this instance
  s_instance = this;

  setWindowTitle(tr("Configure Shortcuts"));
  m_presetChoiceCB = new QComboBox(this);
  buildPresets();

  m_presetChoiceCB->setCurrentIndex(0);
  m_list = new ShortcutTree(this);
  m_list->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  m_list->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);

  m_sViewer            = new ShortcutViewer(this);
  m_removeBtn          = new QPushButton(tr("Remove"), this);
  m_loadShortcutsPopup = NULL;
  m_saveShortcutsPopup = NULL;
  m_dialog             = NULL;
  m_exportButton       = new QPushButton(tr("Export Current Shortcuts"), this);
  m_exportButton->setToolTip(tr("Export Current Shortcuts"));
  m_deletePresetButton = new QPushButton(tr("Delete"), this);
  m_deletePresetButton->setToolTip(tr("Delete Current Preset"));
  m_deletePresetButton->setIcon(createQIcon("delete"));
  m_savePresetButton = new QPushButton(tr("Save As"), this);
  m_savePresetButton->setToolTip(tr("Save Current Shortcuts as New Preset"));
  m_savePresetButton->setIcon(createQIcon("saveas"));
  m_loadPresetButton = new QPushButton(tr("Load"));
  m_loadPresetButton->setToolTip(tr("Use selected preset as shortcuts"));
  m_loadPresetButton->setIcon(createQIcon("load"));
  QGroupBox *presetBox = new QGroupBox(tr("Shortcut Presets"), this);
  presetBox->setObjectName("SolidLineFrame");
  m_clearAllShortcutsButton = new QPushButton(tr("Clear All Shortcuts"));
  QLabel *noSearchResultLabel =
      new QLabel(tr("Couldn't find any matching command."), this);
  noSearchResultLabel->setHidden(true);

  m_searchEdit = new QLineEdit(this);

  m_topLayout->setContentsMargins(5, 5, 5, 5);
  m_topLayout->setSpacing(8);
  {
    QHBoxLayout *searchLay = new QHBoxLayout();
    searchLay->setContentsMargins(0, 0, 0, 0);
    searchLay->setSpacing(5);
    {
      searchLay->addWidget(new QLabel(tr("Search:"), this), 0);
      searchLay->addWidget(m_searchEdit);
    }
    m_topLayout->addLayout(searchLay, 0);

    QVBoxLayout *listLay = new QVBoxLayout();
    listLay->setContentsMargins(0, 0, 0, 0);
    listLay->setSpacing(0);
    {
      listLay->addWidget(noSearchResultLabel, 0,
                         Qt::AlignTop | Qt::AlignHCenter);
      listLay->addWidget(m_list, 1);
    }
    m_topLayout->addLayout(listLay, 1);

    QHBoxLayout *bottomLayout = new QHBoxLayout();
    bottomLayout->setContentsMargins(0, 0, 0, 0);
    bottomLayout->setSpacing(1);
    {
      bottomLayout->addWidget(m_sViewer, 1);
      bottomLayout->addWidget(m_removeBtn, 0);
    }
    m_topLayout->addLayout(bottomLayout, 0);
    m_topLayout->addSpacing(10);
    QHBoxLayout *presetLay = new QHBoxLayout();
    presetLay->setContentsMargins(5, 5, 5, 5);
    presetLay->setSpacing(5);
    {
      presetLay->addWidget(new QLabel(tr("Preset:"), this), 0);
      presetLay->addWidget(m_presetChoiceCB, 1);
      presetLay->addWidget(m_loadPresetButton, 0);
      presetLay->addWidget(m_savePresetButton, 0);
      presetLay->addWidget(m_deletePresetButton, 0);
    }
    presetBox->setLayout(presetLay);
    m_topLayout->addWidget(presetBox, 0, Qt::AlignCenter);
    m_topLayout->addSpacing(10);
    QHBoxLayout *exportLay = new QHBoxLayout();
    exportLay->setContentsMargins(0, 0, 0, 0);
    exportLay->setSpacing(5);
    {
      exportLay->addWidget(m_exportButton, 0);
      exportLay->addWidget(m_clearAllShortcutsButton, 0);
    }
    m_topLayout->addLayout(exportLay, 0);
    // m_topLayout->addWidget(m_exportButton, 0);
  }

  connect(m_list, &ShortcutTree::actionSelected, m_sViewer,
          &ShortcutViewer::setAction);

  connect(m_removeBtn, &QPushButton::clicked, m_sViewer,
          &ShortcutViewer::removeShortcut);

  connect(m_sViewer, &ShortcutViewer::shortcutChanged, m_list,
          &ShortcutTree::onShortcutChanged);

  connect(m_list, &ShortcutTree::searched, noSearchResultLabel,
          &QLabel::setHidden);

  connect(m_searchEdit, &QLineEdit::textChanged, this,
          &ShortcutPopup::onSearchTextChanged);

  connect(m_presetChoiceCB, QOverload<int>::of(&QComboBox::currentIndexChanged),
          this, &ShortcutPopup::onPresetChanged);

  // Use a lambda to adapt the clicked() signal to the slot signature
  connect(m_exportButton, &QPushButton::clicked, this,
          [this]() { onExportButton(); });

  connect(m_deletePresetButton, &QPushButton::clicked, this,
          &ShortcutPopup::onDeletePreset);

  connect(m_savePresetButton, &QPushButton::clicked, this,
          &ShortcutPopup::onSavePreset);

  connect(m_loadPresetButton, &QPushButton::clicked, this,
          &ShortcutPopup::onLoadPreset);

  connect(m_clearAllShortcutsButton, &QPushButton::clicked, this,
          &ShortcutPopup::clearAllShortcuts);
}

//-----------------------------------------------------------------------------

ShortcutPopup::~ShortcutPopup() {
  // Save tree expansion state before closing
  if (m_list) {
    m_list->saveExpandedState();
  }

  // Unregister this instance
  if (s_instance == this) {
    s_instance = nullptr;
  }
}

//-----------------------------------------------------------------------------

void ShortcutPopup::refreshIfOpen() {
  // If the popup is currently open, refresh its tree to show new commands
  if (s_instance && s_instance->m_list) {
    s_instance->m_list->refreshTree();
  }
  // Note: The tree preserves expansion state and search filter automatically
}

//-----------------------------------------------------------------------------

void ShortcutPopup::onSearchTextChanged(const QString &text) {
  static bool busy = false;
  if (busy) return;
  busy = true;
  m_list->searchItems(text);
  busy = false;
}

//-----------------------------------------------------------------------------

void ShortcutPopup::onPresetChanged() {
  if (m_presetChoiceCB->currentData().toString() == QString("LoadFromFile")) {
    importPreset();
  }
}

//-----------------------------------------------------------------------------

void ShortcutPopup::clearAllShortcuts(bool warning) {
  if (warning) {
    QString question(tr("This will erase ALL shortcuts. Continue?"));
    int ret =
        DVGui::MsgBox(question, QObject::tr("OK"), QObject::tr("Cancel"), 0);
    if (ret == 0 || ret == 2) {
      // cancel (or closed message box window)
      return;
    }
    showDialog("Clearing All Shortcuts");
  }
  for (int commandType = UndefinedCommandType; commandType <= MenuCommandType;
       commandType++) {
    std::vector<QAction *> actions;
    CommandManager::instance()->getActions((CommandType)commandType, actions);
    for (QAction *action : actions) {
      qApp->processEvents();
      m_sViewer->setAction(action);
      m_sViewer->removeShortcut();
    }
  }
  setCurrentPresetPref("DELETED");
  // if warning is true, this was called directly- need to hide the dialog after
  if (warning) m_dialog->hide();
}

//-----------------------------------------------------------------------------

void ShortcutPopup::setPresetShortcuts(TFilePath fp) {
  QSettings preset(toQString(fp), QSettings::IniFormat);
  preset.beginGroup("shortcuts");
  QStringList allIds = preset.allKeys();
  QAction *action;
  for (QString id : allIds) {
    QByteArray ba      = id.toLatin1();
    const char *charId = ba.data();
    action = CommandManager::instance()->getAction((CommandId)charId);
    if (!action) continue;
    CommandManager::instance()->setShortcut(
        action, preset.value(id).toString().toStdString(), false);
  }
  preset.endGroup();
  emit m_sViewer->shortcutChanged();
  m_dialog->hide();
  buildPresets();
  setCurrentPresetPref(QString::fromStdString(fp.getName()));
}

//-----------------------------------------------------------------------------

bool ShortcutPopup::showConfirmDialog() {
  QString question(tr("This will overwrite all current shortcuts. Continue?"));
  int ret =
      DVGui::MsgBox(question, QObject::tr("OK"), QObject::tr("Cancel"), 0);
  if (ret == 0 || ret == 2) {
    // cancel (or closed message box window)
    return false;
  } else
    return true;
}

//-----------------------------------------------------------------------------

bool ShortcutPopup::showOverwriteDialog(QString name) {
  QString question(tr("A file named ") + name +
                   tr(" already exists.  Do you want to replace it?"));
  int ret = DVGui::MsgBox(question, QObject::tr("Yes"), QObject::tr("No"), 0);
  if (ret == 0 || ret == 2) {
    // cancel (or closed message box window)
    return false;
  } else
    return true;
}

//-----------------------------------------------------------------------------

void ShortcutPopup::showDialog(QString text) {
  if (m_dialog == NULL) {
    m_dialogLabel = new QLabel("", this);
    m_dialog      = new DVGui::Dialog(this, false, false);
    m_dialog->setWindowTitle(tr("OpenToonz - Setting Shortcuts"));
    m_dialog->setModal(false);

    m_dialog->setTopMargin(10);
    m_dialog->setTopSpacing(10);
    m_dialog->setLabelWidth(500);
    m_dialog->beginVLayout();
    m_dialog->addWidget(m_dialogLabel, false);
    m_dialog->endVLayout();
  }
  m_dialogLabel->setText(text);
  m_dialog->show();
}

//-----------------------------------------------------------------------------

void ShortcutPopup::onExportButton(TFilePath fp) {
  if (fp == TFilePath()) {
    m_saveShortcutsPopup = new GenericSaveFilePopup("Save Current Shortcuts");
    m_saveShortcutsPopup->addFilterType("ini");
    fp = m_saveShortcutsPopup->getPath();
    if (fp == TFilePath()) return;
  }
  showDialog(tr("Saving Shortcuts"));

  QSettings preset(toQString(fp), QSettings::IniFormat);
  preset.beginGroup("shortcuts");

  for (int commandType = UndefinedCommandType; commandType <= MenuCommandType;
       commandType++) {
    std::vector<QAction *> actions;
    CommandManager::instance()->getActions((CommandType)commandType, actions);
    for (QAction *action : actions) {
      qApp->processEvents();
      std::string id = CommandManager::instance()->getIdFromAction(action);
      std::string shortcut =
          CommandManager::instance()->getShortcutFromAction(action);
      if (shortcut != "") {
        preset.setValue(QString::fromStdString(id),
                        QString::fromStdString(shortcut));
      }
    }
  }

  preset.endGroup();

  m_dialog->hide();
}

//-----------------------------------------------------------------------------

void ShortcutPopup::onDeletePreset() {
  // change this to 5 once RETAS shortcuts are updated
  if (m_presetChoiceCB->currentIndex() <= 4) {
    DVGui::MsgBox(DVGui::CRITICAL, tr("Included presets cannot be deleted."));
    return;
  }

  QString question(tr("Are you sure you want to delete the preset: ") +
                   m_presetChoiceCB->currentText() + tr("?"));
  int ret = DVGui::MsgBox(question, QObject::tr("Yes"), QObject::tr("No"), 0);
  if (ret == 0 || ret == 2) {
    // cancel (or closed message box window)
    return;
  }
  TFilePath presetDir =
      ToonzFolder::getMyModuleDir() + TFilePath("shortcutpresets");
  QString presetName = m_presetChoiceCB->currentData().toString();
  if (TSystem::doesExistFileOrLevel(presetDir +
                                    TFilePath(presetName + ".ini"))) {
    TSystem::deleteFile(presetDir + TFilePath(presetName + ".ini"));
    buildPresets();
    m_presetChoiceCB->setCurrentIndex(0);
  }
  if (Preferences::instance()->getShortcutPreset() == presetName)
    setCurrentPresetPref("DELETED");
  getCurrentPresetPref();
}

//-----------------------------------------------------------------------------

void ShortcutPopup::importPreset() {
  m_loadShortcutsPopup = new GenericLoadFilePopup("Load Shortcuts File");
  m_loadShortcutsPopup->addFilterType("ini");
  TFilePath shortcutPath = m_loadShortcutsPopup->getPath();
  if (shortcutPath == TFilePath()) {
    m_presetChoiceCB->setCurrentIndex(0);
    return;
  }
  if (!showConfirmDialog()) return;

  TFilePath presetDir =
      ToonzFolder::getMyModuleDir() + TFilePath("shortcutpresets");
  if (!TSystem::doesExistFileOrLevel(presetDir)) {
    TSystem::mkDir(presetDir);
  }
  QString name        = shortcutPath.withoutParentDir().getQString();
  std::string strName = name.toStdString();
  if (TSystem::doesExistFileOrLevel(presetDir + TFilePath(name))) {
    if (!showOverwriteDialog(name)) return;
  }
  showDialog("Importing Shortcuts");
  TSystem::copyFile(presetDir + TFilePath(name), shortcutPath, true);
  clearAllShortcuts(false);
  setPresetShortcuts(presetDir + TFilePath(name));
  return;
}

//-----------------------------------------------------------------------------

void ShortcutPopup::onLoadPreset() {
  QString preset = m_presetChoiceCB->currentData().toString();
  TFilePath presetDir;
  if (m_presetChoiceCB->currentIndex() <= 4)
    presetDir =
        ToonzFolder::getProfileFolder() + TFilePath("layouts/shortcuts");
  else
    presetDir = ToonzFolder::getMyModuleDir() + TFilePath("shortcutpresets");

  if (preset.isEmpty()) return;
  if (preset == QString("LoadFromFile")) {
    importPreset();
    return;
  }

  if (!showConfirmDialog()) return;
  showDialog(tr("Setting Shortcuts"));
  TFilePath presetFilePath(preset + ".ini");
  if (TSystem::doesExistFileOrLevel(presetDir + presetFilePath)) {
    clearAllShortcuts(false);
    TFilePath fp = presetDir + presetFilePath;
    setPresetShortcuts(fp);
    return;
  }
  m_dialog->hide();
}

//-----------------------------------------------------------------------------

void ShortcutPopup::buildPresets() {
  m_presetChoiceCB->clear();

  m_presetChoiceCB->addItem("", QString(""));
  m_presetChoiceCB->addItem("OpenToonz", QString("defopentoonz"));
  // m_presetChoiceCB->addItem("RETAS PaintMan", QString("otretas"));
  m_presetChoiceCB->addItem("Toon Boom Harmony", QString("otharmony"));
  m_presetChoiceCB->addItem("Adobe Animate", QString("otanimate"));
  m_presetChoiceCB->addItem("Adobe Flash Pro", QString("otadobe"));

  TFilePath presetDir =
      ToonzFolder::getMyModuleDir() + TFilePath("shortcutpresets");
  if (TSystem::doesExistFileOrLevel(presetDir)) {
    TFilePathSet fps = TSystem::readDirectory(presetDir, true, true, false);
    QStringList customPresets;
    for (TFilePath fp : fps) {
      if (fp.getType() == "ini") {
        customPresets << QString::fromStdString(fp.getName());
        std::string name = fp.getName();
      }
    }
    customPresets.sort();
    for (auto customPreset : customPresets)
      m_presetChoiceCB->addItem(customPreset, customPreset);
  }
  m_presetChoiceCB->addItem(tr("Load from file..."), QString("LoadFromFile"));
}

//-----------------------------------------------------------------------------

void ShortcutPopup::onSavePreset() {
  QString presetName =
      DVGui::getText(tr("Enter Preset Name"), tr("Preset Name:"), "");
  if (presetName == "") return;
  TFilePath presetDir =
      ToonzFolder::getMyModuleDir() + TFilePath("shortcutpresets");
  if (!TSystem::doesExistFileOrLevel(presetDir)) {
    TSystem::mkDir(presetDir);
  }
  TFilePath fp;
  fp = presetDir + TFilePath(presetName + ".ini");
  if (TSystem::doesExistFileOrLevel(fp)) {
    if (!showOverwriteDialog(QString::fromStdString(fp.getName()))) return;
  }
  onExportButton(fp);

  buildPresets();
  setCurrentPresetPref(presetName);
}

//-----------------------------------------------------------------------------

void ShortcutPopup::showEvent(QShowEvent *se) {
  getCurrentPresetPref();

  // Refresh brush preset and size commands to ensure they're up-to-date
  ToolPresetCommandManager::instance()->refreshPresetCommands();
  ToolPresetCommandManager::instance()->refreshSizeCommands();

  // Restore search term from TEnv
  const QString lastSearchTerm =
      QString::fromStdString((std::string)ShortcutPopupSearchText);
  // Step 1: Restore search text in the field WITHOUT triggering searchItems
  {
    QSignalBlocker blocker(m_searchEdit);
    m_searchEdit->setText(lastSearchTerm);
  }

  // Step 2: Restore folder expansion state from TEnv
  m_list->restoreExpandedState();

  // Step 3: If there was a search, apply filtering WITHOUT changing expansion
  if (!lastSearchTerm.isEmpty()) {
    // Save current expansion state before filtering
    QStringList expandedBeforeFilter;
    for (int i = 0; i < m_list->topLevelItemCount(); i++) {
      QTreeWidgetItem *item = m_list->topLevelItem(i);
      if (item && item->isExpanded()) {
        expandedBeforeFilter.append(item->text(0));
      }
    }

    // Apply search filter (this will change expansion)
    m_list->searchItems(lastSearchTerm);

    // Restore the expansion state we had before filtering
    for (int i = 0; i < m_list->topLevelItemCount(); i++) {
      QTreeWidgetItem *item = m_list->topLevelItem(i);
      if (item) {
        item->setExpanded(expandedBeforeFilter.contains(item->text(0)));
      }
    }
  }
}

//-----------------------------------------------------------------------------

void ShortcutPopup::hideEvent(QHideEvent *he) {
  // Save tree expansion state when hiding the popup
  if (m_list) {
    m_list->saveExpandedState();
  }

  // Save search text to TEnv
  ShortcutPopupSearchText = m_searchEdit->text().toStdString();
  DVGui::Dialog::hideEvent(he);
}

//-----------------------------------------------------------------------------

void ShortcutPopup::setCurrentPresetPref(QString name) {
  Preferences::instance()->setValue(shortcutPreset, name);
  getCurrentPresetPref();
}

//-----------------------------------------------------------------------------

void ShortcutPopup::getCurrentPresetPref() {
  QString name = Preferences::instance()->getShortcutPreset();
  if (name == "DELETED") name = "";

  m_presetChoiceCB->setCurrentIndex(m_presetChoiceCB->findData(name));
}

OpenPopupCommandHandler<ShortcutPopup> openShortcutPopup(MI_ShortcutPopup);
