-- Copyright (c) 2020-2021 hoob3rt
-- MIT license, see LICENSE for more details.
local M = {}
local lualine_require = require('lualine_require')
local require = lualine_require.require
local modules = lualine_require.lazy_require({
  utils = 'lualine.utils.utils',
  color_utils = 'lualine.utils.color_utils',
})

local section_highlight_map = { x = 'c', y = 'b', z = 'a' }
local active_theme = nil
local create_cterm_colors = false

-- table to store the highlight names created by lualine
local loaded_highlights = {}

-- table to map mode to highlight suffixes
local mode_to_highlight = {
  ['VISUAL'] = '_visual',
  ['V-BLOCK'] = '_visual',
  ['V-LINE'] = '_visual',
  ['SELECT'] = '_visual',
  ['S-LINE'] = '_visual',
  ['S-BLOCK'] = '_visual',
  ['REPLACE'] = '_replace',
  ['V-REPLACE'] = '_replace',
  ['INSERT'] = '_insert',
  ['COMMAND'] = '_command',
  ['EX'] = '_command',
  ['MORE'] = '_command',
  ['CONFIRM'] = '_command',
  ['TERMINAL'] = '_terminal',
}

--- determine if an highlight exist and isn't cleared
---@param highlight_name string
---@return boolean whether hl_group was defined with highlight_name
function M.highlight_exists(highlight_name)
  return loaded_highlights[highlight_name] or false
end

--- clears loaded_highlights table and highlights
local function clear_highlights()
  for highlight_name, _ in pairs(loaded_highlights) do
    vim.cmd('highlight clear ' .. highlight_name)
    loaded_highlights[highlight_name] = nil
  end
end

---converts cterm, color_name type colors to #rrggbb format
---@param color string|number
---@return string
local function sanitize_color(color)
  if type(color) == 'string' then
    if color:sub(1, 1) == '#' then
      return color
    end -- RGB value
    return modules.color_utils.color_name2rgb(color)
  elseif type(color) == 'number' then
    if color > 255 then
      error("What's this it can't be higher then 255 and you've given " .. color)
    end
    return modules.color_utils.cterm2rgb(color)
  end
end

--- Define a hl_group
---@param name string
---@param foreground string|number: color
---@param background string|number: color
---@param gui table cterm/gui options like bold/italic ect
---@param link string hl_group name to link new hl to
function M.highlight(name, foreground, background, gui, link)
  local command = { 'highlight!' }
  if link and #link > 0 then
    vim.list_extend(command, { 'link', name, link })
  else
    foreground = sanitize_color(foreground)
    background = sanitize_color(background)
    table.insert(command, name)
    if foreground and foreground ~= 'None' then
      table.insert(command, 'guifg=' .. foreground)
      if create_cterm_colors then
        table.insert(command, 'ctermfg=' .. modules.color_utils.rgb2cterm(foreground))
      end
    end
    if background and background ~= 'None' then
      table.insert(command, 'guibg=' .. background)
      if create_cterm_colors then
        table.insert(command, 'ctermbg=' .. modules.color_utils.rgb2cterm(background))
      end
    end
    if gui then
      table.insert(command, 'cterm=' .. gui)
      table.insert(command, 'gui=' .. gui)
    end
  end
  vim.cmd(table.concat(command, ' '))
  loaded_highlights[name] = true
end

---define hl_groups for a theme
---@param theme table
function M.create_highlight_groups(theme)
  clear_highlights()
  active_theme = theme
  create_cterm_colors = not vim.go.termguicolors
  for mode, sections in pairs(theme) do
    for section, color in pairs(sections) do
      local highlight_group_name = { 'lualine', section, mode }
      if type(color) == 'string' then -- link to a highlight group
        M.highlight(table.concat(highlight_group_name, '_'), nil, nil, nil, color)
      else -- Define a new highlight
        M.highlight(table.concat(highlight_group_name, '_'), color.fg, color.bg, color.gui, nil)
      end
    end
  end
end

---@description: adds '_mode' at end of highlight_group
---@param highlight_group string name of highlight group
---@return string highlight group name with mode
function M.append_mode(highlight_group, is_focused)
  if is_focused == nil then
    is_focused = modules.utils.is_focused()
  end
  if is_focused == false then
    return highlight_group .. '_inactive'
  end
  local mode = require('lualine.utils.mode').get_mode()
  return highlight_group .. (mode_to_highlight[mode] or '_normal')
end

-- Helper function for create component highlight
---Handles fall back of colors when creating highlight group
---@param color table color passed for creating component highlight
---@param options_color table color set by color option for component
---       this is first fall back
---@param default_color table colors et in theme this is 2nd fall back
---@param kind string fg/bg
local function get_default_component_color(color, options_color, default_color, kind)
  if color[kind] then
    return color[kind]
  end
  if options_color then
    if type(options_color) == 'table' and options_color[kind] then
      return options_color[kind]
    elseif type(options_color) == 'string' then
      return modules.utils.extract_highlight_colors(options_color, kind)
    end
  end
  if type(default_color) == 'table' then
    return default_color[kind]
  elseif type(default_color) == 'string' then
    return modules.utils.extract_highlight_colors(default_color, kind)
  end
end

---Create highlight group with fg bg and gui from theme
---@param color table has to be { fg = "#rrggbb", bg="#rrggbb" gui = "effect" }
---       all the color elements are optional if fg or bg is not given options
---       must be provided So fg and bg can default the themes colors
---@param highlight_tag string is unique tag for highlight group
---returns the name of highlight group
---@param options table is parameter of component.init() function
---@return string unique name that can be used by component_format_highlight
---  to retrieve highlight group
function M.create_component_highlight_group(color, highlight_tag, options)
  local tag_id = 0
  while
    M.highlight_exists(table.concat({ 'lualine', highlight_tag, 'no_mode' }, '_'))
    or (
      options.self.section
      and M.highlight_exists(table.concat({ options.self.section, highlight_tag, 'normal' }, '_'))
    )
  do
    highlight_tag = highlight_tag .. '_' .. tostring(tag_id)
    tag_id = tag_id + 1
  end
  if type(color) == 'string' then
    local highlight_group_name = table.concat({ 'lualine', highlight_tag, 'no_mode' }, '_')
    M.highlight(highlight_group_name, nil, nil, nil, color) -- l8nk to group
    return highlight_group_name
  end
  if color.bg and color.fg then
    -- When bg and fg are both present we donn't need to set highlighs for
    -- each mode as they will surely look the same. So we can work without options
    local highlight_group_name = table.concat({ 'lualine', highlight_tag, 'no_mode' }, '_')
    M.highlight(highlight_group_name, color.fg, color.bg, color.gui, nil)
    return highlight_group_name
  end

  local modes = {
    'normal',
    'insert',
    'visual',
    'replace',
    'command',
    'terminal',
    'inactive',
  }
  local normal_hl
  -- convert lualine_a -> a before setting section
  local section = options.self.section:match('lualine_(.*)')
  if section > 'c' and not active_theme.normal[section] then
    section = section_highlight_map[section]
  end
  for _, mode in ipairs(modes) do
    local highlight_group_name = { options.self.section, highlight_tag, mode }
    local default_color_table = active_theme[mode] and active_theme[mode][section] or active_theme.normal[section]
    local bg = get_default_component_color(color, options.color, default_color_table, 'bg')
    local fg = get_default_component_color(color, options.color, default_color_table, 'fg')
    -- Check if it's same as normal mode if it is no need to create aditional highlight
    if mode ~= 'normal' then
      if bg ~= normal_hl.bg or fg ~= normal_hl.fg then
        M.highlight(table.concat(highlight_group_name, '_'), fg, bg, color.gui, nil)
      end
    else
      normal_hl = { bg = bg, fg = fg }
      M.highlight(table.concat(highlight_group_name, '_'), fg, bg, color.gui, nil)
    end
  end
  return options.self.section .. '_' .. highlight_tag
end

---@description: retrieve highlight_groups for components
---@param highlight_name string highlight group name without mode
---  return value of create_component_highlight_group is to be passed in
---  this parameter to receive highlight that was created
---@return string formatted highlight group name
function M.component_format_highlight(highlight_name)
  local highlight_group = highlight_name
  if highlight_name:find('no_mode') == #highlight_name - #'no_mode' + 1 then
    return '%#' .. highlight_group .. '#'
  end
  highlight_group = M.append_mode(highlight_group)
  if M.highlight_exists(highlight_group) then
    return '%#' .. highlight_group .. '#'
  else
    return '%#' .. highlight_name .. '_normal#'
  end
end

---@description: retrieve highlight_groups for section
---@param highlight_group string highlight group name without mode
---  return value of create_component_highlight_group is to be passed in
---  this parameter to receive highlight that was created
---@param is_focused boolean
---@return string formatted highlight group name
function M.format_highlight(highlight_group, is_focused)
  local highlight_name = M.append_mode(highlight_group, is_focused)
  if highlight_group > 'lualine_c' and not M.highlight_exists(highlight_name) then
    highlight_group = 'lualine_' .. section_highlight_map[highlight_group:match('lualine_(.)')]
    highlight_name = M.append_mode(highlight_group, is_focused)
  end
  if M.highlight_exists(highlight_name) then
    return '%#' .. highlight_name .. '#'
  end
  return '%#' .. highlight_group .. '_normal#'
end

---@description : Provides transitional highlights for section separators.
---@param left_hl string this highlights bg is used for fg of transitional hl
---@param right_hl string this highlights bg is used for bg of transitional hl
---   '▶️' and '◀️' ' needs reverse colors so the caller should swap left and right
---@return string formatted highlight group name
function M.get_transitional_highlights(left_hl, right_hl)
  -- When both left and right highlights are same or one is absent
  -- nothing to transition to.
  if left_hl == nil or right_hl == nil or left_hl == right_hl then
    return nil
  end

  -- construct the name of highlight group
  local highlight_name = table.concat({ 'lualine_transitional', left_hl, 'to', right_hl }, '_')
  if not M.highlight_exists(highlight_name) then
    -- Create the highlight_group if needed
    -- Get colors from highlights
    local fg = modules.utils.extract_highlight_colors(left_hl, 'bg')
    local bg = modules.utils.extract_highlight_colors(right_hl, 'bg')
    if not fg or not bg then
      return nil
    end -- Color retrieval failed
    if bg == fg then
      return nil
    end -- Separator won't be visible anyway
    M.highlight(highlight_name, fg, bg, nil)
  end
  return '%#' .. highlight_name .. '#'
end

return M