-- Copyright 2007-2022 Mitchell. See LICENSE.
-- Contributions from Robert Gieseke.

local M = {}

--[[ This comment is for LuaDoc.
---
-- Defines the menus used by Textadept.
-- Menus are simply tables of menu items and submenus and may be edited in place. A menu item
-- itself is a table whose first element is a menu label and whose second element is a menu
-- command to run. Submenus have `title` keys assigned to string text.
module('textadept.menu')]]

local _L, SEPARATOR = _L, {''}

-- LuaFormatter off
-- The following buffer and view functions need to be made constant in order for menu items to
-- identify the key associated with the functions.
local menu_buffer_functions = {'undo','redo','cut','copy','paste','selection_duplicate','clear','select_all','upper_case','lower_case','move_selected_lines_up','move_selected_lines_down'}
for _, f in ipairs(menu_buffer_functions) do buffer[f] = buffer[f] end
view.zoom_in, view.zoom_out = view.zoom_in, view.zoom_out
-- LuaFormatter on

-- Commonly used functions in menu commands.
local sel_enc = textadept.editing.select_enclosed
local enc = textadept.editing.enclose
local function set_indentation(i)
  buffer.tab_width = i
  events.emit(events.UPDATE_UI, 1) -- for updating statusbar
end
local function set_eol_mode(mode)
  buffer.eol_mode = mode
  buffer:convert_eols(mode)
  events.emit(events.UPDATE_UI, 1) -- for updating statusbar
end
local function set_encoding(encoding)
  buffer:set_encoding(encoding)
  events.emit(events.UPDATE_UI, 1) -- for updating statusbar
end
local function open_page(url)
  local cmd = (WIN32 and 'start ""') or (OSX and 'open') or 'xdg-open'
  os.spawn(string.format('%s "%s"', cmd, not OSX and url or 'file://' .. url))
end

-- LuaFormatter off
---
-- The default main menubar.
-- Individual menus, submenus, and menu items can be retrieved by name in addition to table
-- index number.
-- @class table
-- @name menubar
-- @usage textadept.menu.menubar[_L['File']][_L['New']]
-- @usage textadept.menu.menubar[_L['File']][_L['New']][2] = function() .. end
local default_menubar = {
  {
    title = _L['File'],
    {_L['New'], buffer.new},
    {_L['Open'], io.open_file},
    {_L['Open Recent...'], io.open_recent_file},
    {_L['Reload'], buffer.reload},
    {_L['Save'], buffer.save},
    {_L['Save As'], buffer.save_as},
    {_L['Save All'], io.save_all_files},
    SEPARATOR,
    {_L['Close'], buffer.close},
    {_L['Close All'], io.close_all_buffers},
    SEPARATOR,
    {_L['Load Session...'], textadept.session.load},
    {_L['Save Session...'], textadept.session.save},
    SEPARATOR,
    {_L['Quit'], quit}
  },
  {
    title = _L['Edit'],
    {_L['Undo'], buffer.undo},
    {_L['Redo'], buffer.redo},
    SEPARATOR,
    {_L['Cut'], buffer.cut},
    {_L['Copy'], buffer.copy},
    {_L['Paste'], buffer.paste},
    {_L['Paste Reindent'], textadept.editing.paste_reindent},
    {_L['Duplicate Line/Selection'], buffer.selection_duplicate},
    {_L['Delete'], buffer.clear},
    {_L['Delete Word'], function()
      textadept.editing.select_word()
      buffer:delete_back()
    end},
    {_L['Select All'], buffer.select_all},
    SEPARATOR,
    {_L['Match Brace'], function()
      local match_pos = buffer:brace_match(buffer.current_pos, 0)
      if match_pos ~= -1 then buffer:goto_pos(match_pos) end
    end},
    {_L['Complete Word'], function() textadept.editing.autocomplete('word') end},
    {_L['Toggle Block Comment'], textadept.editing.toggle_comment},
    {_L['Transpose Characters'], textadept.editing.transpose_chars},
    {_L['Join Lines'], textadept.editing.join_lines},
    {_L['Filter Through'], function()
      ui.command_entry.run(textadept.editing.filter_through, 'bash')
    end},
    {
      title = _L['Select'],
      {_L['Select between Matching Delimiters'], sel_enc},
      {_L['Select between XML Tags'], function() sel_enc('>', '<') end},
      {_L['Select in XML Tag'], function() sel_enc('<', '>') end},
      {_L['Select Word'], textadept.editing.select_word},
      {_L['Select Line'], textadept.editing.select_line},
      {_L['Select Paragraph'], textadept.editing.select_paragraph}
    },
    {
      title = _L['Selection'],
      {_L['Upper Case Selection'], buffer.upper_case},
      {_L['Lower Case Selection'], buffer.lower_case},
      SEPARATOR,
      {_L['Enclose as XML Tags'], function()
        buffer:begin_undo_action()
        enc('<', '>')
        for i = 1, buffer.selections do
          local s, e = buffer.selection_n_start[i], buffer.selection_n_end[i]
          while buffer.char_at[s - 1] ~= 60 do s = s - 1 end -- '<'
          buffer:set_target_range(e, e)
          buffer:replace_target('</' .. buffer:text_range(s, e))
          buffer.selection_n_start[i], buffer.selection_n_end[i] = e, e
        end
        buffer:end_undo_action()
      end},
      {_L['Enclose as Single XML Tag'], function() enc('<', ' />') end},
      {_L['Enclose in Single Quotes'], function() enc("'", "'") end},
      {_L['Enclose in Double Quotes'], function() enc('"', '"') end},
      {_L['Enclose in Parentheses'], function() enc('(', ')') end},
      {_L['Enclose in Brackets'], function() enc('[', ']') end},
      {_L['Enclose in Braces'], function() enc('{', '}') end},
      SEPARATOR,
      {_L['Move Selected Lines Up'], buffer.move_selected_lines_up},
      {_L['Move Selected Lines Down'], buffer.move_selected_lines_down}
    },
    {
      title = _L['History'],
      {_L['Navigate Backward'], textadept.history.back},
      {_L['Navigate Forward'], textadept.history.forward},
      {_L['Record Location'], textadept.history.record},
      SEPARATOR,
      {_L['Clear History'], textadept.history.clear}
    },
    SEPARATOR,
    {_L['Preferences'], function() io.open_file(_USERHOME .. '/init.lua') end}
  },
  {
    title = _L['Search'],
    {_L['Find'], ui.find.focus},
    {_L['Find Next'], ui.find.find_next},
    {_L['Find Previous'], ui.find.find_prev},
    {_L['Replace'], ui.find.replace},
    {_L['Replace All'], ui.find.replace_all},
    {_L['Find Incremental'], function() ui.find.focus{incremental = true} end},
    SEPARATOR,
    {_L['Find in Files'], function() ui.find.focus{in_files = true} end},
    {_L['Goto Next File Found'], function() ui.find.goto_file_found(true) end},
    {_L['Goto Previous File Found'], function() ui.find.goto_file_found(false) end},
    SEPARATOR,
    {_L['Jump to'], textadept.editing.goto_line}
  },
  {
    title = _L['Tools'],
    {_L['Command Entry'], ui.command_entry.run},
    {_L['Select Command'], function() M.select_command() end},
    SEPARATOR,
    {_L['Run'], textadept.run.run},
    {_L['Compile'], textadept.run.compile},
    {_L['Set Arguments...'], textadept.run.set_arguments},
    {_L['Build'], textadept.run.build},
    {_L['Run tests'], textadept.run.test},
    {_L['Stop'], textadept.run.stop},
    {_L['Next Error'], function() textadept.run.goto_error(true) end},
    {_L['Previous Error'], function() textadept.run.goto_error(false) end},
    SEPARATOR,
    {
      title = _L['Bookmarks'],
      {_L['Toggle Bookmark'], textadept.bookmarks.toggle},
      {_L['Clear Bookmarks'], textadept.bookmarks.clear},
      {_L['Next Bookmark'], function() textadept.bookmarks.goto_mark(true) end},
      {_L['Previous Bookmark'], function() textadept.bookmarks.goto_mark(false) end},
      {_L['Goto Bookmark...'], textadept.bookmarks.goto_mark},
    },
    {
      title = _L['Macros'],
      {_L['Start/Stop Recording'], textadept.macros.record},
      {_L['Play'], textadept.macros.play},
      SEPARATOR,
      {_L['Save...'], textadept.macros.save},
      {_L['Load...'], textadept.macros.load},
    },
    {
      title = _L['Quick Open'],
      {_L['Quickly Open User Home'], function() io.quick_open(_USERHOME) end},
      {_L['Quickly Open Textadept Home'], function() io.quick_open(_HOME) end},
      {_L['Quickly Open Current Directory'], function()
        if buffer.filename then io.quick_open(buffer.filename:match('^(.+)[/\\]')) end
      end},
      {_L['Quickly Open Current Project'], io.quick_open},
    },
    {
      title = _L['Snippets'],
      {_L['Insert Snippet...'], textadept.snippets.select},
      {_L['Expand Snippet/Next Placeholder'], textadept.snippets.insert},
      {_L['Previous Snippet Placeholder'], textadept.snippets.previous},
      {_L['Cancel Snippet'], textadept.snippets.cancel_current},
      SEPARATOR,
      {_L['Complete Trigger Word'], function() textadept.editing.autocomplete('snippets') end}
    },
    SEPARATOR,
    {_L['Complete Symbol'], function() textadept.editing.autocomplete(buffer:get_lexer(true)) end},
    {_L['Show Documentation'], textadept.editing.show_documentation},
    {_L['Show Style'], function()
      local char = buffer:text_range(buffer.current_pos, buffer:position_after(buffer.current_pos))
      if char == '' then return end -- end of buffer
      local bytes = string.rep(' 0x%X', #char):format(char:byte(1, #char))
      local style = buffer.style_at[buffer.current_pos]
      local text = string.format("'%s' (U+%04X:%s)\n%s %s\n%s %s (%d)", char, utf8.codepoint(char),
        bytes, _L['Lexer'], buffer:get_lexer(true), _L['Style'], buffer:name_of_style(style), style)
      view:call_tip_show(buffer.current_pos, text)
    end}
  },
  {
    title = _L['Buffer'],
    {_L['Next Buffer'], function() view:goto_buffer(1) end},
    {_L['Previous Buffer'], function() view:goto_buffer(-1) end},
    {_L['Switch to Buffer...'], ui.switch_buffer},
    SEPARATOR,
    {
      title = _L['Indentation'],
      {_L['Tab width: 2'], function() set_indentation(2) end},
      {_L['Tab width: 3'], function() set_indentation(3) end},
      {_L['Tab width: 4'], function() set_indentation(4) end},
      {_L['Tab width: 8'], function() set_indentation(8) end},
      SEPARATOR,
      {_L['Toggle Use Tabs'], function()
        buffer.use_tabs = not buffer.use_tabs
        events.emit(events.UPDATE_UI, 1) -- for updating statusbar
      end},
      {_L['Convert Indentation'], textadept.editing.convert_indentation}
    },
    {
      title = _L['EOL Mode'],
      {_L['CRLF'], function() set_eol_mode(buffer.EOL_CRLF) end},
      {_L['LF'], function() set_eol_mode(buffer.EOL_LF) end}
    },
    {
      title = _L['Encoding'],
      {_L['UTF-8 Encoding'], function() set_encoding('UTF-8') end},
      {_L['ASCII Encoding'], function() set_encoding('ASCII') end},
      {_L['CP-1252 Encoding'], function() set_encoding('CP1252') end},
      {_L['UTF-16 Encoding'], function() set_encoding('UTF-16LE') end}
    },
    SEPARATOR,
    {_L['Toggle Wrap Mode'], function()
      local first_visible_line = view.first_visible_line
      local display_line = view:visible_from_doc_line(first_visible_line)
      view.wrap_mode = view.wrap_mode == 0 and view.WRAP_WHITESPACE or 0
      view:line_scroll(0, first_visible_line - display_line)
    end},
    {_L['Toggle View Whitespace'], function()
      view.view_ws = view.view_ws == 0 and view.WS_VISIBLEALWAYS or 0
    end},
    SEPARATOR,
    {_L['Select Lexer...'], textadept.file_types.select_lexer}
  },
  {
    title = _L['View'],
    {_L['Next View'], function() ui.goto_view(1) end},
    {_L['Previous View'], function() ui.goto_view(-1) end},
    SEPARATOR,
    {_L['Split View Horizontal'], function() view:split() end},
    {_L['Split View Vertical'], function() view:split(true) end},
    {_L['Unsplit View'], function() view:unsplit() end},
    {_L['Unsplit All Views'], function() while view:unsplit() do end end},
    {_L['Grow View'], function()
      if view.size then view.size = view.size + view:text_height(1) end
    end},
    {_L['Shrink View'], function()
      if view.size then view.size = view.size - view:text_height(1) end
    end},
    SEPARATOR,
    {_L['Toggle Current Fold'], function()
      local line = buffer:line_from_position(buffer.current_pos)
      view:toggle_fold(math.max(buffer.fold_parent[line], line))
    end},
    SEPARATOR,
    {_L['Toggle Show Indent Guides'], function()
      view.indentation_guides = view.indentation_guides == 0 and view.IV_LOOKBOTH or 0
    end},
    {_L['Toggle Virtual Space'], function()
      buffer.virtual_space_options = buffer.virtual_space_options == 0 and
        buffer.VS_USERACCESSIBLE or 0
    end},
    SEPARATOR,
    {_L['Zoom In'], view.zoom_in},
    {_L['Zoom Out'], view.zoom_out},
    {_L['Reset Zoom'], function() view.zoom = 0 end}
  },
  {
    title = _L['Help'],
    {_L['Show Manual'], function() open_page(_HOME .. '/docs/manual.html') end},
    {_L['Show LuaDoc'], function() open_page(_HOME .. '/docs/api.html') end},
    SEPARATOR,
    {_L['About'], function()
      ui.dialogs.msgbox{
        title = 'Textadept', text = _RELEASE, informative_text = _COPYRIGHT,
        icon_file = _HOME .. '/core/images/ta_64x64.png'
      }
    end}
  }
}

---
-- The default right-click context menu.
-- Submenus, and menu items can be retrieved by name in addition to table index number.
-- @class table
-- @name context_menu
-- @usage textadept.menu.context_menu[#textadept.menu.context_menu + 1] = {...}
local default_context_menu = {
  {_L['Undo'], buffer.undo},
  {_L['Redo'], buffer.redo},
  SEPARATOR,
  {_L['Cut'], buffer.cut},
  {_L['Copy'], buffer.copy},
  {_L['Paste'], buffer.paste},
  {_L['Delete'], buffer.clear},
  SEPARATOR,
  {_L['Select All'], buffer.select_all}
}

---
-- The default tabbar context menu.
-- Submenus, and menu items can be retrieved by name in addition to table index number.
-- @class table
-- @name tab_context_menu
local default_tab_context_menu = {
  {_L['Close'], buffer.close},
  SEPARATOR,
  {_L['Save'], buffer.save},
  {_L['Save As'], buffer.save_as},
  SEPARATOR,
  {_L['Reload'], buffer.reload},
}
-- LuaFormatter on

-- Table of proxy tables for menus.
local proxies = {}

local key_shortcuts, menu_items, contextmenu_items

-- Returns the GDK integer keycode and modifier mask for a key sequence.
-- This is used for creating menu accelerators.
-- @param key_seq The string key sequence.
-- @return keycode and modifier mask
local function get_gdk_key(key_seq)
  if not key_seq then return nil end
  local mods, key = key_seq:match('^(.*%+)(.+)$')
  if not mods and not key then mods, key = '', key_seq end
  local modifiers = ((mods:find('shift%+') or key:lower() ~= key) and 1 or 0) +
    (mods:find('ctrl%+') and 4 or 0) + (mods:find('alt%+') and 8 or 0) +
    (mods:find('cmd%+') and 0x10000000 or 0)
  local code = string.byte(key)
  if #key > 1 or code < 32 then
    for i, s in pairs(keys.KEYSYMS) do
      if s == key and i > 0xFE20 then
        code = i
        break
      end
    end
  end
  return code, modifiers
end

-- Creates a menu suitable for `ui.menu()` from the menu table format.
-- Also assigns key bindings.
-- @param menu The menu to create a GTK menu from.
-- @param contextmenu Flag indicating whether or not the menu is a context menu. If so, menu_id
--   offset is 1000. The default value is `false`.
-- @return GTK menu that can be passed to `ui.menu()`.
-- @see ui.menu
local function read_menu_table(menu, contextmenu)
  local gtkmenu = {title = menu.title}
  for _, item in ipairs(menu) do
    if item.title then
      gtkmenu[#gtkmenu + 1] = read_menu_table(item, contextmenu)
    else -- item = {label, function}
      local menu_id = not contextmenu and #menu_items + 1 or #contextmenu_items + 1000 + 1
      local key, mods = get_gdk_key(key_shortcuts[tostring(item[2])])
      gtkmenu[#gtkmenu + 1] = {item[1], menu_id, key, mods}
      if item[2] then
        local items = not contextmenu and menu_items or contextmenu_items
        items[menu_id < 1000 and menu_id or menu_id - 1000] = item
      end
    end
  end
  return gtkmenu
end

-- Returns a proxy table for menu table *menu* such that when a menu item is changed or added,
-- *update* is called to update the menu in the UI.
-- @param menu The menu or table of menus to create a proxy for.
-- @param update The function to call to update the menu in the UI when a menu item is changed
--   or added.
-- @param menubar Used internally to keep track of the top-level menu for calling *update* with.
local function proxy_menu(menu, update, menubar)
  return setmetatable({}, {
    __index = function(_, k)
      local v
      if type(k) == 'number' or k == 'title' then
        v = menu[k]
      elseif type(k) == 'string' then
        for _, item in ipairs(menu) do
          if item.title == k or item[1] == k then
            v = item
            break
          end
        end
      end
      return type(v) == 'table' and proxy_menu(v, update, menubar or menu) or v
    end, -- LuaFormatter
    __newindex = function(_, k, v)
      menu[k] = getmetatable(v) and getmetatable(v).menu or v
      -- After adding or removing menus or menu items, update the menubar or context menu. When
      -- updating a menu item's function, do nothing extra.
      if type(v) ~= 'function' then update(menubar or menu) end
    end, __len = function() return #menu end, menu = menu -- store existing menu for copying (e.g. m[#m + 1] = m[#m])
  })
end

-- Sets `ui.menubar` from menu table *menubar*.
-- Each menu is an ordered list of menu items and has a `title` key for the title text. Menu
-- items are tables containing menu text and either a function to call or a table containing a
-- function with its parameters to call when an item is clicked. Menu items may also be sub-menus,
-- ordered lists of menu items with an additional `title` key for the sub-menu's title text.
-- @param menubar The table of menu tables to create the menubar from. If `nil`, clears the
--   menubar from view, but keeps it intact in order for `M.select_command()` to function properly.
-- @see ui.menubar
-- @see ui.menu
local function set_menubar(menubar)
  if not menubar then
    ui.menubar = {}
    return
  end
  key_shortcuts, menu_items = {}, {} -- reset
  for key, f in pairs(keys) do key_shortcuts[tostring(f)] = key end
  local _menubar = {}
  for _, menu in ipairs(menubar) do _menubar[#_menubar + 1] = ui.menu(read_menu_table(menu)) end
  ui.menubar = _menubar
  proxies.menubar = proxy_menu(menubar, set_menubar)
end
events.connect(events.INITIALIZED, function() set_menubar(default_menubar) end)
-- Define menu proxy for use by keys.lua and user scripts.
-- Do not use an update function because this is expensive at startup, and `events.INITIALIZED`
-- will create the first visible menubar and proper proxy.
proxies.menubar = proxy_menu(default_menubar, function() end)

-- Sets `ui.context_menu` and `ui.tab_context_menu` from menu item lists *buffer_menu* and
-- *tab_menu*, respectively.
-- Menu items are tables containing menu text and either a function to call or a table containing a
-- function with its parameters to call when an item is clicked. Menu items may also be sub-menus,
-- ordered lists of menu items with an additional `title` key for the sub-menu's title text.
-- @param buffer_menu Optional menu table to create the buffer context menu from. If `nil`,
--   uses the default context menu.
-- @param tab_menu Optional menu table to create the tabbar context menu from. If `nil`, uses
--   the default tab context menu.
-- @see ui.context_menu
-- @see ui.tab_context_menu
-- @see ui.menu
local function set_contextmenus(buffer_menu, tab_menu)
  contextmenu_items = {} -- reset
  local menus = {
    context_menu = buffer_menu or default_context_menu,
    tab_context_menu = tab_menu or default_tab_context_menu
  }
  for name, menu in pairs(menus) do
    ui[name] = ui.menu(read_menu_table(menu, true))
    proxies[name] = proxy_menu(menu, function()
      set_contextmenus(menus.context_menu, menus.tab_context_menu)
    end)
  end
end
events.connect(events.INITIALIZED, set_contextmenus)
-- Define menu proxies for use by user scripts.
-- Do not use an update function because this is expensive at startup, and `events.INITIALIZED`
-- will create these visible menus and their proper proxies.
proxies.context_menu = proxy_menu(default_context_menu, function() end)
proxies.tab_context_menu = proxy_menu(default_tab_context_menu, function() end)

-- Performs the appropriate action when clicking a menu item.
events.connect(events.MENU_CLICKED, function(menu_id)
  local items = menu_id < 1000 and menu_items or contextmenu_items
  local f = items[menu_id < 1000 and menu_id or menu_id - 1000][2]
  assert_type(f, 'function', 'command')()
end)

---
-- Prompts the user to select a menu command to run.
-- @name select_command
function M.select_command()
  local items = {}
  -- Builds the item tables for the filtered list dialog.
  -- @param menu The menu to read from.
  local function build_command_tables(menu)
    for _, item in ipairs(menu) do
      if item.title then
        build_command_tables(item)
      elseif item[1] ~= '' then -- item = {label, function}
        local label = menu.title and string.format('%s: %s', menu.title, item[1]) or item[1]
        items[#items + 1] = label:gsub('_([^_])', '%1')
        items[#items + 1] = key_shortcuts[tostring(item[2])] or ''
      end
    end
  end
  build_command_tables(getmetatable(M.menubar).menu)
  local button, i = ui.dialogs.filteredlist{
    title = _L['Run Command'], columns = {_L['Command'], _L['Key Binding']}, items = items
  }
  if button == 1 and i then events.emit(events.MENU_CLICKED, i) end
end

return setmetatable(M, {
  __index = function(_, k) return proxies[k] or rawget(M, k) end, __newindex = function(_, k, v)
    if k == 'menubar' then
      set_menubar(v)
    elseif k == 'context_menu' then
      set_contextmenus(v) -- TODO: this can reset tab_context_menu
    elseif k == 'tab_context_menu' then
      set_contextmenus(nil, v) -- TODO: this can reset context_menu
    else
      rawset(M, k, v)
    end
  end
})
