---@module "data.func"
--- This module contains utility functions used throughout the configuration.
--          ╭─────────────────────────────────────────────────────────╮
--          │                    Utility Functions                    │
--          ╰─────────────────────────────────────────────────────────╯

-- Define a namespace for utility functions
local M = {}

---@function Check if the terminal is kitty
---@return boolean condition true if the terminal is kitty, false otherwise
function M.is_kitty()
  local term = os.getenv("TERM") or ""
  local kit = string.find(term, "kitty")
  return kit ~= nil
end

---@function Check if using kitty-scrollback
---@return boolean condition true if using kitty-scrollback, false otherwise
function M.is_kitty_scrollback()
  if vim.env.KITTY_SCROLLBACK_NVIM == "true" then
    return true
  end
  return false
end

---@function Check if the terminal is alacritty
---@return boolean condition true if the terminal is alacritty, false otherwise
function M.is_alacritty()
  local term = os.getenv("TERM") or ""
  local alc = string.find(term, "alacritty")
  return alc ~= nil
end

---@function Check if the terminal is tmux
---@return boolean condition true if the terminal is tmux, false otherwise
function M.is_tmux()
  local tterm = os.getenv("TERM")
  if tterm and string.find(tterm, "screen") then
    if os.getenv("TMUX") then
      return true
    end
  else
    if tterm and string.find(tterm, "tmux") then
      return true
    end
  end
  return false
end

---@function Check if the terminal is wezterm
---@return boolean condition true if the terminal is wezterm, false otherwise
function M.is_wezterm()
  local wterm = os.getenv("TERM_PROGRAM")
  if wterm and string.find(wterm, "WezTerm") then
    return true
  end
  return false
end

---@function Check if the terminal is neovide
---@return boolean condition true if the terminal is neovide, false otherwise
function M.is_neovide()
  local neovide = vim.g.neovide
  if neovide then
    return true
  end
  return false
end

---@function Check if the terminal is ssh
---@return boolean condition true if the terminal is ssh, false otherwise
function M.is_ssh()
  local ssh = os.getenv("SSH_TTY") or false
  if ssh then
    return true
  end
  return false
end

---@function Check if OS is Windows
---@return boolean condition true if the OS is Windows, false otherwise
function M.is_windows()
  local win = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1
  if win then
    return true
  end
  return false
end

---@function Check if OS is macOS
---@return boolean condition true if the OS is macOS, false otherwise
function M.is_mac()
  local mac = vim.fn.has("macunix")
  if mac == 1 then
    return true
  end
  return false
end

---@function Check if OS is Linux
---@return boolean condition true if the OS is Linux, false otherwise
function M.is_linux()
  local lin = vim.fn.has("unix")
  if lin == 1 then
    return true
  end
  return false
end

---@function Get the name of the OS.
---@param format string The format of the OS name.
---                    Possible values:
---                    - "verbose": Returns the full name of the OS.
---                    - "short": Returns a short name or abbreviation.
---                    - "code": Returns a code or identifier.
---                    - "platform": Returns either "windows", "osx", or "linux".
---@return string os The name of the OS.
function M.get_os(format)
  ---@diagnostic disable-next-line: undefined-field
  local uname = vim.loop and vim.loop.os_uname and vim.loop.os_uname() or {}
  local os_name = uname.sysname or "unknown"

  if os_name == "Windows_NT" then
    if format == "platform" then
      return "windows"
    elseif format == "short" then
      return "Win"
    elseif format == "code" then
      return "win"
    else
      return "Windows"
    end
  elseif os_name == "Darwin" then
    if format == "platform" then
      return "osx"
    elseif format == "short" then
      return "macOS"
    elseif format == "code" then
      return "osx"
    else
      return "macOS"
    end
  elseif os_name == "Linux" then
    -- Check if the system is running Android
    if vim.env.ANDROID_ROOT then
      if format == "platform" then
        return "linux"
      elseif format == "short" then
        return "Android"
      elseif format == "code" then
        return "android"
      else
        return "Android"
      end
    end

    -- Determine the Linux distribution
    local distro = "Linux"
    local release_file = "/etc/os-release"

    local fd = io.open(release_file, "r")
    if fd then
      for line in fd:lines() do
        if line:match("^ID=") then
          distro = line:gsub("ID=", ""):gsub('"', "")
          break
        end
      end
      fd:close()
    end

    if format == "platform" then
      return "linux"
    elseif format == "short" then
      return "Linux"
    elseif format == "code" then
      return distro
    else
      return "Linux (" .. distro .. ")"
    end
  else
    -- Falback tests
    if M.is_mac() then
      if format == "platform" then
        return "osx"
      elseif format == "short" then
        return "macOS"
      elseif format == "code" then
        return "osx"
      else
        return "macOS"
      end
    elseif M.is_linux() then
      if format == "platform" then
        return "linux"
      elseif format == "short" then
        return "Linux"
      elseif format == "code" then
        return "linux"
      else
        return "Linux"
      end
    elseif M.is_windows() then
      if format == "platform" then
        return "windows"
      elseif format == "short" then
        return "Win"
      elseif format == "code" then
        return "win"
      else
        return "Windows"
      end
    else
      -- Failed to determine OS
      return "Unknown OS"
    end
  end
end

---@function Function to send a notification
---@param message string The message to send
---@param level string|nil The level of the notification (default: "info")
---@param title string|nil The title of the notification
---@return boolean condition true if the notification was sent successfully, false otherwise
function M.notify(message, level, title)
  level = level or "info"
  if title then
    vim.notify(message, vim.log.levels[level:upper()], { title = title })
  else
    vim.notify(message, vim.log.levels[level:upper()])
  end
  return true
end

---@function Function to generate a y/n confirmation prompt
---@param prompt string The prompt text to display
---@param action function|string The action to execute if the user confirms the prompt, or a Vim command as a string
---@return boolean condition true if the user confirms the prompt, false otherwise
function M.ConfirmPrompt(prompt, action)
  -- Validate the action parameter
  local function perform_action()
    if type(action) == "function" then
      action() -- Call the function
    elseif type(action) == "string" then
      vim.cmd(action) -- Run the Vim command
    else
      M.notify(
        "Action must be a function or a string",
        "ERROR",
        "Configuration Error"
      )
    end
  end
  -- Create a new buffer
  local buf = vim.api.nvim_create_buf(false, true) -- Create a new empty buffer
  -- Set the prompt text in the buffer
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, { prompt, "y/n: " })
  -- Create a floating window to display the buffer
  local win_height = 2 -- Height of floating window
  local win_width = math.floor(vim.o.columns * 0.25) -- Width of floating window
  local row = math.floor((vim.o.lines - win_height) / 2) -- Position row
  local col = math.floor((vim.o.columns - win_width) / 2) -- Position column
  local win_border = "rounded"
  local style = "minimal"
  -- Create a floating window
  local win = vim.api.nvim_open_win(buf, true, {
    relative = "editor",
    width = win_width,
    height = win_height,
    col = col,
    row = row,
    style = style,
    border = win_border,
  })
  -- Move the cursor to the end of the buffer
  vim.api.nvim_win_set_cursor(win, { 2, 5 })
  -- Define the yes function
  local yes = function()
    vim.api.nvim_win_close(win, true)
    perform_action() -- Perform the action
    return true
  end
  -- Define the no function
  local no = function()
    vim.api.nvim_win_close(win, true)
    M.notify("Action Canceled", "INFO", "Info")
  end
  -- Define buffer-specific key mappings
  local keymaps = {
    y = function()
      yes()
    end,
    n = function()
      no()
    end,
    q = function()
      no()
    end,
    ["<Esc>"] = function()
      no()
    end,
  }
  -- Set the key mappings
  for key, callback in pairs(keymaps) do
    vim.api.nvim_buf_set_keymap(buf, "n", key, "", {
      noremap = true,
      nowait = true,
      callback = callback,
    })
  end

  return false
end

---@function Function to generate an input prompt
---@param prompt string The prompt text to display
---@param callback function The function to call with the user input
function M.InputPrompt(prompt, callback)
  -- Create a new buffer
  local buf = vim.api.nvim_create_buf(false, true) -- Create a new empty buffer

  -- Set the buffer name
  vim.api.nvim_buf_set_name(buf, "Input")
  -- Set the buffer filetype (e.g., for custom behavior or syntax highlighting)
  vim.bo[buf].filetype = "input"
  -- Set the buffer type to "nofile" to avoid editing or saving the buffer
  vim.bo[buf].buftype = "nofile"
  -- Set the prompt text in the buffer
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, { prompt, "Input: " })

  -- Create a floating window to display the buffer
  local win_height = 2 -- Height of floating window
  local win_width = math.floor(vim.o.columns * 0.25) -- Width of floating window
  local row = math.floor((vim.o.lines - win_height) / 2) -- Position row
  local col = math.floor((vim.o.columns - win_width) / 2) -- Position column
  local win_border = "rounded"
  local style = "minimal"

  -- Create a floating window
  local win = vim.api.nvim_open_win(buf, true, {
    relative = "editor",
    width = win_width,
    height = win_height,
    col = col,
    row = row,
    style = style,
    border = win_border,
  })

  -- Move the cursor to the end of the buffer
  vim.api.nvim_win_set_cursor(win, { 2, 8 })
  -- Set input mode
  vim.api.nvim_command("startinsert")

  -- Function to close the window
  local function exit_win()
    vim.api.nvim_command("stopinsert")
    vim.api.nvim_win_close(win, true)
  end

  -- Function to handle input and close the window
  local function handle_input()
    -- Get the text after the "Input: " string
    local input = vim.api.nvim_buf_get_lines(buf, 1, 2, false)[1]:sub(7) -- Adjust to trim "Input: "
    return input
  end

  ---@function Function to return user input
  local function yes()
    local user_input = handle_input() -- Get the input from the buffer
    exit_win()
    if callback then
      callback(user_input) -- Call the callback with the user input
    end
  end

  ---@function Function to cancel user input
  local function no()
    exit_win()
    if callback then
      callback(nil) -- Call the callback with nil to indicate cancellation
    end
  end

  -- Define buffer-specific key mappings
  local keymaps = {
    ["<CR>"] = yes,
    ["<Esc>"] = no,
  }

  -- Set the key mappings
  for key, keyback in pairs(keymaps) do
    vim.api.nvim_buf_set_keymap(buf, "n", key, "", {
      noremap = true,
      nowait = true,
      callback = keyback,
    })
    vim.api.nvim_buf_set_keymap(buf, "i", key, "", {
      noremap = true,
      nowait = true,
      callback = keyback,
    })
  end
end

---@function Function to move blocks of text up and down
---@param up? boolean Whether to move the blocks up or down
---  Ex: Move selected block of text up: move_visual(true)
---      Move selected block of text down: move_visual(false)
---@param multiplier? integer The number of times to move the blocks
---  Ex: Move selected block of text up 3 times: move_visual(true, 3)
---      Move selected block of text down 2 times: move_visual(false, 2)
---@return nil
function M.move_visual(up, multiplier)
  -- Handle parameter
  local offset = 1
  if up then
    offset = -2
  end
  if multiplier then
    offset = offset * multiplier
  end

  -- escape visual mode
  vim.cmd("norm v")

  -- GET REGION
  -- eg region = { [103] = {0,-1}, [104] = {0,-1}, [105] = {0,-1}}
  ---@diagnostic disable-next-line: deprecated
  local region = vim.region(0, "'<", "'>", vim.fn.visualmode(), true)

  local function array_keys(array)
    local keys = {}
    for k in pairs(array) do
      table.insert(keys, k)
    end
    return keys
  end

  -- GET LINES
  -- eg lines = { 103, 104, 105 }
  local lines = array_keys(region)
  table.sort(lines)

  -- GET TOP/BOTTOM LINE NUMBERS
  local top = lines[1] + 1
  local bottom = lines[#lines] + 1

  -- EXECUTE
  local new_pos = offset > 0 and bottom + offset or top + offset
  vim.cmd(string.format("silent %d, %d move %d", top, bottom, new_pos))
  -- eg :silent 104, 106 move 107

  vim.cmd("norm gv")
end

---@function Function to reload the user's Neovim configuration
---@return nil
function M.reload_config()
  -- Notify the user about the reload process
  M.notify("Reloading configuration...", "WARN", "Reloading Config")
  -- 1. Save all buffers
  vim.cmd("silent! wa")
  -- 2. Record the list of open buffers to reopen later
  local buffer_list = vim.api.nvim_list_bufs()
  local buffers_to_reopen = {}
  for _, buf in ipairs(buffer_list) do
    if vim.api.nvim_buf_is_loaded(buf) and vim.fn.bufname(buf) ~= "" then
      table.insert(
        buffers_to_reopen,
        { buf = buf, file = vim.api.nvim_buf_get_name(buf) }
      )
    end
  end
  -- 3. Close all buffers
  vim.cmd("silent! bufdo! bwipeout")
  -- 4. Clear loaded lua modules related to your custom configuration
  for name, _ in pairs(package.loaded) do
    -- Replace 'userconfig' and 'plugin' with your actual config module names
    if name:match("^userconfig") or name:match("^plugins") then
      package.loaded[name] = nil
    end
  end
  -- 5. Reload the vim script (init.lua)
  vim.cmd("source $MYVIMRC")
  -- 6. Reopen the buffers
  for _, bufinfo in ipairs(buffers_to_reopen) do
    local buf = vim.fn.bufadd(bufinfo.file)
    vim.cmd("buffer " .. buf)
    vim.api.nvim_buf_call(buf, function()
      -- You could also restore the exact cursor position if desired
      vim.cmd('silent! normal! g`"')
    end)
  end -- Notify the user that the config has been reloaded
  M.notify("Configuration reloaded successfully!", "INFO", "Reloaded Config")
end

---@function Function to reload all plugins.
--- This is a messy operation. It's not recommended to use it.
--- If you do, please define the exclusion list in your config.lua file.
--- Suggested defaults:
---   vim.g.plugin_reloader_exclusion_list = {
---         ["lazy.nvim"]      = true,
---         ["noice.nvim"]     = true,
---         ["unception.nvim"] = true,
---         ["nvim-unception"] = true,
---         ["nui.nvim"]       = true,
---         ["packer.nvim"]    = true,
---         ["trouble.nvim"]   = true,
---         ["which-key.nvim"] = true,
---   },
---
--- You should add any other plugins that won't handle a live reload
--- well to this exclusion list and define it in your configuration.
--- The exclusion list can be defined with:
---   'vim.g.plugin_reloader_exclusion_list'
---@see data.types.plugin_reloader.exclusion_list
---@return nil
function M.reload_all_plugins()
  -- Define the exclusion list
  local exclude = {}
  if vim.g.plugin_reloader.exclusion_list then
    -- Use global variable if provided
    exclude = vim.g.plugin_reloader.exclusion_list
  elseif pcall(require, "data.types") then
    -- Use data.types if available
    exclude = require("data.types").plugin_reloader.exclusion_list
  else
    -- Fallback to default
    exclude = {
      ["lazy.nvim"] = true,
      ["noice.nvim"] = true,
      ["unception.nvim"] = true,
      ["nvim-unception"] = true,
      ["nui.nvim"] = true,
      ["packer.nvim"] = true,
      ["trouble.nvim"] = true,
      ["which-key.nvim"] = true,
    }
  end

  -- Get the list of currently loaded plugins
  local plugins = require("lazy.core.config").plugins

  -- Iterate over each plugin and reload it if it's not in the exclusion list
  for plugin_name, _ in pairs(plugins) do
    if not exclude[plugin_name] then
      vim.cmd("Lazy reload " .. plugin_name)
    end
  end
end

---@function Check if a plugin is installed.
---@param plugins string|table The name of the plugin module(s) to check.
---  Options:
---   - string: The name of the plugin module to check.
---   - table: A list of plugin modules to check.
---@param simple boolean|nil Whether to use a simple check (pcall(require, plugin)).
---@return boolean|table condition The installed state of the plugin(s).
---   - boolean: The installed state of the plugin.
---   - table: A list of installed states of the plugins.
---     - boolean: The installed state of the plugin.
---  The return type is determined based on the input type.
---  If the input is a table of plugin modules, the return type is a table.
function M.is_installed(plugins, simple)
  local is_single = type(plugins) == "string"
  if type(plugins) == "string" then
    plugins = { plugins }
  end

  local installed_plugins = {}
  for _, plugin in ipairs(plugins) do
    local installed = false
    if simple then
      installed = pcall(require, plugin)
    else
      local lazy_installed = pcall(require, "lazy")
      if lazy_installed then
        installed = require("lazy.core.config").plugins[plugin] ~= nil
      end
      local packer_installed = pcall(require, "packer_plugins")
      if packer_installed then
        ---@diagnostic disable-next-line: undefined-field
        installed = _G.packer_plugins and _G.packer_plugins[plugin] ~= nil
      end
      if vim.fn.exists("g:plugs") == 1 then
        installed = vim.g.plugs[plugin] ~= nil
      end
      if not installed then
        local has_plug = pcall(require, plugin)
        if has_plug then
          installed = true
        end
      end
    end
    installed_plugins[plugin] = installed
  end

  if is_single then
    return installed_plugins[plugins[1]]
  else
    return installed_plugins
  end
end

-- Function to create a floating terminal window
function M.open_floating_terminal()
  -- Create a scratch buffer specifically for the terminal
  local buf = vim.api.nvim_create_buf(false, true) -- Create an unnamed, non-file, scratch buffer

  if not buf or buf == 0 then
    vim.notify("Failed to create buffer", vim.log.levels.ERROR)
    return
  end

  -- Get the dimensions of the current editor window
  -- This helps us center the floating window
  local ui = vim.api.nvim_list_uis()[1]
  local width = ui.width
  local height = ui.height
  local win_width = math.floor(width * 0.8)
  local win_height = math.floor(height * 0.8)

  -- Define settings of the floating window, sizing it to 80% of the full editor size
  local win_opts = {
    style = "minimal", -- Minimal UI, no status line or tab line
    relative = "editor", -- Float relative to the whole editor UI
    width = win_width, -- Set width to 80% of editor width
    height = win_height, -- Set height to 80% of editor height
    row = math.floor((height - win_height) / 2), -- Centered vertically
    col = math.floor((width - win_width) / 2), -- Centered horizontally
    border = "rounded", -- Add a border for aesthetics (can be 'single', 'double', etc.)
  }

  -- Open the floating window with our newly created buffer
  local win = vim.api.nvim_open_win(buf, true, win_opts)

  -- Check if the window was created successfully
  if not win or win == 0 then
    vim.notify("Failed to create floating window", vim.log.levels.ERROR)
    return
  end

  -- Set very specific buffer and window configurations
  vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = buf }) -- Auto-remove when the buffer is closed
  vim.api.nvim_set_option_value("winblend", 10, { win = win }) -- Add slight transparency to the floating window

  -- Now that the floating window is ready, we run the terminal shell in the created buffer
  -- Open the terminal in the buffer when we're sure the buffer is set up in the float
  vim.fn.termopen(vim.o.shell, {
    on_exit = function()
      -- Safety measure: Ensure the window still exists before trying to close it
      if vim.api.nvim_win_is_valid(win) then
        -- Notify the user that the terminal has closed
        vim.notify("Terminal closed", vim.log.levels.INFO)
        -- Close and wipe the associated floating window and its buffer
        vim.api.nvim_win_close(win, true) -- Force close the terminal window
      end
    end,
  })

  -- Switch focus to the terminal window in the floating buffer and enter insert mode
  vim.api.nvim_set_current_win(win)
  vim.cmd("startinsert!") -- Automatically enter insert mode within the terminal
end

---@function Helper function to add keymaps with common properties
---@param lhs string|table The keybind (or list of keybinds)
--- This field can be the following:
--- - A string representing the keybind
--- - A list of strings representing a set of keybinds
--- - A table of multiple keybind specifications
--- This field is required.
---@param rhs string|function|nil The function to execute when the key is pressed
--- This field can be the following:
--- - A string representing the vimscript command
--- - A lua function (only when using which-key.nvim or global keymaps)
--- This field is required.
---@param desc string|nil The description of the keybind (optional)
--- This field can be the following:
--- - A string representing the description
---   The description will be displayed in the which-key menu
---@param mode string|table|nil The mode(s) in which the keybind should be added (optional)
--- This field can be the following:
--- - A string representing the mode
--- - A table of multiple modes (only when using which-key.nvim or global keymaps)
---@param icon string|nil The icon to use for the keybind (optional)
--- This field can be the following:
--- - A string representing the icon (only when defining a menu)
---@param group string|nil The group to add the keybind to (optional)
--- This field can be the following:
--- - A string representing the group (only when defining a menu)
---@param bufnr number|nil The buffer number to add the keymap to (optional)
--- This field can be the following:
--- - A number representing the buffer
---   This option is incompatible with some extended keymap options
---@param hidden boolean|nil Whether the keybind should be hidden (optional)
--- This field can be the following:
--- - A boolean representing whether to hide the keymap in which-key menus
---   This option is only compatible with which-key configurations
---   It will be ignored if which-key is not installed
---   The default value is false
---@return boolean condition true if the keybind was added, false otherwise
--- There are three main types of keymaps:
--- - Global keymaps
---   This is the most common type of keymap.
---   Ex:
---     add_keymap("<leader>ff", "lua require('telescope.builtin').find_files()")
--- - Buffer keymaps
---   This is used to add keymaps to specific buffers.
---   Ex:
---     add_keymap("<leader>ff", "lua require('telescope.builtin').find_files()", nil, nil, nil, 0)
--- - Which-key menus
---   This is used to add which-key menus.
---   Ex:
---     add_keymap("<leader>l", nil, nil, nil, "󰒲", "Lazy")
---@see which-key.nvim-which-key-mappings
---@see vim.api.nvim_buf_set_keymap
---@see vim.keymap.set
function M.add_keymap(
  lhs, -- The keybind
  rhs, -- Function to execute when the key is pressed
  desc, -- Description of the keybind
  mode, -- Mode(s) in which the keybind should be added
  icon, -- Icon to use for the keybind menu
  group, -- Group to use for the keybind menu
  bufnr, -- Buffer number to add the keymap to
  hidden
)
  -- Check if which-key.nvim is installed
  if M.is_installed("which-key.nvim") and not bufnr then
    require("which-key").add({
      -- stylua: ignore start
      {
        lhs,                 -- The keybind
        rhs   = rhs,         -- Function to execute when the key is pressed
        desc  = desc,        -- Description of the keybind
        mode  = mode or "n", -- Default to "n" (normal mode) if mode is not provided
        icon  = icon,        -- Icon to use for the keybind
        group = group,       -- Group to add the keybind to
        hidden = hidden,     -- Hide the keybind in which-key menus
      },
      -- stylua: ignore end
    })
    return true
  else
    -- Handle the case where lhs is a table
    if type(lhs) == "table" then
      for _, keymap in ipairs(lhs) do
        -- Set default values or use provided ones
        local keymap_rhs = keymap.rhs or rhs
        local keymap_desc = keymap.desc or desc
        local keymap_mode = keymap.mode or mode or "n"
        local keymap_icon = keymap.icon or icon
        local keymap_group = keymap.group or group
        local keymap_lhs = keymap.lhs

        if not keymap_group and not keymap_icon then
          if not bufnr then
            -- Apply the keymap using vim.keymap.set
            vim.keymap.set(
              keymap_mode, -- Mode(s) in which the keybind should be added
              keymap_lhs, -- The keybind
              ---@diagnostic disable-next-line: param-type-mismatch
              keymap_rhs, -- Function to execute when the key is pressed
              { desc = keymap_desc } -- Description of the keybind
            )
          else
            if -- Check if the keymap is compatible with buffer-based keymaps
              type(keymap_mode) == "table" or type(keymap_rhs) == "function"
            then
              -- The keymap is incompatible with buffer-based keymaps
              local msg = string.format(
                "Mapping incompatible with buffer-based keymaps:\n%s%s",
                vim.inspect(keymap_lhs), -- Convert lhs to a readable format
                keymap_desc and ("\nDescription: " .. keymap_desc) or ""
              )
              M.notify(msg, "WARN")
              return false
            end
            -- Apply the keymap using vim.api.nvim_buf_set_keymap
            vim.api.nvim_buf_set_keymap(
              bufnr,
              keymap_mode,
              keymap_lhs,
              ---@diagnostic disable-next-line: param-type-mismatch
              keymap_rhs,
              { desc = keymap_desc }
            )
          end
        else
          -- The keymap requires which-key.nvim
          local msg = string.format(
            "Mapping requires which-key.nvim:\n%s%s",
            vim.inspect(keymap_lhs), -- Convert lhs to a readable format
            keymap_desc and ("\nDescription: " .. keymap_desc) or ""
          )
          M.notify(msg, "WARN")
          return false
        end
      end
      return true
    else -- Handle the case where lhs is not a table
      if not group and not icon then
        if not bufnr then
          vim.keymap.set(
            mode or "n", -- Default to "n" (normal mode) if mode is not provided
            lhs, -- The keybind
            ---@diagnostic disable-next-line: param-type-mismatch
            rhs, -- Function to execute when the key is pressed
            { desc = desc } -- Description of the keybind and optional buffer number
          )
        else
          if -- Check if the keymap is compatible with buffer-based keymaps
            type(mode) == "table" or type(rhs) == "function"
          then
            -- The keymap is incompatible with buffer-based keymaps
            local msg = string.format(
              "Mapping incompatible with buffer-based keymaps:\n%s%s",
              vim.inspect(lhs), -- Convert lhs to a readable format
              desc and ("\nDescription: " .. desc) or ""
            )
            M.notify(msg, "WARN")
            return false
          end
          -- Apply the keymap using vim.api.nvim_buf_set_keymap
          vim.api.nvim_buf_set_keymap(
            bufnr,
            mode or "n",
            lhs,
            ---@diagnostic disable-next-line: param-type-mismatch
            rhs,
            { desc = desc }
          )
        end
        return true
      else
        local msg = string.format(
          "Mapping requires which-key.nvim:\n%s%s",
          vim.inspect(lhs), -- Convert lhs to a readable format
          desc and ("\nDescription: " .. desc) or ""
        )
        M.notify(msg, "WARN")
        return false
      end
    end
  end
end

---@function Helper function to remove keymaps
---@param lhs string The keybind
---@param mode string|nil The mode in which the keybind should be removed (optional)
---@param bufnr number|nil The buffer number to remove the keymap from (optional)
---@return boolean condition true if the keymap was removed, false otherwise
---@see vim.api.nvim_del_keymap
---@see vim.api.nvim_buf_del_keymap
function M.rm_keymap(
  lhs, -- The keybind
  mode, -- Mode(s) in which the keybind should be removed
  bufnr -- Buffer number to remove the keymap from
)
  mode = mode or "n" -- Default to "n" (normal mode) if mode is not provided

  ---@class Keymap
  ---@field lhs string The keybind
  ---@field rhs string|function The function or command associated with the keybind

  if bufnr then
    -- If a buffer number is provided, remove the keymap from the specified buffer
    ---@type Keymap[]
    local keymaps = vim.api.nvim_buf_get_keymap(bufnr, mode)
    for _, keymap in pairs(keymaps) do
      if keymap.lhs and keymap.lhs == lhs then
        vim.api.nvim_buf_del_keymap(bufnr, mode, lhs)
        return true -- Indicate that the keymap was removed
      end
    end
  else
    -- If no buffer number is provided, remove the global keymap
    ---@type Keymap[]
    local keymaps = vim.api.nvim_get_keymap(mode)
    for _, keymap in pairs(keymaps) do
      if keymap.lhs and keymap.lhs == lhs then
        vim.api.nvim_del_keymap(mode, lhs)
        return true -- Indicate that the keymap was removed
      end
    end
  end

  return false -- Indicate that the keymap did not exist
end

---@function Helper function to add a mark
---@param mark string The mark to add
---@param line integer The line to add the mark to
---@param col integer The column to add the mark to
---@return boolean condition true if the mark was added, false otherwise
function M.add_mark(mark, line, col)
  -- Set the mark at the specified line and column
  vim.api.nvim_buf_set_mark(0, mark, line, col, {})
  return true -- Indicate that the mark was successfully added
end

---@function Helper function to remove a mark
---@param mark string The mark to remove
---@return boolean condition true if the mark was removed, false otherwise
function M.rm_mark(mark)
  -- Get a list of marks in the current buffer
  local marks = vim.fn.getmarklist(vim.fn.bufnr("%"))
  -- Check if the mark exists
  for _, m in ipairs(marks) do
    if m.mark == mark then
      -- Mark exists, remove it
      vim.cmd("delmarks " .. mark)
      return true -- Indicate that the mark was removed
    end
  end
  return false -- Indicate that the mark did not exist
end

---@function Function to check if buffer is modified
---@return boolean condition true if buffer is modified, false otherwise
function M.is_buffer_modified()
  return vim.bo.modified
end

---@function Function to check if buffer is empty
---@return boolean condition true if buffer is empty, false otherwise
function M.is_buffer_empty()
  return vim.fn.empty(vim.fn.expand("%:t")) == 1
end

---@function Function to check if buffer is read-only
---@return boolean condition true if buffer is read-only, false otherwise
function M.is_buffer_readonly()
  return vim.bo.readonly
end

---@function Function to check if file exists
---@param filepath string The path to the file
---@return boolean condition true if file exists, false otherwise
function M.file_exists(filepath)
  local f = io.open(filepath, "r")
  if f then
    f:close()
  end
  return f ~= nil
end

---@function Function to get the current git branch
---@return string branch git branch name or "No branch"
function M.get_git_branch()
  local branch = vim.fn.systemlist("git rev-parse --abbrev-ref HEAD")[1]
  if branch and branch ~= "" then
    return branch
  else
    return "No branch"
  end
end

---@function Function to get the current git commit hash
---@return string The current git commit hash or "No commit hash"
function M.get_git_commit_hash()
  local commit_hash = vim.fn.systemlist("git rev-parse --short HEAD")[1]
  if commit_hash and commit_hash ~= "" then
    return commit_hash
  else
    return "No commit hash"
  end
end

---@function Function to run a shell command
---@param cmd string The command to run
---@return string|nil result The output of the command
function M.run_shell_command(cmd)
  local handle = io.popen(cmd)
  if handle then
    local result = handle:read("*a")
    handle:close()
    return result
  else
    -- Handle the error case where `handle` is nil
    M.notify("Failed to run the command: " .. cmd, "ERROR")
    return nil
  end
end

---@function Function to get the dimensions of the current window
---@param format string The format of the dimensions
---@return string|table dimensions dimensions of the current window
function M.get_ws_dimensions(format)
  format = format or "verbose"
  local dimensions = {
    width = tonumber(vim.opt.columns:get()) or 0,
    height = tonumber(vim.opt.lines:get()) or 0,
  }
  if format == "verbose" then
    return string.format(
      "Width: %d cells\nHeight: %d cells",
      dimensions.width,
      dimensions.height
    )
  elseif format == "basic" then
    return string.format("%dx%d", dimensions.width, dimensions.height)
  elseif format == "raw" then
    return dimensions
  else
    -- Trigger an error if the format is not valid
    error("Invalid format: " .. format)
  end
end

---@function Function to get the cursor position
---@return integer row The current line number
---@return integer col The current column number
function M.get_cursor_position()
  local row, col = unpack(vim.api.nvim_win_get_cursor(0))
  return row, col
end

---@function Function to check if a global variable is set
---@param var_name string The name of the global variable
---@param expected_value any The expected value of the global variable
---@param default_value any The default value of the global variable
---@return boolean condition true if the global variable is set to the expected value, false otherwise
function M.check_global_var(var_name, expected_value, default_value)
  local actual_value = vim.g[var_name]

  -- If the global variable is not set, use the default value (if provided)
  if actual_value == nil and default_value ~= nil then
    actual_value = default_value
  end

  -- Return whether the actual value matches the expected value
  return actual_value == expected_value
end

---@function Function to split a string
---@param inputstr string The string to split
---@param sep string The separator
---@return table output The split string
function M.split_string(inputstr, sep)
  if sep == nil then
    sep = "%s"
  end
  local output = {}
  for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do
    table.insert(output, str)
  end
  return output
end

---@function Function to convert RGB to hexadecimal
---@param rgb table The RGB color
---@return string hex The hexadecimal color
function M.rgb_to_hex(rgb)
  return string.format("#%02x%02x%02x", rgb[1], rgb[2], rgb[3])
end

---@function Function to get the foreground color of a highlight group
---@param hlgroup string The name of the highlight group
---@return string|nil hex The foreground color of the highlight group
function M.get_fg_color(hlgroup)
  local hl = vim.api.nvim_get_hl(0, { name = hlgroup, link = false })
  local fg = hl.fg
  if fg then
    return M.rgb_to_hex({
      bit.rshift(bit.band(fg, 0xFF0000), 16),
      bit.rshift(bit.band(fg, 0x00FF00), 8),
      bit.band(fg, 0x0000FF),
    })
  end
  return nil
end

---@function Function to get the background color of a highlight group
---@param hlgroup string The name of the highlight group
---@return string|nil hex The background color of the highlight group
function M.get_bg_color(hlgroup)
  local hl = vim.api.nvim_get_hl(0, { name = hlgroup, link = false })
  local bg = hl.bg
  if bg then
    return M.rgb_to_hex({
      bit.rshift(bit.band(bg, 0xFF0000), 16),
      bit.rshift(bit.band(bg, 0x00FF00), 8),
      bit.band(bg, 0x0000FF),
    })
  end
  return nil
end

---@function Function to check if the window is wide enough
---@param width_limit number The minimum width of the window
---@return boolean condition true if the window is wide enough, false otherwise
function M.is_window_wide_enough(width_limit)
  local width = vim.fn.winwidth(0)
  return width >= width_limit
end

---@function Function to check if the window is tall enough
---@param height_limit number The minimum height of the window
---@return boolean condition true if the window is tall enough, false otherwise
function M.is_window_tall_enough(height_limit)
  local height = vim.fn.winheight(0)
  return height >= height_limit
end

---@function Function to get the current date
---@return string|osdate date The current date
function M.get_date()
  return os.date("%Y-%m-%d")
end

---@function Function to exit neovim
---@return nil
function M.exit()
  vim.api.nvim_command("wqall")
end

-- Export the module
return M
