// Aseprite
// Copyright (C) 2019-2025  Igara Studio S.A.
// Copyright (C) 2001-2018  David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.

#ifdef HAVE_CONFIG_H
  #include "config.h"
#endif

#include "app/app_menus.h"

#include "app/app.h"
#include "app/commands/command.h"
#include "app/commands/commands.h"
#include "app/commands/params.h"
#include "app/console.h"
#include "app/extensions.h"
#include "app/gui_xml.h"
#include "app/i18n/strings.h"
#include "app/recent_files.h"
#include "app/resource_finder.h"
#include "app/tools/tool_box.h"
#include "app/ui/app_menuitem.h"
#include "app/ui/keyboard_shortcuts.h"
#include "app/ui/main_window.h"
#include "app/ui_context.h"
#include "base/fs.h"
#include "base/string.h"
#include "fmt/format.h"
#include "os/menus.h"
#include "os/system.h"
#include "ui/ui.h"
#include "ver/info.h"

#include "tinyxml2.h"

#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <string>

#define MENUS_TRACE(...) // TRACEARGS

namespace app {

using namespace tinyxml2;
using namespace ui;

namespace {

// TODO Move this to "os" layer
const int kUnicodeEsc = 27;
const int kUnicodeEnter = '\r';      // 10
const int kUnicodeInsert = 0xF727;   // NSInsertFunctionKey
const int kUnicodeDel = 0xF728;      // NSDeleteFunctionKey
const int kUnicodeHome = 0xF729;     // NSHomeFunctionKey
const int kUnicodeEnd = 0xF72B;      // NSEndFunctionKey
const int kUnicodePageUp = 0xF72C;   // NSPageUpFunctionKey
const int kUnicodePageDown = 0xF72D; // NSPageDownFunctionKey
const int kUnicodeLeft = 0xF702;     // NSLeftArrowFunctionKey
const int kUnicodeRight = 0xF703;    // NSRightArrowFunctionKey
const int kUnicodeUp = 0xF700;       // NSUpArrowFunctionKey
const int kUnicodeDown = 0xF701;     // NSDownArrowFunctionKey

const char* kFileRecentListGroup = "file_recent_list";

bool is_text_entry_shortcut(const os::Shortcut& shortcut)
{
  const os::KeyModifiers mod = shortcut.modifiers();
  const int chr = shortcut.unicode();
  const int lchr = std::tolower(chr);

  bool result =
    ((mod == os::KeyModifiers::kKeyNoneModifier || mod == os::KeyModifiers::kKeyShiftModifier) &&
     chr >= 32 && chr < 0xF000) ||
    ((mod == os::KeyModifiers::kKeyCmdModifier || mod == os::KeyModifiers::kKeyCtrlModifier) &&
     (lchr == 'a' || lchr == 'c' || lchr == 'v' || lchr == 'x')) ||
    (chr == kUnicodeInsert || chr == kUnicodeDel || chr == kUnicodeHome || chr == kUnicodeEnd ||
     chr == kUnicodeLeft || chr == kUnicodeRight || chr == kUnicodeEsc || chr == kUnicodeEnter);

  return result;
}

bool can_call_global_shortcut(const AppMenuItem::Native* native)
{
  ASSERT(native);

  ui::Manager* manager = ui::Manager::getDefault();
  ASSERT(manager);
  ui::Widget* focus = manager->getFocus();
  return
    // The mouse is not capture
    (manager->getCapture() == nullptr) &&
    // The foreground window must be the main window to avoid calling
    // a global command inside a modal dialog.
    (manager->getForegroundWindow() == App::instance()->mainWindow()) &&
    // If we are in a menubox window (e.g. we've pressed
    // Alt+mnemonic), we should disable the native shortcuts
    // temporarily so we can use mnemonics without modifiers
    // (e.g. Alt+S opens the Sprite menu, then 'S' key should execute
    // "Sprite Size" command in that menu, instead of Stroke command
    // which is in 'Edit > Stroke'). This is necessary in macOS, when
    // the native menu + Aseprite pixel-art menus are enabled.
    (dynamic_cast<MenuBoxWindow*>(manager->getTopWindow()) == nullptr) &&
    // The focused widget cannot be an entry, because entry fields
    // prefer text input, so we cannot call shortcuts without
    // modifiers (e.g. F or T keystrokes) to trigger a global command
    // in a text field.
    (focus == nullptr ||
     (focus->type() != ui::kEntryWidget && focus->type() != ui::kTextEditWidget) ||
     !is_text_entry_shortcut(native->shortcut)) &&
    (native->keyContext == KeyContext::Any ||
     native->keyContext == KeyboardShortcuts::getCurrentKeyContext());
}

// TODO this should be on laf-os library (or we should use
// os::Shortcut instead of ui::Shortcuts)
int from_scancode_to_unicode(KeyScancode scancode)
{
  static int map[] = {
    0,                // kKeyNil
    'a',              // kKeyA
    'b',              // kKeyB
    'c',              // kKeyC
    'd',              // kKeyD
    'e',              // kKeyE
    'f',              // kKeyF
    'g',              // kKeyG
    'h',              // kKeyH
    'i',              // kKeyI
    'j',              // kKeyJ
    'k',              // kKeyK
    'l',              // kKeyL
    'm',              // kKeyM
    'n',              // kKeyN
    'o',              // kKeyO
    'p',              // kKeyP
    'q',              // kKeyQ
    'r',              // kKeyR
    's',              // kKeyS
    't',              // kKeyT
    'u',              // kKeyU
    'v',              // kKeyV
    'w',              // kKeyW
    'x',              // kKeyX
    'y',              // kKeyY
    'z',              // kKeyZ
    '0',              // kKey0
    '1',              // kKey1
    '2',              // kKey2
    '3',              // kKey3
    '4',              // kKey4
    '5',              // kKey5
    '6',              // kKey6
    '7',              // kKey7
    '8',              // kKey8
    '9',              // kKey9
    0,                // kKey0Pad
    0,                // kKey1Pad
    0,                // kKey2Pad
    0,                // kKey3Pad
    0,                // kKey4Pad
    0,                // kKey5Pad
    0,                // kKey6Pad
    0,                // kKey7Pad
    0,                // kKey8Pad
    0,                // kKey9Pad
    0xF704,           // kKeyF1 (NSF1FunctionKey)
    0xF705,           // kKeyF2
    0xF706,           // kKeyF3
    0xF707,           // kKeyF4
    0xF708,           // kKeyF5
    0xF709,           // kKeyF6
    0xF70A,           // kKeyF7
    0xF70B,           // kKeyF8
    0xF70C,           // kKeyF9
    0xF70D,           // kKeyF10
    0xF70E,           // kKeyF11
    0xF70F,           // kKeyF12
    kUnicodeEsc,      // kKeyEsc
    '~',              // kKeyTilde
    '-',              // kKeyMinus
    '=',              // kKeyEquals
    8,                // kKeyBackspace
    9,                // kKeyTab
    '[',              // kKeyOpenbrace
    ']',              // kKeyClosebrace
    kUnicodeEnter,    // kKeyEnter
    ':',              // kKeyColon
    '\'',             // kKeyQuote
    '\\',             // kKeyBackslash
    0,                // kKeyBackslash2
    ',',              // kKeyComma
    '.',              // kKeyStop
    '/',              // kKeySlash
    ' ',              // kKeySpace
    kUnicodeInsert,   // kKeyInsert (NSInsertFunctionKey)
    kUnicodeDel,      // kKeyDel (NSDeleteFunctionKey)
    kUnicodeHome,     // kKeyHome (NSHomeFunctionKey)
    kUnicodeEnd,      // kKeyEnd (NSEndFunctionKey)
    kUnicodePageUp,   // kKeyPageUp (NSPageUpFunctionKey)
    kUnicodePageDown, // kKeyPageDown (NSPageDownFunctionKey)
    kUnicodeLeft,     // kKeyLeft (NSLeftArrowFunctionKey)
    kUnicodeRight,    // kKeyRight (NSRightArrowFunctionKey)
    kUnicodeUp,       // kKeyUp (NSUpArrowFunctionKey)
    kUnicodeDown,     // kKeyDown (NSDownArrowFunctionKey)
    '/',              // kKeySlashPad
    '*',              // kKeyAsterisk
    0,                // kKeyMinusPad
    0,                // kKeyPlusPad
    0,                // kKeyDelPad
    0,                // kKeyEnterPad
    0,                // kKeyPrtscr
    0,                // kKeyPause
    0,                // kKeyAbntC1
    0,                // kKeyYen
    0,                // kKeyKana
    0,                // kKeyConvert
    0,                // kKeyNoconvert
    0,                // kKeyAt
    0,                // kKeyCircumflex
    0,                // kKeyColon2
    0,                // kKeyKanji
    0,                // kKeyEqualsPad
    '`',              // kKeyBackquote
    0,                // kKeySemicolon
    0,                // kKeyUnknown1
    0,                // kKeyUnknown2
    0,                // kKeyUnknown3
    0,                // kKeyUnknown4
    0,                // kKeyUnknown5
    0,                // kKeyUnknown6
    0,                // kKeyUnknown7
    0,                // kKeyUnknown8
    0,                // kKeyLShift
    0,                // kKeyRShift
    0,                // kKeyLControl
    0,                // kKeyRControl
    0,                // kKeyAlt
    0,                // kKeyAltGr
    0,                // kKeyLWin
    0,                // kKeyRWin
    0,                // kKeyMenu
    0,                // kKeyCommand
    0,                // kKeyScrLock
    0,                // kKeyNumLock
    0,                // kKeyCapsLock
  };
  if (scancode >= 0 && scancode < sizeof(map) / sizeof(map[0]))
    return map[scancode];
  else
    return 0;
}

AppMenuItem::Native get_native_shortcut_for_command(const char* commandId,
                                                    const Params& params = Params())
{
  AppMenuItem::Native native;
  KeyPtr key = KeyboardShortcuts::instance()->command(commandId, params);
  if (key) {
    native.shortcut = get_os_shortcut_from_key(key.get());
    native.keyContext = key->keycontext();
  }
  return native;
}

void destroy_menu_item(ui::Widget* item)
{
  if (item->parent())
    item->parent()->removeChild(item);

  if (auto appItem = dynamic_cast<AppMenuItem*>(item)) {
    if (appItem)
      appItem->disposeNative();
  }

  item->deferDelete();
}

} // anonymous namespace

os::Shortcut get_os_shortcut_from_key(const Key* key)
{
  if (key && !key->shortcuts().empty()) {
    const ui::Shortcut& shortcut = key->shortcuts().front();

#if LAF_MACOS
    // Shortcuts with spacebar as modifier do not work well in macOS
    // (they will be called when the space bar is unpressed too).
    if ((shortcut.modifiers() & ui::kKeySpaceModifier) == ui::kKeySpaceModifier)
      return os::Shortcut();
#endif

    return os::Shortcut((shortcut.unicodeChar() ? shortcut.unicodeChar() :
                                                  from_scancode_to_unicode(shortcut.scancode())),
                        shortcut.modifiers());
  }
  return {};
}

AppMenus* AppMenus::s_instance = nullptr;

// static
AppMenus* AppMenus::instance()
{
  return s_instance;
}

AppMenus::AppMenus(RecentFiles* recentFiles) : m_recentFilesPlaceholder(nullptr), m_osMenu(nullptr)
{
  ASSERT(s_instance == nullptr);
  s_instance = this;
  m_recentFilesConn = recentFiles->Changed.connect([this] { rebuildRecentList(); });
}

AppMenus::~AppMenus()
{
  ASSERT(s_instance == this);
  s_instance = nullptr;
}

void AppMenus::reload()
{
  MENUS_TRACE("MENUS: AppMenus::reload()");

  XMLDocument* doc = GuiXml::instance()->doc();
  XMLHandle handle(doc);
  const char* path = GuiXml::instance()->filename();

  ////////////////////////////////////////
  // Remove all menu items added to groups from recent files and
  // scripts so we can re-add them later in the new menus.

  for (auto& it : m_groups) {
    GroupInfo& group = it.second;
    MENUS_TRACE("MENUS: - groups", it.first, "with", group.items.size(), "item(s)");

    for (auto& item : group.items)
      group.menu->removeChild(item);

    // These values will be restored later
    group.end = nullptr;
  }

  ////////////////////////////////////////
  // Load menus

  LOG("MENU: Loading menus from %s\n", path);

  m_rootMenu.reset(loadMenuById(handle, "main_menu"));

  LOG("MENU: Main menu loaded.\n");

  m_tabPopupMenu.reset(loadMenuById(handle, "tab_popup_menu"));
  m_documentTabPopupMenu.reset(loadMenuById(handle, "document_tab_popup_menu"));
  m_layerPopupMenu.reset(loadMenuById(handle, "layer_popup_menu"));
  m_framePopupMenu.reset(loadMenuById(handle, "frame_popup_menu"));
  m_celPopupMenu.reset(loadMenuById(handle, "cel_popup_menu"));
  m_celMovementPopupMenu.reset(loadMenuById(handle, "cel_movement_popup_menu"));
  m_tagPopupMenu.reset(loadMenuById(handle, "tag_popup_menu"));
  m_slicePopupMenu.reset(loadMenuById(handle, "slice_popup_menu"));
  m_palettePopupMenu.reset(loadMenuById(handle, "palette_popup_menu"));
  m_inkPopupMenu.reset(loadMenuById(handle, "ink_popup_menu"));
  m_newFramePopupMenu.reset(loadMenuById(handle, "new_frame_popup_menu"));

  // Add one menu item to run each script from the user scripts/ folder
  {
    MenuItem* scriptsMenu = dynamic_cast<MenuItem*>(m_rootMenu->findItemById("scripts_menu"));
#ifdef ENABLE_SCRIPTING
    // Load scripts
    ResourceFinder rf;
    rf.includeUserDir("scripts");
    std::string scriptsDir = rf.getFirstOrCreateDefault();
    if (base::is_directory(scriptsDir)) {
      loadScriptsSubmenu(scriptsMenu->getSubmenu(), scriptsDir, true);
    }
#else
    // Scripting is not available
    if (scriptsMenu) {
      delete scriptsMenu;
      delete m_rootMenu->findItemById("scripts_menu_separator");

      // Remove scripts group
      auto it = m_groups.find("file_scripts");
      if (it != m_groups.end())
        m_groups.erase(it);
    }
#endif
  }

  // Remove the "Enter license" menu item when DRM is not enabled.
#ifndef ENABLE_DRM
  if (auto helpMenuItem = m_rootMenu->findItemById("help_menu")) {
    if (Menu* helpMenu = dynamic_cast<MenuItem*>(helpMenuItem)->getSubmenu()) {
      delete helpMenu->findChild("enter_license_separator");
      delete helpMenu->findChild("enter_license");
    }

    auto it = m_groups.find("help_enter_license");
    if (it != m_groups.end())
      m_groups.erase(it);
  }
#endif

  ////////////////////////////////////////
  // Re-add menu items in groups (recent files & scripts)

  for (auto& it : m_groups) {
    GroupInfo& group = it.second;
    if (group.menu) {
      MENUS_TRACE("MENUS: - re-adding group ", it.first, "with", group.items.size(), "item(s)");

      auto menu = group.menu;
      int insertIndex = menu->getChildIndex(group.end);
      if (insertIndex < 0)
        insertIndex = -1;
      for (auto& item : group.items) {
        menu->insertChild(++insertIndex, item);
        group.end = item;
      }
    }
    // Delete items that don't have a group now
    else {
      MENUS_TRACE("MENUS: - deleting group ", it.first, "with", group.items.size(), "item(s)");
      for (auto& item : group.items)
        item->deferDelete();
      group.items.clear();
    }
  }

  ////////////////////////////////////////
  // Load keyboard shortcuts for commands

  LOG("MENU: Loading commands keyboard shortcuts from %s\n", path);

  XMLElement* xmlKey = handle.FirstChildElement("gui").FirstChildElement("keyboard").ToElement();

  // From a fresh start, load the default keys
  KeyboardShortcuts::instance()->clear();
  KeyboardShortcuts::instance()->importFile(xmlKey, KeySource::Original);

  // Load extension-defined keys
  for (const Extension* ext : App::instance()->extensions()) {
    if (ext->isEnabled() && ext->hasKeys()) {
      for (const auto& kv : ext->keys()) {
        KeyboardShortcuts::instance()->importFile(kv.second, KeySource::ExtensionDefined);
      }
    }
  }

  // Load user-defined keys
  {
    ResourceFinder rf;
    rf.includeUserDir("user.aseprite-keys");
    std::string fn = rf.getFirstOrCreateDefault();
    if (base::is_file(fn))
      KeyboardShortcuts::instance()->importFile(fn, KeySource::UserDefined);
  }

  // Create native menus after the default + user defined keyboard
  // shortcuts are loaded correctly.
  createNativeMenus();

  MenusLoaded();
}

#ifdef ENABLE_SCRIPTING
void AppMenus::loadScriptsSubmenu(ui::Menu* menu, const std::string& dir, const bool rootLevel)
{
  auto files = base::list_files(dir);
  std::sort(files.begin(), files.end(), [](const std::string& a, const std::string& b) {
    return base::compare_filenames(a, b) < 0;
  });
  int insertPos = 0;
  for (auto fn : files) {
    std::string fullFn = base::join_path(dir, fn);
    AppMenuItem* menuitem = nullptr;

    if (fn[0] == '.') // Ignore all files and directories that start with a dot
      continue;

    if (base::is_file(fullFn)) {
      if (base::string_to_lower(base::get_file_extension(fn)) == "lua") {
        Params params;
        params.set("filename", fullFn.c_str());
        menuitem =
          new AppMenuItem(base::get_file_title(fn).c_str(), CommandId::RunScript(), params);
      }
    }
    else if (base::is_directory(fullFn)) {
      Menu* submenu = new Menu();
      loadScriptsSubmenu(submenu, fullFn, false);

      menuitem = new AppMenuItem(base::get_file_title(fn).c_str());
      menuitem->setSubmenu(submenu);
    }
    if (menuitem) {
      menu->insertChild(insertPos++, menuitem);
    }
  }
  if (rootLevel && insertPos > 0)
    menu->insertChild(insertPos, new MenuSeparator());
}
#endif

void AppMenus::initTheme()
{
  updateMenusList();
  for (Menu* menu : m_menus)
    if (menu)
      menu->initTheme();
}

bool AppMenus::rebuildRecentList()
{
  MENUS_TRACE("MENUS: AppMenus::rebuildRecentList m_recentFilesPlaceholder=",
              m_recentFilesPlaceholder);

  if (!m_recentFilesPlaceholder)
    return true;

  Menu* menu = dynamic_cast<Menu*>(m_recentFilesPlaceholder->parent());
  if (!menu)
    return false;

  AppMenuItem* owner = dynamic_cast<AppMenuItem*>(menu->getOwnerMenuItem());
  if (!owner || owner->hasSubmenuOpened())
    return false;

  // Remove active items
  for (auto item : m_recentMenuItems)
    removeMenuItemFromGroup(item);
  m_recentMenuItems.clear();

  auto recent = App::instance()->recentFiles();
  base::paths files;
  files.insert(files.end(), recent->pinnedFiles().begin(), recent->pinnedFiles().end());
  files.insert(files.end(), recent->recentFiles().begin(), recent->recentFiles().end());
  if (!files.empty()) {
    Params params;
    for (const auto& fn : files) {
      params.set("filename", fn.c_str());

      std::unique_ptr<AppMenuItem> menuitem(
        new AppMenuItem(base::get_file_name(fn).c_str(), CommandId::OpenFile(), params));
      menuitem->setIsRecentFileItem(true);

      m_recentMenuItems.push_back(menuitem.get());
      addMenuItemIntoGroup(kFileRecentListGroup, std::move(menuitem));
    }
  }
  else {
    std::unique_ptr<AppMenuItem> menuitem(
      new AppMenuItem(Strings::main_menu_file_no_recent_file()));
    menuitem->setIsRecentFileItem(true);
    menuitem->setEnabled(false);

    m_recentMenuItems.push_back(menuitem.get());
    addMenuItemIntoGroup(kFileRecentListGroup, std::move(menuitem));
  }

  // Sync native menus
  if (owner->native() && owner->native()->menuItem) {
    auto menus = os::System::instance()->menus();
    os::MenuRef osMenu = (menus ? menus->makeMenu() : nullptr);
    if (osMenu) {
      createNativeSubmenus(osMenu.get(), menu);
      owner->native()->menuItem->setSubmenu(osMenu);
    }
  }

  return true;
}

Menu* AppMenus::getAnimationMenu()
{
  auto menuItem = dynamic_cast<MenuItem*>(m_rootMenu->findItemById("animation_menu"));
  if (menuItem)
    return menuItem->getSubmenu();
  else
    return nullptr;
}

void AppMenus::addMenuGroup(const std::string& groupId, MenuItem* menuItem)
{
  GroupInfo& group = m_groups[groupId];
  ASSERT(group.menu == nullptr);
  ASSERT(group.end == nullptr);
  group.menu = menuItem->getSubmenu();
  group.end = nullptr;
}

void AppMenus::removeMenuGroup(const std::string& groupId)
{
  auto it = m_groups.find(groupId);
  if (it != m_groups.end()) {
    GroupInfo& group = it->second;

    ASSERT(group.items.empty()); // To remove a group, the group must be empty

    if (group.menu->getOwnerMenuItem()) {
      ui::MenuItem* item = group.menu->getOwnerMenuItem();
      removeMenuItemFromGroup([item](Widget* i) { return item == i; });
    }
    m_groups.erase(it);
  }
}

void AppMenus::addMenuItemIntoGroup(const std::string& groupId, std::unique_ptr<Widget>&& menuItem)
{
  auto it = m_groups.find(groupId);
  if (it == m_groups.end()) {
    LOG(ERROR,
        "MENU: An extension tried to add a command (%s) in a non-existent group (%s)\n",
        menuItem->text().c_str(),
        groupId.c_str());
    menuItem.release();
    return;
  }

  GroupInfo& group = it->second;
  Menu* menu = group.menu;
  ASSERT(menu);

  if (group.end) {
    int insertIndex = menu->getChildIndex(group.end);
    ASSERT(insertIndex >= 0);
    menu->insertChild(insertIndex + 1, menuItem.get());
  }
  else {
    menu->addChild(menuItem.get());
  }

  group.end = menuItem.get();
  group.items.push_back(menuItem.get());

  menuItem.release();
}

template<typename Pred>
void AppMenus::removeMenuItemFromGroup(Pred pred)
{
  for (auto& it : m_groups) {
    GroupInfo& group = it.second;
    for (auto it = group.items.begin(); it != group.items.end();) {
      auto& item = *it;
      if (pred(item)) {
        if (item == group.end)
          group.end = group.end->previousSibling();

        destroy_menu_item(item);

        it = group.items.erase(it);
      }
      else {
        ++it;
      }
    }
  }
}

void AppMenus::removeMenuItemFromGroup(Command* cmd)
{
  removeMenuItemFromGroup([cmd](Widget* item) {
    auto appMenuItem = dynamic_cast<AppMenuItem*>(item);
    return (appMenuItem && appMenuItem->getCommand() == cmd);
  });
}

void AppMenus::removeMenuItemFromGroup(Widget* menuItem)
{
  removeMenuItemFromGroup([menuItem](Widget* item) { return (item == menuItem); });
}

Menu* AppMenus::loadMenuById(XMLHandle& handle, const char* id)
{
  ASSERT(id != NULL);

  // <gui><menus><menu>
  XMLElement* xmlMenu =
    handle.FirstChildElement("gui").FirstChildElement("menus").FirstChildElement("menu").ToElement();
  while (xmlMenu) {
    const char* menuId = xmlMenu->Attribute("id");

    if (menuId && strcmp(menuId, id) == 0) {
      m_xmlTranslator.setStringIdPrefix(menuId);
      return convertXmlelemToMenu(xmlMenu);
    }

    xmlMenu = xmlMenu->NextSiblingElement();
  }

  throw base::Exception("Error loading menu '%s'\nReinstall the application.", id);
}

Menu* AppMenus::convertXmlelemToMenu(XMLElement* elem)
{
  Menu* menu = new Menu();
  menu->setText(m_xmlTranslator(elem, "text"));

  XMLElement* child = elem->FirstChildElement();
  while (child) {
    Widget* menuitem = convertXmlelemToMenuitem(child, menu);
    if (menuitem)
      menu->addChild(menuitem);
    else
      throw base::Exception("Error converting the element \"%s\" to a menu-item.\n",
                            static_cast<const char*>(child->Value()));

    child = child->NextSiblingElement();
  }

  return menu;
}

Widget* AppMenus::convertXmlelemToMenuitem(XMLElement* elem, Menu* menu)
{
  const char* id = elem->Attribute("id");
  const char* group = elem->Attribute("group");

  // is it a <separator>?
  if (strcmp(elem->Value(), "separator") == 0) {
    auto item = new MenuSeparator;
    if (id) {
      item->setId(id);

      // Recent list menu
      if (std::strcmp(id, "recent_files_placeholder") == 0) {
        m_recentFilesPlaceholder = item;
      }
    }
    if (group) {
      m_groups[group].menu = menu;
      m_groups[group].end = item;
    }
    return item;
  }

  const char* commandId = elem->Attribute("command");
  Command* command = (commandId ? Commands::instance()->byId(commandId) : nullptr);

  // load params
  Params params;
  if (command) {
    XMLElement* xmlParam = elem->FirstChildElement("param");
    while (xmlParam) {
      const char* param_name = xmlParam->Attribute("name");
      const char* param_value = xmlParam->Attribute("value");

      if (param_name && param_value)
        params.set(param_name, param_value);

      xmlParam = xmlParam->NextSiblingElement();
    }
  }

  // Create the item
  AppMenuItem* menuitem =
    new AppMenuItem(m_xmlTranslator(elem, "text"), (command ? command->id() : ""), params);
  if (!menuitem)
    return nullptr;

  // Get menu item text from command friendly name
  if (command && menuitem->text().empty()) {
    command->loadParams(params);
    menuitem->setText(command->friendlyName());
  }
  // If the menu item has a specific text, process its mnemonic
  else {
    menuitem->processMnemonicFromText();
  }

  if (id)
    menuitem->setId(id);
  if (group) {
    m_groups[group].menu = menu;
    m_groups[group].end = menuitem;
  }

#if LAF_MACOS
  const char* standard = elem->Attribute("standard");
  if (standard && strcmp(standard, "edit") == 0)
    menuitem->setAsStandardEditMenu();
#endif

  // Has it a ID?
  if (id) {
    if (std::strcmp(id, "help_menu") == 0) {
      m_helpMenuitem = menuitem;
    }
  }

  // Has it a sub-menu (<menu>)?
  if (strcmp(elem->Value(), "menu") == 0) {
    // Create the sub-menu
    Menu* subMenu = convertXmlelemToMenu(elem);
    if (!subMenu)
      throw base::Exception("Error reading the sub-menu\n");

    menuitem->setSubmenu(subMenu);
  }

  return menuitem;
}

void AppMenus::applyShortcutToMenuitemsWithCommand(Command* command,
                                                   const Params& params,
                                                   const KeyPtr& key)
{
  updateMenusList();
  for (Menu* menu : m_menus)
    if (menu)
      applyShortcutToMenuitemsWithCommand(menu, command, params, key);
}

void AppMenus::applyShortcutToMenuitemsWithCommand(Menu* menu,
                                                   Command* command,
                                                   const Params& params,
                                                   const KeyPtr& key)
{
  for (auto child : menu->children()) {
    if (child->type() == kMenuItemWidget) {
      AppMenuItem* menuitem = dynamic_cast<AppMenuItem*>(child);
      if (!menuitem)
        continue;

      const std::string& mi_commandId = menuitem->getCommandId();
      const Params& mi_params = menuitem->getParams();

      if ((base::utf8_icmp(mi_commandId, command->id()) == 0) && (mi_params == params)) {
        // Set the keyboard shortcut to be shown in this menu-item
        menuitem->setKey(key);
      }

      if (Menu* submenu = menuitem->getSubmenu())
        applyShortcutToMenuitemsWithCommand(submenu, command, params, key);
    }
  }
}

void AppMenus::syncNativeMenuItemKeyShortcuts()
{
  syncNativeMenuItemKeyShortcuts(m_rootMenu.get());
}

void AppMenus::syncNativeMenuItemKeyShortcuts(Menu* menu)
{
  for (auto child : menu->children()) {
    if (child->type() == kMenuItemWidget) {
      if (AppMenuItem* menuitem = dynamic_cast<AppMenuItem*>(child))
        menuitem->syncNativeMenuItemKeyShortcut();

      if (Menu* submenu = static_cast<MenuItem*>(child)->getSubmenu())
        syncNativeMenuItemKeyShortcuts(submenu);
    }
  }
}

// TODO redesign the list of popup menus, it might be an
//      autogenerated widget from 'gen'
void AppMenus::updateMenusList()
{
  m_menus.clear();
  m_menus.push_back(m_rootMenu.get());
  m_menus.push_back(m_tabPopupMenu.get());
  m_menus.push_back(m_documentTabPopupMenu.get());
  m_menus.push_back(m_layerPopupMenu.get());
  m_menus.push_back(m_framePopupMenu.get());
  m_menus.push_back(m_celPopupMenu.get());
  m_menus.push_back(m_celMovementPopupMenu.get());
  m_menus.push_back(m_tagPopupMenu.get());
  m_menus.push_back(m_slicePopupMenu.get());
  m_menus.push_back(m_palettePopupMenu.get());
  m_menus.push_back(m_inkPopupMenu.get());
  m_menus.push_back(m_newFramePopupMenu.get());
}

void AppMenus::createNativeMenus()
{
  os::Menus* menus = os::System::instance()->menus();
  if (!menus) // This platform doesn't support native menu items
    return;

  // Save a reference to the old menu to avoid destroying it.
  os::MenuRef oldOSMenu = m_osMenu;
  m_osMenu = menus->makeMenu();

#if LAF_MACOS // Create default macOS app menus (App ... Window)
  {
    os::MenuItemInfo about(fmt::format("About {}", get_app_name()));
    auto native = get_native_shortcut_for_command(CommandId::About());
    about.shortcut = native.shortcut;
    about.execute = [native] {
      if (can_call_global_shortcut(&native)) {
        Command* cmd = Commands::instance()->byId(CommandId::About());
        UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
      }
    };
    about.validate = [native](os::MenuItem* item) {
      item->setEnabled(can_call_global_shortcut(&native));
    };

    os::MenuItemInfo preferences("Preferences...");
    native = get_native_shortcut_for_command(CommandId::Options());
    preferences.shortcut = native.shortcut;
    preferences.execute = [native] {
      if (can_call_global_shortcut(&native)) {
        Command* cmd = Commands::instance()->byId(CommandId::Options());
        UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
      }
    };
    preferences.validate = [native](os::MenuItem* item) {
      item->setEnabled(can_call_global_shortcut(&native));
    };

    os::MenuItemInfo hide(fmt::format("Hide {}", get_app_name()), os::MenuItemInfo::Hide);
    hide.shortcut = os::Shortcut('h', os::kKeyCmdModifier);

    os::MenuItemInfo quit(fmt::format("Quit {}", get_app_name()), os::MenuItemInfo::Quit);
    quit.shortcut = os::Shortcut('q', os::kKeyCmdModifier);

    os::MenuRef appMenu = menus->makeMenu();
    appMenu->addItem(menus->makeMenuItem(about));
    appMenu->addItem(menus->makeMenuItem(os::MenuItemInfo(os::MenuItemInfo::Separator)));
    appMenu->addItem(menus->makeMenuItem(preferences));
    appMenu->addItem(menus->makeMenuItem(os::MenuItemInfo(os::MenuItemInfo::Separator)));
    appMenu->addItem(menus->makeMenuItem(hide));
    appMenu->addItem(
      menus->makeMenuItem(os::MenuItemInfo("Hide Others", os::MenuItemInfo::HideOthers)));
    appMenu->addItem(menus->makeMenuItem(os::MenuItemInfo("Show All", os::MenuItemInfo::ShowAll)));
    appMenu->addItem(menus->makeMenuItem(os::MenuItemInfo(os::MenuItemInfo::Separator)));
    appMenu->addItem(menus->makeMenuItem(quit));

    os::MenuItemRef appItem = menus->makeMenuItem(os::MenuItemInfo("App"));
    appItem->setSubmenu(appMenu);
    m_osMenu->addItem(appItem);
  }
#endif

  createNativeSubmenus(m_osMenu.get(), m_rootMenu.get());

#if LAF_MACOS
  {
    // Search the index where help menu is located (so the Window menu
    // can take its place/index position)
    int i = 0, helpIndex = int(m_rootMenu->children().size());
    for (const auto child : m_rootMenu->children()) {
      if (child == m_helpMenuitem) {
        helpIndex = i;
        break;
      }
      ++i;
    }

    os::MenuItemInfo minimize("Minimize", os::MenuItemInfo::Minimize);
    minimize.shortcut = os::Shortcut('m', os::kKeyCmdModifier);

    os::MenuRef windowMenu = menus->makeMenu();
    windowMenu->addItem(menus->makeMenuItem(minimize));
    windowMenu->addItem(menus->makeMenuItem(os::MenuItemInfo("Zoom", os::MenuItemInfo::Zoom)));

    os::MenuItemRef windowItem = menus->makeMenuItem(os::MenuItemInfo("Window"));
    windowItem->setSubmenu(windowMenu);

    // We use helpIndex+1 because the first index in m_osMenu is the
    // App menu.
    m_osMenu->insertItem(helpIndex + 1, windowItem);
  }
#endif

  menus->setAppMenu(m_osMenu);
  if (oldOSMenu)
    oldOSMenu.reset();
}

void AppMenus::createNativeSubmenus(os::Menu* osMenu, const ui::Menu* uiMenu)
{
  os::Menus* menus = os::System::instance()->menus();

  for (const auto& child : uiMenu->children()) {
    os::MenuItemInfo info;
    AppMenuItem* appMenuItem = dynamic_cast<AppMenuItem*>(child);
    AppMenuItem::Native native;

    if (child->type() == kSeparatorWidget) {
      info.type = os::MenuItemInfo::Separator;
    }
    else if (child->type() == kMenuItemWidget) {
      if (appMenuItem && appMenuItem->getCommand()) {
        native = get_native_shortcut_for_command(appMenuItem->getCommandId().c_str(),
                                                 appMenuItem->getParams());
      }

      info.type = os::MenuItemInfo::Normal;
      info.text = child->text();
      info.shortcut = native.shortcut;
      info.execute = [appMenuItem] {
        if (can_call_global_shortcut(appMenuItem->native()))
          appMenuItem->executeClick();
      };
      info.validate = [appMenuItem](os::MenuItem* osItem) {
        if (can_call_global_shortcut(appMenuItem->native())) {
          appMenuItem->validateItem();
          osItem->setEnabled(appMenuItem->isEnabled());
          osItem->setChecked(appMenuItem->isSelected());
        }
        else {
          // Disable item when there are a modal window
          osItem->setEnabled(false);
        }
      };
    }
    else {
      ASSERT(false); // Unsupported menu item type
      continue;
    }

    os::MenuItemRef osItem = menus->makeMenuItem(info);
    if (osItem) {
      osMenu->addItem(osItem);
      if (appMenuItem) {
        native.menuItem = osItem;
        appMenuItem->setNative(native);
      }

      if (child->type() == ui::kMenuItemWidget && ((ui::MenuItem*)child)->hasSubmenu()) {
        os::MenuRef osSubmenu = menus->makeMenu();
        createNativeSubmenus(osSubmenu.get(), ((ui::MenuItem*)child)->getSubmenu());
        osItem->setSubmenu(osSubmenu);
      }
    }
  }
}

} // namespace app
