local Line = require("nui.line")
local Popup = require("nui.popup")
local Text = require("nui.text")
local Tree = require("nui.tree")
local _ = require("nui.utils")._
local defaults = require("nui.utils").defaults
local is_type = require("nui.utils").is_type

local function calculate_initial_max_width(items)
  local max_width = 0

  for _, item in ipairs(items) do
    local width = 0
    if is_type("string", item.text) then
      width = vim.api.nvim_strwidth(item.text)
    elseif is_type("table", item.text) and item.text.width then
      width = item.text:width()
    end

    if max_width < width then
      max_width = width
    end
  end

  return max_width
end

local default_keymap = {
  close = { "<Esc>", "<C-c>" },
  focus_next = { "j", "<Down>", "<Tab>" },
  focus_prev = { "k", "<Up>", "<S-Tab>" },
  submit = { "<CR>" },
}

---@param keymap table<_nui_menu_keymap_action, string|string[]>
---@return table<_nui_menu_keymap_action, string[]>
local function parse_keymap(keymap)
  local result = defaults(keymap, {})

  for name, default_keys in pairs(default_keymap) do
    if is_type("nil", result[name]) then
      result[name] = default_keys
    elseif is_type("string", result[name]) then
      result[name] = { result[name] }
    end
  end

  return result
end

---@type nui_menu_should_skip_item
local function default_should_skip_item(node)
  return node._type == "separator"
end

---@param menu NuiMenu
---@return nui_menu_prepare_item
local function make_default_prepare_node(menu)
  local border = menu.border

  local fallback_sep = {
    char = Text(is_type("table", border._.char) and border._.char.top or " "),
    text_align = is_type("table", border._.text) and border._.text.top_align or "left",
  }

  -- luacov: disable
  if menu._.sep then
    -- @deprecated

    if menu._.sep.char then
      fallback_sep.char = Text(menu._.sep.char)
    end

    if menu._.sep.text_align then
      fallback_sep.text_align = menu._.sep.text_align
    end
  end
  -- luacov: enable

  local max_width = menu._.size.width

  ---@type nui_menu_prepare_item
  local function default_prepare_node(node)
    ---@type NuiText|NuiLine
    local content = is_type("string", node.text) and Text(node.text) or node.text

    if node._type == "item" then
      if content:width() > max_width then
        if is_type("function", content.set) then
          ---@cast content NuiText
          _.truncate_nui_text(content, max_width)
        else
          ---@cast content NuiLine
          _.truncate_nui_line(content, max_width)
        end
      end

      local line = Line()

      line:append(content)

      return line
    end

    if node._type == "separator" then
      local sep_char = Text(defaults(node._char, fallback_sep.char))
      local sep_text_align = defaults(node._text_align, fallback_sep.text_align)

      local sep_max_width = max_width - sep_char:width() * 2

      if content:width() > sep_max_width then
        if content._texts then
          ---@cast content NuiLine
          _.truncate_nui_line(content, sep_max_width)
        else
          ---@cast content NuiText
          _.truncate_nui_text(content, sep_max_width)
        end
      end

      local left_gap_width, right_gap_width =
        _.calculate_gap_width(defaults(sep_text_align, "center"), sep_max_width, content:width())

      local line = Line()

      line:append(Text(sep_char))

      if left_gap_width > 0 then
        line:append(Text(sep_char):set(string.rep(sep_char:content(), left_gap_width)))
      end

      line:append(content)

      if right_gap_width > 0 then
        line:append(Text(sep_char):set(string.rep(sep_char:content(), right_gap_width)))
      end

      line:append(Text(sep_char))

      return line
    end
  end

  return default_prepare_node
end

---@param menu NuiMenu
---@param direction "'next'" | "'prev'"
---@param current_linenr nil | number
local function focus_item(menu, direction, current_linenr)
  local curr_linenr = current_linenr or vim.api.nvim_win_get_cursor(menu.winid)[1]

  local next_linenr = nil

  if direction == "next" then
    if curr_linenr == #menu.tree:get_nodes() then
      next_linenr = 1
    else
      next_linenr = curr_linenr + 1
    end
  elseif direction == "prev" then
    if curr_linenr == 1 then
      next_linenr = #menu.tree:get_nodes()
    else
      next_linenr = curr_linenr - 1
    end
  end

  local next_node = menu.tree:get_node(next_linenr)

  if menu._.should_skip_item(next_node) then
    return focus_item(menu, direction, next_linenr)
  end

  if next_linenr then
    vim.api.nvim_win_set_cursor(menu.winid, { next_linenr, 0 })
    menu._.on_change(next_node)
  end
end

---@alias nui_menu_prepare_item nui_tree_prepare_node
---@alias nui_menu_should_skip_item fun(node: NuiTree.Node): boolean

---@alias _nui_menu_keymap_action 'close'|'focus_next'|'focus_prev'|'submit'

---@class nui_menu_internal: nui_popup_internal
---@field items NuiTree.Node[]
---@field keymap table<_nui_menu_keymap_action, string[]>
---@field sep { char?: string|NuiText, text_align?: nui_t_text_align } # deprecated
---@field prepare_item nui_menu_prepare_item
---@field should_skip_item nui_menu_should_skip_item
---@field on_change fun(item: NuiTree.Node): nil

---@class nui_menu_options
---@field lines NuiTree.Node[]
---@field prepare_item? nui_tree_prepare_node
---@field should_skip_item? nui_menu_should_skip_item
---@field max_height? integer
---@field min_height? integer
---@field max_width? integer
---@field min_width? integer
---@field keymap? table<_nui_menu_keymap_action, string|string[]>
---@field on_change? fun(item: NuiTree.Node, menu: NuiMenu): nil
---@field on_close? fun(): nil
---@field on_submit? fun(item: NuiTree.Node): nil

---@class NuiMenu: NuiPopup
---@field private _ nui_menu_internal
local Menu = Popup:extend("NuiMenu")

---@param content? string|NuiText|NuiLine
---@param options? { char?: string|NuiText, text_align?: nui_t_text_align }
---@return NuiTree.Node
function Menu.separator(content, options)
  options = options or {}
  return Tree.Node({
    _id = tostring(math.random()),
    _type = "separator",
    _char = options.char,
    _text_align = options.text_align,
    text = defaults(content, ""),
  })
end

---@param content string|NuiText|NuiLine
---@param data? table
---@return NuiTree.Node
function Menu.item(content, data)
  if not data then
    ---@diagnostic disable-next-line: undefined-field
    if is_type("table", content) and content.text then
      ---@cast content table
      data = content
    else
      data = { text = content }
    end
  else
    data.text = content
  end

  data._type = "item"
  data._id = data.id or tostring(math.random())

  return Tree.Node(data)
end

---@param popup_options nui_popup_options
---@param options nui_menu_options
function Menu:init(popup_options, options)
  local max_width = calculate_initial_max_width(options.lines)

  local width = math.max(math.min(max_width, defaults(options.max_width, 256)), defaults(options.min_width, 4))
  local height = math.max(math.min(#options.lines, defaults(options.max_height, 256)), defaults(options.min_height, 1))

  ---@type nui_popup_options
  popup_options = vim.tbl_deep_extend("force", {
    enter = true,
    size = {
      width = width,
      height = height,
    },
    win_options = {
      cursorline = true,
      scrolloff = 1,
      sidescrolloff = 0,
    },
    zindex = 60,
  }, popup_options)

  Menu.super.init(self, popup_options)

  self._.items = options.lines
  self._.keymap = parse_keymap(options.keymap)

  ---@param node NuiTree.Node
  self._.on_change = function(node)
    if options.on_change then
      options.on_change(node, self)
    end
  end

  ---@deprecated
  self._.sep = options.separator

  self._.should_skip_item = defaults(options.should_skip_item, default_should_skip_item)
  self._.prepare_item = defaults(options.prepare_item, self._.prepare_item)

  self.menu_props = {}

  local props = self.menu_props

  props.on_submit = function()
    local item = self.tree:get_node()

    self:unmount()

    if options.on_submit then
      options.on_submit(item)
    end
  end

  props.on_close = function()
    self:unmount()

    if options.on_close then
      options.on_close()
    end
  end

  props.on_focus_next = function()
    focus_item(self, "next")
  end

  props.on_focus_prev = function()
    focus_item(self, "prev")
  end
end

---@param config? nui_layout_options
function Menu:update_layout(config)
  Menu.super.update_layout(self, config)

  self._.prepare_item = defaults(self._.prepare_item, make_default_prepare_node(self))
end

function Menu:mount()
  Menu.super.mount(self)

  local props = self.menu_props

  for _, key in pairs(self._.keymap.focus_next) do
    self:map("n", key, props.on_focus_next, { noremap = true, nowait = true })
  end

  for _, key in pairs(self._.keymap.focus_prev) do
    self:map("n", key, props.on_focus_prev, { noremap = true, nowait = true })
  end

  for _, key in pairs(self._.keymap.close) do
    self:map("n", key, props.on_close, { noremap = true, nowait = true })
  end

  for _, key in pairs(self._.keymap.submit) do
    self:map("n", key, props.on_submit, { noremap = true, nowait = true })
  end

  self.tree = Tree({
    winid = self.winid,
    ns_id = self.ns_id,
    nodes = self._.items,
    get_node_id = function(node)
      return node._id
    end,
    prepare_node = self._.prepare_item,
  })

  ---@deprecated
  self._tree = self.tree

  self.tree:render()

  -- focus first item
  for linenr = 1, #self.tree:get_nodes() do
    local node, target_linenr = self.tree:get_node(linenr)
    if not self._.should_skip_item(node) then
      vim.api.nvim_win_set_cursor(self.winid, { target_linenr, 0 })
      self._.on_change(node)
      break
    end
  end
end

---@alias NuiMenu.constructor fun(popup_options: nui_popup_options, options: nui_menu_options): NuiMenu
---@type NuiMenu|NuiMenu.constructor
local NuiMenu = Menu

return NuiMenu
