---@param type string
---@param _ string
---@return string?, string?
local function default_icon_provider(type, _)
  if type == "directory" then
    return "D", "FylerFSDirectory"
  elseif type == "file" then
    return "F", "FylerFSFile"
  else
    return "*", ""
  end
end

return setmetatable({},

{
  __index = function(_, key)
    local status, icon_provider = pcall(require, string.format("fyler.integrations.icon.%s", key))
    if not status then
      return default_icon_provider
    end
    return function(...)
      local success, icon, hl = pcall(icon_provider.get_icon, ...)
      if not success then
        return default_icon_provider(...)
      end
      return icon, hl
    end
  end,
}

local a = require("fyler.lib.async")
local components = require("fyler.lib.ui.components")
local config = require("fyler.config")
local git = require("fyler.lib.git")

local icon_provider = (function()
  if type(config.values.icon_provider) == "function" then
    return config.values.icon_provider
  else
    return require("fyler.integrations.icon")[config.values.icon_provider]
  end
end)()

local Line = components.Line

local M = {}

-- stylua: ignore start
local git_status_hl = setmetatable({
  ["  "] = "FylerGitAdded",
  ["UU"] = "FylerGitConflict",
  [" D"] = "FylerGitDeleted",
  ["!!"] = "FylerGitIgnored",
  [" M"] = "FylerGitModified",
  ["R "] = "FylerGitRenamed",
  ["M "] = "FylerGitStaged",
  ["??"] = "FylerGitUntracked",
  ["MM"] = "FylerGitUnstaged",
}, {
  __index = function()
    return ""
  end,
})
-- stylua: ignore end

local function get_sorted(tbl)
  table.sort(tbl, function(x, y)
    if x:is_directory() and not y:is_directory() then
      return true
    elseif not x:is_directory() and y:is_directory() then
      return false
    else
      return x.name < y.name
    end
  end)

  return tbl
end

local TREE_STRUCTURE
---@param tbl table
---@param status_map? table
---@param cb fun(lines: FylerUiLine[])
TREE_STRUCTURE = a.async(function(tbl, status_map, depth, cb)
  depth = depth or 0
  if not tbl then
    return cb {}
  end

  local lines = {}
  for _, item in ipairs(get_sorted(tbl)) do
    local icon, hl = (function()
      if item.type == "directory" then
        return icon_provider(item.type, item.name)
      elseif item.type == "link" then
        return icon_provider(item.link_type, item.name)
      else
        return icon_provider(item.type, item.name)
      end
    end)()

    local git_symbol = (function()
      if not status_map then
        return nil
      end

      if status_map[item.path] then
        return status_map[item.path]
      end

      return nil
    end)()

    table.insert(
      lines,
      Line {
        words = {
          { str = string.rep("  ", depth) },
          {
            str = icon,
            hl = (function()
              if item.type == "directory" then
                return "FylerFSDirectory"
              elseif item.link_type == "directory" then
                return "FylerFSDirectory"
              else
                return hl
              end
            end)(),
          },
          {
            str = git_symbol and string.format(" %s ", git_symbol) or " ",
            hl = git_status_hl[git_symbol],
          },
          { str = string.format("/%s", item.meta) },
          {
            str = string.format(" %s", item.name),
            hl = (function()
              if git_symbol then
                return git_status_hl[git_symbol]
              elseif item.type == "directory" then
                return "FylerFSDirectory"
              elseif item.link_type == "directory" then
                return "FylerFSDirectory"
              else
                return ""
              end
            end)(),
          },
        },
        marks = (function()
          local line = {}
          if item.type == "link" then
            table.insert(line, {
              hl = "FylerFSLink",
              str = string.format(" %s", item.link_path),
            })
          end

          return line
        end)(),
      }
    )

    if item.children then
      for _, line in ipairs(a.await(TREE_STRUCTURE, item.children, status_map, depth + 1)) do
        table.insert(lines, line)
      end
    end
  end

  return cb(lines)
end)

M.Explorer = a.async(function(tbl, cb)
  return cb(a.await(TREE_STRUCTURE, tbl, config.values.git_status and a.await(git.status_map) or {}, 0))
end)

return M

local M = {}

local fn = vim.fn

---@param type string
---@param name string
---@return string?, string?
function M.get_icon(type, name)
  local success, devicons = pcall(require, "nvim-web-devicons")
  if not success then
    return nil, nil
  end

  local icon, hl = devicons.get_icon(name, fn.fnamemodify(name, ":e"))
  icon = (type == "directory" and "" or (icon or ""))
  hl = hl or (type == "directory" and "Fylerblue" or "")
  return icon, hl
end

return M

---@class FylerConfigView
---@field width  number
---@field height number
---@field kind   FylerWinKind
---@field border string|string[]

---@class FylerConfig
---@field auto_confirm_simple_edits? boolean
---@field close_on_select?           boolean
---@field default_explorer?          boolean
---@field git_status                 boolean
---@field icon_provider?             string|fun(type: string, name: string): string, string
---@field indentscope                { enabled: boolean, group: string, marker: string }
---@field on_highlights              fun(hl_groups: table, palette: table): nil
---@field views?                     table<string, FylerConfigView>
---@field mappings?                  table<string, table<"n"|"i", table<string, string>>>

local M = {}

local defaults = {
  auto_confirm_simple_edits = false,
  close_on_select = true,
  default_explorer = false,
  git_status = true,
  icon_provider = "mini-icons",
  on_highlights = nil,
  indentscope = {
    enabled = true,
    group = "FylerIndentMarker",
    marker = "│",
  },
  mappings = {
    confirm = {
      n = {
        ["y"] = "Confirm",
        ["n"] = "Discard",
      },
    },
    explorer = {
      n = {
        ["q"] = "CloseView",
        ["<CR>"] = "Select",
      },
    },
  },
  views = {
    confirm = {
      width = 0.5,
      height = 0.4,
      kind = "float",
      border = "single",
      buf_opts = {
        buflisted = false,
        modifiable = false,
      },
      win_opts = {
        winhighlight = "Normal:Normal,FloatBorder:FloatBorder,FloatTitle:FloatTitle",
        wrap = false,
      },
    },
    explorer = {
      width = 0.8,
      height = 0.8,
      kind = "float",
      border = "single",
      buf_opts = {
        buflisted = false,
        buftype = "acwrite",
        filetype = "fyler",
        syntax = "fyler",
      },
      win_opts = {
        concealcursor = "nvic",
        conceallevel = 3,
        cursorline = true,
        number = true,
        relativenumber = true,
        winhighlight = "Normal:Normal,FloatBorder:FloatBorder,FloatTitle:FloatTitle",
        wrap = false,
      },
    },
  },
}

---@param name string
function M.get_view(name)
  assert(name, "name is required")

  return M.values.views[name]
end

---@param name string
function M.get_mappings(name)
  assert(name, "name is required")

  return M.values.mappings[name]
end

---@param name string
function M.get_reverse_mappings(name)
  assert(name, "name is required")

  local mappings = M.get_mappings(name)
  local reverse_mappings = {}

  for _, map in pairs(mappings) do
    for key, val in pairs(map) do
      reverse_mappings[val] = key
    end
  end

  return reverse_mappings
end

---@param opts? FylerConfig
function M.setup(opts)
  opts = opts or {}
  M.values = vim.tbl_deep_extend("force", defaults, opts)
end

return M

local M = {}

---@param type string
---@param name string
---@return string?, string?
function M.get_icon(type, name)
  local success, miniicons = pcall(require, "mini.icons")
  if not success then
    return nil, nil
  end

  return miniicons.get(type, name)
end

return M

local a = require("fyler.lib.async")
local algos = require("fyler.views.explorer.algos")
local cache = require("fyler.cache")
local config = require("fyler.config")
local confirm_view = require("fyler.views.confirm")
local fs = require("fyler.lib.fs")
local regex = require("fyler.views.explorer.regex")
local store = require("fyler.views.explorer.store")
local ui = require("fyler.views.explorer.ui")

local M = {}

local fn = vim.fn
local api = vim.api

---@param view table
function M.n_close_view(view)
  return function()
    local success = pcall(api.nvim_win_close, view.win.winid, true)
    if not success then
      api.nvim_win_set_buf(view.win.winid, fn.bufnr("#", true))
    end

    pcall(api.nvim_buf_delete, view.win.bufnr, { force = true })
  end
end

---@param view FylerExplorerView
function M.n_select(view)
  return function()
    local key = regex.match_meta(api.nvim_get_current_line())
    if not key then
      return
    end

    local meta_data = store.get(key)
    if meta_data:is_directory() then
      view.fs_root:find(key):toggle()
      api.nvim_exec_autocmds("User", { pattern = "RefreshView" })
    else
      local recent_win = cache.get_entry("recent_win")

      if recent_win and api.nvim_win_is_valid(recent_win) then
        fn.win_execute(recent_win, string.format("edit %s", meta_data:resolved_path()))
        fn.win_gotoid(recent_win)

        if config.values.close_on_select then
          view.win:hide()
        end
      end
    end
  end
end

---@param tbl table
---@return table
local function get_tbl(tbl)
  local lines = { { { str = "", hl = "" } } }
  if not vim.tbl_isempty(tbl.create) then
    table.insert(lines, { { str = "# Creation", hl = "FylerConfirmGreen" } })
    for _, change in ipairs(tbl.create) do
      table.insert(lines, {
        { str = "  | " },
        { str = fs.relpath(fs.getcwd(), change), hl = "FylerConfirmGrey" },
      })
    end
    table.insert(lines, { { str = "" } })
  end

  if not vim.tbl_isempty(tbl.delete) then
    table.insert(lines, { { str = "# Deletion", hl = "FylerConfirmRed" } })
    for _, change in ipairs(tbl.delete) do
      table.insert(lines, {
        { str = "  | " },
        { str = fs.relpath(fs.getcwd(), change), hl = "FylerConfirmGrey" },
      })
    end
    table.insert(lines, { { str = "" } })
  end

  if not vim.tbl_isempty(tbl.move) then
    table.insert(lines, { { str = "# Migration", hl = "FylerConfirmYellow" } })
    for _, change in ipairs(tbl.move) do
      table.insert(lines, {
        { str = "  | " },
        { str = fs.relpath(fs.getcwd(), change.src), hl = "FylerConfirmGrey" },
        { str = " > " },
        { str = fs.relpath(fs.getcwd(), change.dst), hl = "FylerConfirmGrey" },
      })
    end
    table.insert(lines, { { str = "" } })
  end

  return lines
end

---@param changes table
---@return boolean
local function can_bypass(changes)
  if not vim.tbl_isempty(changes.delete) then
    return false
  end

  if #changes.move > 1 then
    return false
  end

  if #changes.create > 5 then
    return false
  end

  return true
end

---@param view FylerExplorerView
function M.synchronize(view)
  return a.async(function()
    local changes = algos.get_diff(view)
    local can_sync = (function()
      if #changes.create == 0 and #changes.delete == 0 and #changes.move == 0 then
        return true
      end

      if not config.values.auto_confirm_simple_edits then
        return a.await(confirm_view.open, get_tbl(changes), "(y/n)")
      end

      if can_bypass(changes) then
        return true
      end

      return a.await(confirm_view.open, get_tbl(changes), "(y/n)")
    end)()

    if can_sync then
      for _, change in ipairs(changes.create) do
        local err, success = a.await(fs.create, change)
        if not success then
          vim.schedule(function()
            vim.notify(err, vim.log.levels.ERROR)
          end)
        end
      end

      for _, change in ipairs(changes.delete) do
        local err, success = a.await(fs.delete, change)
        if not success then
          vim.schedule(function()
            vim.notify(err, vim.log.levels.ERROR)
          end)
        end
      end

      for _, change in ipairs(changes.move) do
        local err, success = a.await(fs.move, change.src, change.dst)
        if not success then
          vim.schedule(function()
            vim.notify(err, vim.log.levels.ERROR)
          end)
        end
      end
    end

    vim.schedule(function()
      api.nvim_exec_autocmds("User", { pattern = "RefreshView" })
    end)
  end)
end

---@param view FylerExplorerView
---@param on_render function
function M.refreshview(view, on_render)
  return a.async(function()
    if not view.win:has_valid_bufnr() then
      return
    end

    a.await(view.fs_root.update, view.fs_root)

    vim.bo[view.win.bufnr].undolevels = -1

    view.win.ui:render {
      ui_lines = a.await(ui.Explorer, algos.tree_table_from_node(view).children),
      on_render = function()
        if on_render then
          on_render()
        end

        M.draw_indentscope(view)()

        if not view.win:has_valid_bufnr() then
          return
        end

        vim.bo[view.win.bufnr].undolevels = vim.go.undolevels
      end,
    }

    vim.bo[view.win.bufnr].syntax = "fyler"
    vim.bo[view.win.bufnr].filetype = "fyler"
  end)
end

function M.constrain_cursor(view)
  return function()
    local cur = api.nvim_get_current_line()
    local meta = regex.match_meta(cur)
    if not meta then
      return
    end

    local _, ub = string.find(cur, meta)
    if not view.win:has_valid_winid() then
      return
    end

    local row, col = unpack(api.nvim_win_get_cursor(view.win.winid))
    if col <= ub then
      api.nvim_win_set_cursor(view.win.winid, { row, ub + 1 })
    end
  end
end

---@param view FylerExplorerView
function M.try_focus_buffer(view)
  return a.schedule_async(function(arg)
    local explorer = require("fyler.views.explorer")

    if not view.win:is_visible() then
      return
    end

    if arg.file == "" then
      return
    end

    if string.match(arg.file, "^fyler://*") then
      local recent_win = cache.get_entry("recent_win")
      if (type(recent_win) ~= "number") or (not api.nvim_win_is_valid(recent_win)) then
        return
      end

      local recent_bufname = fn.bufname(api.nvim_win_get_buf(recent_win))
      if recent_bufname == "" or string.match(recent_bufname, "^fyler://*") then
        return
      end

      arg.file = fs.abspath(recent_bufname)
    end

    if not vim.startswith(arg.file, view.cwd) then
      explorer.open {
        enter = fn.bufname("%") == view.win.bufname,
        cwd = fn.fnamemodify(arg.file, ":h"),
      }
    end

    local relpath = fs.relpath(view.cwd, arg.file)
    if not relpath then
      return
    end

    local focused_node = view.fs_root
    local last_visit = 0
    local parts = vim.split(relpath, "/")

    a.await(focused_node.update, focused_node)

    for i, part in ipairs(parts) do
      local child = vim.iter(focused_node.children):find(function(child)
        return store.get(child.meta).name == part
      end)

      if not child then
        break
      end

      if store.get(child.meta):is_directory() then
        child.open = true
        a.await(child.update, child)
      end

      focused_node, last_visit = child, i
    end

    if last_visit ~= #parts then
      return
    end

    M.refreshview(view, function()
      if not view.win:has_valid_winid() then
        return
      end

      local buf_lines = api.nvim_buf_get_lines(view.win.bufnr, 0, -1, false)
      for ln, buf_line in ipairs(buf_lines) do
        if buf_line:find(focused_node.meta) then
          api.nvim_win_set_cursor(view.win.winid, { ln, 0 })
        end
      end
    end)()
  end)
end

local extmark_namespace = api.nvim_create_namespace("FylerIndentScope")

---@param view FylerExplorerView
function M.draw_indentscope(view)
  local function draw_line(ln)
    if not view.win:has_valid_bufnr() then
      return
    end

    local cur_line = unpack(api.nvim_buf_get_lines(view.win.bufnr, ln - 1, ln, false))
    local cur_indent = #regex.match_indent(cur_line)
    if cur_indent == 0 then
      return
    end

    local indent_depth = math.floor(cur_indent * 0.5)
    for i = 1, indent_depth do
      api.nvim_buf_set_extmark(view.win.bufnr, extmark_namespace, ln - 1, 0, {
        hl_mode = "combine",
        virt_text = {
          {
            config.values.indentscope.marker,
            config.values.indentscope.group,
          },
        },
        virt_text_pos = "overlay",
        virt_text_win_col = (i - 1) * 2,
      })
    end
  end

  return function()
    if not view.win:has_valid_bufnr() then
      return
    end

    if not config.values.indentscope.enabled then
      return
    end

    api.nvim_buf_clear_namespace(view.win.bufnr, extmark_namespace, 0, -1)
    for i = 1, api.nvim_buf_line_count(view.win.bufnr) do
      draw_line(i)
    end
  end
end

return M

local Ui = require("fyler.lib.ui")

local api = vim.api

---@alias FylerWinKind
---| "float"
---| "split:left"
---| "split:above"
---| "split:right"
---| "split:below"
---| "split:leftmost"
---| "split:abovemost"
---| "split:rightmost"
---| "split:belowmost"

---@class FylerWin
---@field augroup       string          - Autogroup associated with window instance
---@field autocmds      table           - Autocommands locally associated with window instance
---@field border        string|string[] - Border format - read ':winborder' for more info
---@field bufname       string          - Builtin way to name neovim buffers
---@field bufnr?        integer         - Buffer number associated with window instance
---@field buf_opts      table           - Buffer local options
---@field enter         boolean         - whether to enter in the window on open
---@field footer?       any             - Footer content
---@field footer_pos?   string          - Footer alignment
---@field height        number          - Height of window
---@field kind          FylerWinKind    - Decides launch behaviour of window instance
---@field mappings      table           - Kemaps local to the window instance
---@field name          string          - Also know as `view_name` which helps to get specific config from user end
---@field namespace     integer         - Namespace associated with window instance
---@field render?       function        - Defines what to render on the screen on open
---@field title?        any             - Title content
---@field title_pos?    string          - Title alignment
---@field ui            FylerUi         - Ui responsible to render lines return from corresponding render function
---@field user_autocmds table           - User autocommands associated with window instance
---@field width         number          - Width of window
---@field winid?        integer         - Window id associated with window instance
---@field win_opts      table           - Window local options
local Win = {}
Win.__index = Win

-- Prepares namespace ID by attaching buffer's given name
---@param name string
local function get_namespace(name)
  return api.nvim_create_namespace("Fyler" .. name)
end

-- Prepares autogroup ID by attaching buffer's given name
---@param name string
local function get_augroup(name)
  return api.nvim_create_augroup("Fyler" .. name, { clear = true })
end

local M = setmetatable({}, {
  ---@return FylerWin
  __call = function(_, opts)
    opts = opts or {}

    assert(opts.name, "name is required field")
    assert(opts.bufname, "bufname is required field")

    -- stylua: ignore start
    local instance = {
      augroup       = get_augroup(opts.name),
      autocmds      = opts.autocmds or {},
      border        = opts.border,
      bufname       = opts.bufname,
      buf_opts      = opts.buf_opts or {},
      enter         = opts.enter,
      footer        = opts.footer,
      footer_pos    = opts.footer_pos,
      height        = opts.height,
      kind          = opts.kind or "float",
      mappings      = opts.mappings or {},
      name          = opts.name or "",
      namespace     = get_namespace(opts.name),
      render        = opts.render,
      title         = opts.title,
      title_pos     = opts.title_pos,
      user_autocmds = opts.user_autocmds or {},
      width         = opts.width,
      win_opts      = opts.win_opts or {},
    }
    -- stylua: ignore end

    instance.ui = Ui(instance)
    setmetatable(instance, Win)

    return instance
  end,
})

-- Determine whether the `Win` has valid buffer
---@return boolean
function Win:has_valid_bufnr()
  return type(self.bufnr) == "number" and api.nvim_buf_is_valid(self.bufnr)
end

-- Determine whether the `Win` has valid window
---@return boolean
function Win:has_valid_winid()
  return type(self.winid) == "number" and api.nvim_win_is_valid(self.winid)
end

---@return boolean
function Win:is_visible()
  return self:has_valid_bufnr() and self:has_valid_winid()
end

-- Construct respective window config in vim understandable format
---@return vim.api.keyset.win_config
function Win:config()
  local winconfig = {
    style = "minimal",
    noautocmd = true,
    title = self.title,
    title_pos = self.title_pos,
    footer = self.footer,
    footer_pos = self.footer_pos,
  }

  if self.kind:match("^split:") then
    winconfig.split = self.kind:match("^split:(.*)")
    winconfig.title = nil
    winconfig.title_pos = nil
    winconfig.footer = nil
    winconfig.footer_pos = nil
  end

  if self.kind == "float" then
    winconfig.relative = "editor"
    winconfig.border = self.border
    winconfig.col = math.floor((1 - self.width) * 0.5 * vim.o.columns)
    winconfig.row = math.floor((1 - self.height) * 0.5 * vim.o.lines)
  end

  winconfig.width = math.ceil(self.width * vim.o.columns)
  winconfig.height = math.ceil(self.height * vim.o.lines)

  return winconfig
end

function Win:show()
  if self:has_valid_winid() then
    return
  end

  local recent_win = api.nvim_get_current_win()
  local win_config = self:config()

  self.bufnr = api.nvim_create_buf(false, true)
  if self.render then
    self.render()
  end

  api.nvim_buf_set_name(self.bufnr, self.bufname)

  if win_config.split and win_config.split:match("^%w+most$") then
    if win_config.split == "leftmost" then
      api.nvim_command(string.format("topleft %dvsplit", win_config.width))
    elseif win_config.split == "abovemost" then
      api.nvim_command(string.format("topleft %dsplit", win_config.height))
    elseif win_config.split == "rightmost" then
      api.nvim_command(string.format("botright %dvsplit", win_config.width))
    elseif win_config.split == "belowmost" then
      api.nvim_command(string.format("botright %dsplit", win_config.height))
    else
      error(string.format("Invalid window kind `%s`", win_config.split))
    end

    self.winid = api.nvim_get_current_win()

    if not self.enter then
      api.nvim_set_current_win(recent_win)
    end

    api.nvim_win_set_buf(self.winid, self.bufnr)
  else
    self.winid = api.nvim_open_win(self.bufnr, self.enter, win_config)
  end

  api.nvim_exec_autocmds("BufEnter", {})

  for mode, map in pairs(self.mappings) do
    for key, val in pairs(map) do
      vim.keymap.set(mode, key, val, { buffer = self.bufnr, silent = true, noremap = true })
    end
  end

  for option, value in pairs(self.win_opts) do
    vim.w[self.winid][option] = vim.wo[self.winid][option]
    vim.wo[self.winid][option] = value
  end

  for option, value in pairs(self.buf_opts) do
    vim.bo[self.bufnr][option] = value
  end

  for event, callback in pairs(self.autocmds) do
    api.nvim_create_autocmd(event, {
      group = self.augroup,
      buffer = self.bufnr,
      callback = callback,
    })
  end

  for event, callback in pairs(self.user_autocmds) do
    api.nvim_create_autocmd("User", {
      pattern = event,
      group = self.augroup,
      callback = callback,
    })
  end
end

function Win:hide()
  if self:has_valid_winid() then
    api.nvim_win_close(self.winid, true)
  end

  if self:has_valid_bufnr() then
    api.nvim_buf_delete(self.bufnr, { force = true })
  end
end

return M

local M = {}

local queue = require("fyler.lib.structs.queue")() ---@type FylerQueue
local is_loop_running = false

---@param co thread
---@param ... any
local function schedule(co, ...)
  queue:enqueue {
    args = { ... },
    co = co,
    started = false,
  }

  if is_loop_running then
    return
  end

  is_loop_running = true

  local function loop()
    if queue:is_empty() then
      is_loop_running = false
      return
    end

    local front = queue:front()
    local status = coroutine.status(front.co)

    if status == "dead" then
      queue:dequeue()

      vim.defer_fn(loop, 0)
    elseif status == "suspended" and front.started == false then
      local success = coroutine.resume(front.co, unpack(front.args))
      if not success then
        queue:dequeue()
      end

      front.started = true

      vim.defer_fn(loop, 0)
    else
      vim.defer_fn(loop, 10)
    end
  end

  vim.defer_fn(loop, 0)
end

function M.schedule_async(async_fn)
  return function(...)
    schedule(coroutine.create(async_fn), ...)
  end
end

function M.async(async_fn)
  return function(...)
    local ok, err = coroutine.resume(coroutine.create(async_fn), ...)
    if not ok then
      error(err)
    end
  end
end

function M.await(fn, ...)
  local thread = coroutine.running()
  local args = { ... }

  table.insert(
    args,
    vim.schedule_wrap(function(...)
      if not thread then
        return error("no coroutine is running")
      end

      coroutine.resume(thread, ...)
    end)
  )

  fn(unpack(args))

  return coroutine.yield()
end

return M

local List = require("fyler.lib.structs.list")
local Stack = require("fyler.lib.structs.stack")

local a = require("fyler.lib.async")

local M = {}

local api = vim.api

local uv = vim.uv or vim.loop
local fn = vim.fn

---@return string
function M.getcwd()
  return uv.cwd() or fn.getcwd(0)
end

---@param path string
---@return string
function M.normalize(path)
  return vim.fs.normalize(path)
end

---@return string
function M.joinpath(...)
  return vim.fs.joinpath(...)
end

---@param path string
---@return string
function M.abspath(path)
  return vim.fs.abspath(path)
end

---@param path string
function M.relpath(base, path)
  return vim.fs.relpath(base, path)
end

---@param path string
---@return string?, string?
function M.resolve_link(path)
  local res_path = nil
  local res_type = nil
  while true do
    local stat = uv.fs_stat(path)
    if not stat then
      break
    end

    local linkdata = uv.fs_readlink(path)
    if not linkdata then
      res_path = path
      res_type = stat.type
      break
    end

    path = linkdata
  end

  return res_path, res_type
end

---@param path string
---@param cb fun(err?: string, items: table)
M.ls = a.async(function(path, cb)
  local stat_err, stat = a.await(uv.fs_stat, path)
  if not stat then
    return cb(stat_err, {})
  end

  ---@diagnostic disable-next-line: param-type-mismatch
  local dir = uv.fs_opendir(path, nil, 1000)
  local items = {}

  while true do
    local _, entries = a.await(uv.fs_readdir, dir)
    if not entries then
      break
    end

    items = vim.list_extend(
      items,
      vim
        .iter(entries)
        :map(function(entry)
          local link_path, link_type = M.resolve_link(M.joinpath(path, entry.name))
          return {
            name = entry.name,
            type = entry.type,
            path = M.joinpath(path, entry.name),
            link_path = link_path,
            link_type = link_type,
          }
        end)
        :totable()
    )
  end

  return cb(nil, items)
end)

---@param path string
---@param cb fun(err?: string, success: boolean)
M.touch = a.async(function(path, cb)
  local stat_err, stat = a.await(uv.fs_stat, path)
  if stat then
    return cb(stat_err, false)
  end

  local fd_err, fd = a.await(uv.fs_open, path, "a", 420)
  if not fd then
    return cb(fd_err, false)
  end

  return cb(a.await(uv.fs_close, fd))
end)

---@param path string
---@param cb fun(err?: string, success: boolean)
M.rm = a.async(function(path, cb)
  local stat_err, stat = a.await(uv.fs_stat, path)
  if not stat or stat.type == "directory" then
    return cb(stat_err, false)
  end

  local unlink_err, success = a.await(uv.fs_unlink, path)
  if not success then
    return cb(unlink_err, false)
  end

  return cb(nil, true)
end)

---@param path string
---@param cb fun(err?: string, success: boolean)
M.rm_r = a.async(function(path, cb)
  local stat_err, stat = a.await(uv.fs_stat, path)
  if not stat then
    return cb(stat_err, false)
  end

  local stk, lst = Stack(), List()

  stk:push { path = path, type = stat.type }

  while not stk:is_empty() do
    local cur_entry = stk:pop()

    lst:insert(1, cur_entry)

    if cur_entry.type == "directory" then
      local ls_err, entries = a.await(M.ls, cur_entry.path)
      if ls_err then
        return cb(ls_err, false)
      end

      for _, entry in ipairs(entries) do
        stk:push(entry)
      end
    end
  end

  for _, entry in ipairs(lst:totable()) do
    if entry.type == "directory" then
      local rmdir_err, rmdir_success = a.await(M.rmdir, entry.path)
      if not rmdir_success then
        return cb(rmdir_err, false)
      end
    else
      local rm_err, rm_success = a.await(M.rm, entry.path)
      if not rm_success then
        return cb(rm_err, false)
      end
    end
  end

  return cb(nil, true)
end)

---@param path string
---@param cb fun(err?: string, success: boolean)
M.mkdir = a.async(function(path, cb)
  local stat_err, stat = a.await(uv.fs_stat, path)
  if stat and stat.type == "directory" then
    return cb(stat_err, false)
  end

  local mkdir_err, success = a.await(uv.fs_mkdir, path, 493)
  if not success then
    return cb(mkdir_err, false)
  end

  return cb(nil, true)
end)

---@param path string
---@param cb fun(err?: string, success: boolean)
M.mkdir_p = a.async(function(path, cb)
  local parts = vim
    .iter(vim.split(path, "/"))
    :filter(function(part)
      return part ~= ""
    end)
    :totable()

  local is_win = fn.has("win32") == 1 or fn.has("win64") == 1

  local dir = is_win and parts[1] or ""

  local start_idx = is_win and 2 or 1
  for i = start_idx, #parts do
    dir = dir .. string.format("/%s", parts[i])

    local _, stat = a.await(uv.fs_stat, dir)
    if not stat then
      local mkdir_err, mkdir_success = a.await(M.mkdir, dir)
      if not mkdir_success then
        return cb(mkdir_err, false)
      end
    end
  end

  return cb(nil, true)
end)

---@param path string
---@param cb fun(err?: string, success: boolean)
M.rmdir = a.async(function(path, cb)
  local stat_err, stat = a.await(uv.fs_stat, path)
  if not stat then
    return cb(stat_err, false)
  end

  local rmdir_err, success = a.await(uv.fs_rmdir, path)
  if not success then
    return cb(rmdir_err, false)
  end

  return cb(nil, true)
end)

---@param src_path string
---@param dst_path string
---@param cb fun(err?: string, success: boolean)
M.mv = a.async(function(src_path, dst_path, cb)
  local stat_err, src_stat = a.await(uv.fs_stat, src_path)
  if not src_stat then
    return cb(stat_err, false)
  end

  local _, dst_stat = a.await(uv.fs_stat, dst_path)
  if dst_stat then
    return cb("Destination path already exists", false)
  end

  local rename_err, rename_success = a.await(uv.fs_rename, src_path, dst_path)
  if not rename_success then
    return cb(rename_err, false)
  end

  return cb(nil, true)
end)

---@param path string
---@param cb fun(err?: string, success: boolean)
M.create = a.async(function(path, cb)
  local mkdirp_err, mkdirp_success =
    a.await(M.mkdir_p, fn.fnamemodify(path, vim.endswith(path, "/") and ":h:h" or ":h"))
  if not mkdirp_success then
    return cb(mkdirp_err, false)
  end

  if vim.endswith(path, "/") then
    local mkdir_err, mkdir_success = a.await(M.mkdir, path)
    if not mkdir_success then
      return cb(mkdir_err, false)
    end
  else
    local touch_err, touch_success = a.await(M.touch, path)
    if not touch_success then
      return cb(touch_err, false)
    end
  end

  return cb(nil, true)
end)

local function get_alt_buf(for_buf)
  for _, buf in ipairs(api.nvim_list_bufs()) do
    if api.nvim_buf_is_valid(buf) and buf ~= for_buf then
      return buf
    end
  end

  return api.nvim_create_buf(false, true)
end

---@param path string
---@param cb fun(err?: string, success: boolean)
M.delete = a.async(function(path, cb)
  local rm_r_err, rm_r_success = a.await(M.rm_r, path)
  if not rm_r_success then
    return cb(rm_r_err, false)
  end

  local buf = fn.bufnr(path)
  if buf == -1 then
    return cb("Unable to find buffer to delete", true)
  end

  local alt = get_alt_buf(buf)
  for _, win in ipairs(api.nvim_list_wins()) do
    if api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == buf then
      if alt < 1 or alt == buf then
        alt = api.nvim_create_buf(true, false)
      end

      api.nvim_win_set_buf(win, alt)
    end
  end

  local success, msg = pcall(api.nvim_buf_delete, buf, { force = true })
  if not success then
    return cb(msg, true)
  end

  return cb(nil, true)
end)

---@param src_path string
---@param dst_path string
---@param cb fun(err?: string, success: boolean)
M.move = a.async(function(src_path, dst_path, cb)
  local mkdirp_err, mkdirp_success = a.await(M.mkdir_p, fn.fnamemodify(dst_path, ":h"))
  if not mkdirp_success then
    return cb(mkdirp_err, false)
  end

  local mv_err, mv_success = a.await(M.mv, src_path, dst_path)
  if not mv_success then
    return cb(mv_err, false)
  end

  local src_buf = fn.bufnr(src_path)
  if src_buf == -1 then
    return cb("unable to find moved buffer", true)
  end

  local dst_buf = fn.bufadd(dst_path)
  fn.bufload(dst_buf)
  vim.bo[dst_buf].buflisted = true

  for _, win in ipairs(api.nvim_list_wins()) do
    if api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == src_buf then
      api.nvim_win_set_buf(win, dst_buf)
    end
  end

  local success, msg = pcall(api.nvim_buf_delete, src_buf, { force = true })
  if not success then
    return cb(msg, true)
  end

  return cb(nil, true)
end)

return M

local Win = require("fyler.lib.win")
local config = require("fyler.config")
local ui = require("fyler.views.confirm.ui")

---@class FylerConfirmView
---@field win FylerWin
local FylerConfirmView = {}
FylerConfirmView.__index = FylerConfirmView

---@param msg { str: string, hl: string }[]
---@param chs string
---@param cb fun(c: boolean)
function FylerConfirmView:open(msg, chs, cb)
  local mappings = config.get_reverse_mappings("confirm")
  local view = config.get_view("confirm")

  --stylua: ignore start
  self.win = Win {
    border   = view.border,
    buf_opts = view.buf_opts,
    bufname  = "confirm",
    enter    = true,
    height   = view.height,
    kind     = view.kind,
    name     = "confirm",
    title    = string.format(" Confirm %s ", chs),
    width    = view.width,
    win_opts = view.win_opts,
    mappings = {
      n = {
        [mappings["Confirm"]] = self:_action("n_confirm", cb),
        [mappings["Discard"]] = self:_action("n_discard", cb),
      },
    },
    autocmds = {
      ["QuitPre"] = self:_action("n_close_view", cb),
    },
    render = function()
      self.win.ui:render({ ui_lines = ui.Confirm(msg) })
    end,
  }
  --stylua: ignore end

  self.win:show()
end

function FylerConfirmView:close()
  self.win:hide()
end

---@param name string
function FylerConfirmView:_action(name, ...)
  local action = require("fyler.views.confirm.actions")[name]

  assert(action, string.format("%s action is not available", name))

  return action(self, ...)
end

local M = {}

---@param msg { str: string, hl: string }[]
---@param chs string
---@param cb fun(c: boolean)
M.open = vim.schedule_wrap(function(msg, chs, cb)
  if not M.instance then
    M.instance = setmetatable({}, FylerConfirmView)
  end

  if M.instance.win and M.instance.win:has_valid_winid() then
    M.instance:close()
  end

  M.instance:open(msg, chs, cb)
end)

return M

local M = {}

local api = vim.api
local uv = vim.uv or vim.loop

local augroup = api.nvim_create_augroup("Fyler", { clear = true })

function M.setup(opts)
  opts = opts or {}

  api.nvim_create_autocmd("ColorScheme", {
    group = augroup,
    callback = function()
      require("fyler.lib.hls").setup()
    end,
  })

  api.nvim_create_autocmd("BufEnter", {
    group = augroup,
    callback = function(...)
      local cur_instance = require("fyler.views.explorer").instance
      if cur_instance then
        cur_instance:_action("try_focus_buffer")(...)
      end
    end,
  })

  api.nvim_create_autocmd("BufEnter", {
    group = augroup,
    callback = vim.schedule_wrap(function(arg)
      local explorer = require("fyler.views.explorer").instance
      if not explorer then
        return
      end

      if vim.fn.bufname(arg.buf) == explorer.win.bufname then
        return
      end

      if api.nvim_get_current_win() == explorer.win.winid then
        for option, _ in pairs(require("fyler.config").get_view("explorer").win_opts) do
          if not explorer.win:has_valid_winid() then
            return
          end

          vim.wo[explorer.win.winid][option] = vim.w[explorer.win.winid][option]
        end
      end
    end),
  })

  if opts.values.default_explorer then
    api.nvim_create_autocmd("BufEnter", {
      group = augroup,
      callback = function(arg)
        local stats = uv.fs_stat(arg.file)

        if stats and stats.type == "directory" then
          local cur_buf = api.nvim_get_current_buf()

          if api.nvim_buf_is_valid(cur_buf) then
            api.nvim_buf_delete(cur_buf, { force = true })
          end

          require("fyler").open { cwd = arg.file }
        end
      end,
    })
  end
end

return M

local api = vim.api

---@class FylerUi
---@field win FylerWin
local Ui = {}
Ui.__index = Ui

local M = setmetatable({}, {
  ---@param win FylerWin
  ---@return FylerUi
  __call = function(_, win)
    assert(win, "win is required")

    return setmetatable({ win = win }, Ui)
  end,
})

---@param align FylerUiLineAlign
---@param line string
---@return string
local function get_margin(width, align, line)
  if align == "center" then
    return string.rep(" ", math.floor((width - #line) * 0.5))
  elseif align == "end" then
    return string.rep(" ", math.floor((width - #line)))
  else
    return ""
  end
end

---@param ui_lines FylerUiLine[]
function Ui:_render(ui_lines)
  if not self.win:has_valid_bufnr() then
    return
  end

  local was_modifiable = vim.bo[self.win.bufnr].modifiable
  local win_width = api.nvim_win_get_width(self.win.winid)
  local buf_lines = {}

  vim.bo[self.win.bufnr].modifiable = true

  for _, line in ipairs(ui_lines) do
    local line_text = table.concat(vim.tbl_map(function(word)
      return word.str
    end, line.words))

    local margin = get_margin(win_width, line.align, line_text)

    table.insert(line.words, 1, {
      str = margin,
    })

    table.insert(buf_lines, margin .. line_text)
  end

  api.nvim_buf_set_lines(self.win.bufnr, 0, -1, false, buf_lines)
  api.nvim_buf_clear_namespace(self.win.bufnr, self.win.namespace, 0, -1)

  for i, line in ipairs(ui_lines) do
    local offset = 0

    for _, word in ipairs(line.words) do
      api.nvim_buf_set_extmark(self.win.bufnr, self.win.namespace, i - 1, offset, {
        end_col = offset + #word.str,
        hl_group = word.hl or "",
      })

      offset = offset + #word.str
    end

    for _, mark in pairs(line.marks) do
      api.nvim_buf_set_extmark(self.win.bufnr, self.win.namespace, i - 1, 0, {
        virt_text = { { mark.str, mark.hl or "" } },
        virt_text_pos = "eol",
        hl_mode = "combine",
      })
    end
  end

  if not was_modifiable then
    vim.bo[self.win.bufnr].modifiable = false
  end

  vim.bo[self.win.bufnr].modified = false
end

function Ui:render(opts)
  opts = opts or {}

  vim.schedule(function()
    self:_render(opts.ui_lines)
    if opts.on_render then
      opts.on_render()
    end
  end)
end

return M

local a = require("fyler.lib.async")
local fs = require("fyler.lib.fs")
local store = require("fyler.views.explorer.store")

---@class FylerFSItem
---@field meta string
---@field open boolean
---@field children FylerFSItem[]
local FSItem = {}
FSItem.__index = FSItem

local M = setmetatable({}, {
  ---@param meta string
  ---@return FylerFSItem
  __call = function(_, meta)
    local instance = {
      meta = meta,
      open = false,
      children = {},
    }

    return setmetatable(instance, FSItem)
  end,
})

function FSItem:toggle()
  self.open = not self.open
end

---@param addr string
---@param meta string
function FSItem:add_child(addr, meta)
  local target_node = self:find(addr)
  if target_node then
    table.insert(target_node.children, M(meta))
  end
end

---@param addr string
---@return FylerFSItem?
function FSItem:find(addr)
  if self.meta == addr then
    return self
  end

  for _, child in ipairs(self.children) do
    local found = child:find(addr)
    if found then
      return found
    end
  end

  return nil
end

FSItem.update = a.async(function(self, cb)
  if not self.open then
    return cb()
  end

  local meta_data = store.get(self.meta)
  local err, items = a.await(fs.ls, meta_data.path)
  if err then
    return cb()
  end

  self.children = vim
    .iter(self.children)
    :filter(function(child) ---@param child FylerFSItem
      return vim.iter(items):any(function(item)
        return item.path == store.get(child.meta).path and item.type == store.get(child.meta).type
      end)
    end)
    :totable()

  for _, item in ipairs(items) do
    if
      not vim.iter(self.children):any(function(child) ---@param child FylerFSItem
        return store.get(child.meta).path == item.path and store.get(child.meta).type == item.type
      end)
    then
      self:add_child(self.meta, store.set(item))
    end
  end

  for _, child in ipairs(self.children) do
    a.await(child.update, child)
  end

  return cb()
end)

return M

local FSItem = require("fyler.views.explorer.struct")
local Win = require("fyler.lib.win")
local a = require("fyler.lib.async")
local config = require("fyler.config")
local fs = require("fyler.lib.fs")
local store = require("fyler.views.explorer.store")

local fn = vim.fn
local api = vim.api

---@class FylerExplorerView
---@field cwd     string      - Directory path which act as a root
---@field fs_root FylerFSItem - Root |FylerFSItem| instance
---@field win     FylerWin    - Window instance
local ExplorerView = {}
ExplorerView.__index = ExplorerView

function ExplorerView.new(opts)
  local fs_root = FSItem(store.set {
    name = fn.fnamemodify(opts.cwd, ":t"),
    type = "directory",
    path = opts.cwd,
  })

  fs_root:toggle()

  local instance = {
    cwd = opts.cwd,
    fs_root = fs_root,
    kind = opts.kind,
  }

  setmetatable(instance, ExplorerView)

  return instance
end

ExplorerView.open = a.async(function(self, opts)
  local mappings = config.get_reverse_mappings("explorer")
  local view = config.get_view("explorer")

  -- stylua: ignore start
  self.win = Win {
    border   = view.border,
    buf_opts = view.buf_opts,
    bufname  = string.format("fyler://%s", self.cwd),
    enter    = opts.enter,
    height   = view.height,
    kind     = opts.kind or view.kind,
    name     = "explorer",
    width    = view.width,
    win_opts = view.win_opts,
    mappings = {
      n = {
        [mappings["Select"]]    = self:_action("n_select"),
        [mappings["CloseView"]] = self:_action("n_close_view"),
      },
    },
    autocmds = {
      ["BufReadCmd"]   = self:_action("refreshview"),
      ["BufWriteCmd"]  = self:_action("synchronize"),
      ["CursorMoved"]  = self:_action("constrain_cursor"),
      ["CursorMovedI"] = self:_action("constrain_cursor"),
      ["WinClosed"]    = self:_action("n_close_view"),
    },
    user_autocmds = {
      ["RefreshView"] = self:_action("refreshview"),
      ["Synchronize"] = self:_action("synchronize"),
    },
    render = self:_action("refreshview"),
  }
  -- stylua: ignore end

  require("fyler.cache").set_entry("recent_win", api.nvim_get_current_win())

  self.win:show()
end)

---@param ... any
function ExplorerView:_action(name, ...)
  local action = require("fyler.views.explorer.actions")[name]

  assert(action, string.format("%s action is not available", name))

  return action(self, ...)
end

local M = {
  instance = nil, ---@type FylerExplorerView
}

---@param opts { cwd?: string, kind?: FylerWinKind }
---@return FylerExplorerView
function M.get_instance(opts)
  if (not M.instance) or (M.instance.cwd ~= opts.cwd) then
    M.instance = ExplorerView.new {
      cwd = opts.cwd,
      kind = opts.kind,
    }
  end

  return M.instance
end

---@param opts? { enter?: boolean, cwd?: string, kind?: FylerWinKind }
function M.open(opts)
  opts = opts or {}
  opts.enter = opts.enter == nil and true or opts.enter
  opts.cwd = opts.cwd or fs.getcwd()
  opts.kind = opts.kind or config.get_view("explorer").kind

  if M.instance and fn.winbufnr(M.instance.win.winid) == M.instance.win.bufnr then
    M.instance.win:hide()
  end

  M.get_instance(opts):open(opts)
end

return M

---@alias FylerUiLineAlign
---| "end"
---| "start"
---| "center"

---@alias FylerUiWord { str: string, hl?: string }
---@alias FylerUiMark { str: string, hl?: string, id: string }

---@class FylerUiLine
---@field align FylerUiLineAlign
---@field words FylerUiWord[]
---@field marks FylerUiMark[]
local Line = {}
Line.__index = Line

return {
  Line = setmetatable({}, {
    ---@param opts { words: FylerUiWord[], marks: FylerUiMark[], align?: FylerUiLineAlign }
    ---@return FylerUiLine
    __call = function(_, opts)
      local instance = {
        words = opts.words or {},
        align = opts.align or "start",
        marks = opts.marks or {},
      }

      setmetatable(instance, Line)

      return instance
    end,
  }),
}

local api = vim.api

local M = {}

---@param dec integer
local function to_hex(dec)
  return string.format("%06X", math.max(0, math.min(0xFFFFFF, math.floor(dec))))
end

-- https://github.com/NeogitOrg/neogit
---@param name string
---@return string|nil
local function get_fg(name)
  local color = api.nvim_get_hl(0, { name = name })
  if color["link"] then
    return get_fg(color["link"])
  elseif color["reverse"] and color["bg"] then
    return "#" .. to_hex(color["bg"])
  elseif color["fg"] then
    return "#" .. to_hex(color["fg"])
  end
end

-- https://github.com/NeogitOrg/neogit
---@param name string
---@return string|nil
local function get_bg(name)
  local color = api.nvim_get_hl(0, { name = name })
  if color["link"] then
    return get_bg(color["link"])
  elseif color["reverse"] and color["fg"] then
    return "#" .. to_hex(color["fg"])
  elseif color["bg"] then
    return "#" .. to_hex(color["bg"])
  end
end

local function build_palette()
  -- stylua: ignore start
  return {
    black     = "#000000",
    white     = "#ffffff",

    bg        = get_bg("Normal"),
    blue      = get_fg("Directory"),
    cyan      = get_fg("Operator"),
    dark_grey = get_fg("WhiteSpace"),
    fg        = get_fg("Normal"),
    green     = get_fg("String"),
    grey      = get_fg("Comment"),
    orange    = get_fg("SpecialChar"),
    red       = get_fg("Error"),
    yellow    = get_fg("WarningMsg"),
  }
  -- stylua: ignore end
end

---@param opts? FylerConfig
function M.setup(opts)
  opts = opts or {}

  local palette = build_palette()

  -- stylua: ignore start
  local hl_groups = {
    FylerConfirmGreen  = { fg = palette.green },
    FylerConfirmGrey   = { fg = palette.grey },
    FylerConfirmRed    = { fg = palette.red },
    FylerConfirmYellow = { fg = palette.yellow },
    FylerFSDirectory   = { fg = palette.blue },
    FylerFSFile        = { fg = palette.white },
    FylerFSLink        = { fg = palette.grey },
    FylerGitAdded      = { fg = palette.green },
    FylerGitConflict   = { fg = palette.red },
    FylerGitDeleted    = { fg = palette.red },
    FylerGitIgnored    = { fg = palette.red },
    FylerGitModified   = { fg = palette.yellow },
    FylerGitRenamed    = { fg = palette.yellow },
    FylerGitStaged     = { fg = palette.green },
    FylerGitUnstaged   = { fg = palette.orange },
    FylerGitUntracked  = { fg = palette.cyan },
    FylerIndentMarker  = { fg = palette.dark_grey },
  }
  -- stylua: ignore end

  if opts.on_highlights then
    opts.on_highlights(hl_groups, palette)
  end

  for key, val in pairs(hl_groups) do
    api.nvim_set_hl(0, key, val)
  end
end

return M

local a = require("fyler.lib.async")
local fs = require("fyler.lib.fs")

local M = {}

---@param cb fun(result: boolean)
local is_git_repo = a.async(function(cb)
  local out = a.await(vim.system, { "git", "rev-parse", "--is-inside-work-tree" }, nil) ---@type vim.SystemCompleted
  return cb(string.match(out.stdout, "^(.*)\n$") == "true")
end)

---@param cb fun(status?: string)
local git_status = a.async(function(cb)
  if not a.await(is_git_repo) then
    return cb(nil)
  end

  local out = a.await(vim.system, { "git", "status", "--porcelain", "-z" }, nil) ---@type vim.SystemCompleted
  if not out.stdout then
    return cb(nil)
  end

  return cb(out.stdout)
end)

---@param cb fun(path?: string)
local git_toplevel = a.async(function(cb)
  local out = a.await(vim.system, { "git", "rev-parse", "--show-toplevel" }, nil) ---@type vim.SystemCompleted
  if not out.stdout then
    return cb(nil)
  end

  return cb(string.match(out.stdout, "^(.*)\n$"))
end)

---@param cb fun(status_map?: table)
M.status_map = a.async(function(cb)
  local status_str = a.await(git_status)
  if not status_str then
    return cb(nil)
  end

  local statuses = vim
    .iter(vim.split(status_str, "\0"))
    :filter(function(status)
      if status == "" then
        return false
      end

      return true
    end)
    :totable()

  local toplevel = a.await(git_toplevel)
  if not toplevel then
    return cb(nil)
  end

  local status_map = {}
  for _, status in ipairs(statuses) do
    local symbol, path = (function()
      if string.match(status, "^.*%s.*$") then
        return string.match(status, "^(.*)%s(.*)$")
      else
        return nil, status
      end
    end)()

    status_map[fs.joinpath(toplevel, fs.normalize(path))] = symbol
  end

  return cb(status_map)
end)

return M

local M = {}

local did_setup = false

local api = vim.api
local fn = vim.fn

---@param opts FylerConfig
function M.setup(opts)
  if fn.has("nvim-0.11") ~= 1 then
    return vim.notify("Fyler requires at least NVIM 0.11")
  end

  if did_setup then
    return
  end

  require("fyler.config").setup(opts)
  require("fyler.lib.hls").setup(require("fyler.config"))
  require("fyler.autocmds").setup(require("fyler.config"))

  if require("fyler.config").values.default_explorer then
    vim.g.loaded_netrw = 1
    vim.g.loaded_netrwPlugin = 1
    if fn.exists("#FileExplorer") then
      api.nvim_create_augroup("FileExplorer", { clear = true })
    end

    if vim.v.vim_did_enter == 0 then
      local arg_path = fn.argv(0)
      local first_arg = type(arg_path) == "string" and arg_path or arg_path[1]
      if fn.isdirectory(first_arg) == 0 then
        return
      end

      local current_bufnr = api.nvim_get_current_buf()
      if api.nvim_buf_is_valid(current_bufnr) then
        api.nvim_buf_delete(current_bufnr, { force = true })
      end

      M.open { cwd = arg_path }
    end
  end

  did_setup = true
end

M.open = vim.schedule_wrap(function(opts)
  opts = opts or {}

  require("fyler.views.explorer").open {
    cwd = opts.cwd,
    kind = opts.kind,
  }
end)

return M

---@class FylerLinkedList
---@field node FylerLinkedListNode
local LinkedList = {}
LinkedList.__index = LinkedList

---@class FylerLinkedListNode
---@field next? FylerLinkedListNode
---@field data  any
local LinkedListNode = {}
LinkedListNode.__index = LinkedListNode

---@return integer
function LinkedList:len()
  local count = 0
  local current = self.node
  while current do
    count = count + 1
    current = current.next
  end

  return count
end

---@param fn fun(node: FylerLinkedListNode)
function LinkedList:each(fn)
  local start = self.node
  while start do
    fn(start.data)
    start = start.next
  end
end

---@param pos integer
---@param data any
function LinkedList:insert(pos, data)
  local newNode = setmetatable({ data = data }, LinkedListNode)
  if pos == 1 then
    newNode.next = self.node
    self.node = newNode
    return
  end

  local start = self.node
  for _ = 1, pos - 2 do
    if not start then
      error("position is out of bound")
    end
    start = start.next
  end

  if not start then
    error("position is out of bound")
  end

  newNode.next = start.next
  start.next = newNode
end

---@param pos integer
function LinkedList:erase(pos)
  assert(pos >= 1, "position must be 1 or greater")

  if not self.node then
    error("list is empty")
  end

  if pos == 1 then
    self.node = self.node.next
    return
  end

  local start = self.node
  for _ = 1, pos - 2 do
    if not start or not start.next then
      error("position is out of bound")
    end

    start = start.next
  end

  if not start or not start.next then
    error("position is out of bound")
  end

  start.next = start.next.next
end

---@return table
function LinkedList:totable()
  local tbl = {}
  self:each(function(item)
    table.insert(tbl, item)
  end)

  return tbl
end

return setmetatable({}, {
  ---@return FylerLinkedList
  __call = function()
    return setmetatable({}, LinkedList)
  end,
})

vim.api.nvim_create_user_command("Fyler", function(args)
  local opts = {}
  for _, farg in ipairs(args.fargs) do
    local key, value = unpack(vim.split(farg, "="))
    opts[key] = value
  end

  require("fyler").open(opts)
end, {
  nargs = "*",
  complete = function(arglead, cmdline)
    if arglead:find("^kind=") then
      return {
        "kind=float",
        "kind=split:left",
        "kind=split:above",
        "kind=split:right",
        "kind=split:below",
        "kind=split:leftmost",
        "kind=split:abovemost",
        "kind=split:rightmost",
        "kind=split:belowmost",
      }
    end

    if arglead:find("^cwd=") then
      return {
        "cwd=" .. (vim.uv or vim.loop).cwd(),
      }
    end

    return vim.tbl_filter(function(arg)
      return cmdline:match(arg) == nil
    end, {
      "kind=",
      "cwd=",
    })
  end,
})

local fs = require("fyler.lib.fs")
local regex = require("fyler.views.explorer.regex")
local store = require("fyler.views.explorer.store")
local M = {}

local api = vim.api

---@param view FylerExplorerView
---@return table
function M.tree_table_from_node(view)
  ---@param node FylerFSItem
  local function get_tbl(node)
    local sub_tbl = store.get(node.meta)
    sub_tbl.meta = node.meta

    if sub_tbl:is_directory() then
      sub_tbl.children = {}
    end

    if not node.open then
      return sub_tbl
    end

    for _, child in ipairs(node.children) do
      table.insert(sub_tbl.children, get_tbl(child))
    end

    return sub_tbl
  end

  return get_tbl(view.fs_root)
end

---@param view FylerExplorerView
---@return table
function M.tree_table_from_buffer(view)
  if not view.win:has_valid_bufnr() then
    return {}
  end

  local buf_lines = vim
    .iter(api.nvim_buf_get_lines(view.win.bufnr, 0, -1, false))
    :filter(function(buf_line)
      return buf_line ~= ""
    end)
    :totable()

  local root = vim.tbl_deep_extend("force", store.get(view.fs_root.meta), {
    meta = view.fs_root.meta,
    children = {},
  })

  local stack = {
    { node = root, indent = -1 },
  }

  for _, buf_line in ipairs(buf_lines) do
    local meta = regex.match_meta(buf_line)
    local name = regex.match_name(buf_line)
    local indent = regex.match_indent(buf_line)
    local metadata = meta and store.get(meta)

    while #stack > 1 and #stack[#stack].indent >= #indent do
      table.remove(stack)
    end

    local parent = stack[#stack].node
    local path = fs.joinpath(parent.path, name)
    local new_node = {
      meta = meta,
      name = name,
      type = (metadata or {}).type or "",
      path = path,
    }

    table.insert(parent.children, new_node)
    if metadata and metadata:is_directory() then
      new_node.children = {}
      table.insert(stack, { node = new_node, indent = indent })
    end
  end

  return root
end

---@param view FylerExplorerView
---@return table
function M.get_diff(view)
  local recent_tree_hash = {}

  local function save_hash(root)
    recent_tree_hash[root.meta] = root.path
    for _, child in ipairs(root.children or {}) do
      save_hash(child)
    end
  end

  save_hash(M.tree_table_from_node(view))

  local fs_actions = {
    create = {},
    delete = {},
    move = {},
  }

  local function calculate_fs_actions(root)
    if not root.meta then
      table.insert(fs_actions.create, root.path)
    else
      if recent_tree_hash[root.meta] ~= root.path then
        table.insert(fs_actions.move, { src = recent_tree_hash[root.meta], dst = root.path })
      end

      recent_tree_hash[root.meta] = nil
    end

    for _, child in ipairs(root.children or {}) do
      calculate_fs_actions(child)
    end
  end

  calculate_fs_actions(M.tree_table_from_buffer(view))

  for _, v in pairs(recent_tree_hash) do
    if v then
      table.insert(fs_actions.delete, v)
    end
  end

  return fs_actions
end

return M

local M = {}

local count = 0
local store = {}

local Metadata = {}
Metadata.__index = Metadata

function Metadata:is_directory()
  return self.type == "directory" or (self.type == "link" and self.link_type == "directory")
end

function Metadata:resolved_path()
  return self.type == "link" and self.link_path or self.path
end

---@param key string
function M.get(key)
  return setmetatable(vim.deepcopy(store[key]), Metadata)
end

---@param tbl table
---@return string
function M.set(tbl)
  count = count + 1
  local meta = string.format("%05d", count)
  store[meta] = tbl
  return meta
end

function M.debug()
  for k, v in pairs(store) do
    print(k, vim.inspect(v))
  end
end

return M

local List = require("fyler.lib.structs.list")

---@class FylerStack
---@field items FylerLinkedList
local Stack = {}
Stack.__index = Stack

---@param data any
function Stack:push(data)
  self.items:insert(1, data)
end

function Stack:pop()
  assert(not self:is_empty(), "stack is empty")

  local data = self.items.node.data
  self.items:erase(1)
  return data
end

---@return any
function Stack:top()
  assert(self.items.node, "stack is empty")
  return self.items.node.data
end

---@return boolean
function Stack:is_empty()
  return self.items:len() == 0
end

return setmetatable({}, {
  ---@return FylerStack
  __call = function()
    return setmetatable({ items = List() }, Stack)
  end,
})

local List = require("fyler.lib.structs.list")

---@class FylerQueue
---@field items FylerLinkedList
local Queue = {}
Queue.__index = Queue

---@param data any
function Queue:enqueue(data)
  self.items:insert(self.items:len() + 1, data)
end

---@return any
function Queue:dequeue()
  assert(not self:is_empty(), "Queue is empty")

  local data = self.items.node.data
  self.items:erase(1)
  return data
end

---@return any
function Queue:front()
  assert(self.items.node, "Queue is empty")
  return self.items.node.data
end

---@return boolean
function Queue:is_empty()
  return self.items:len() == 0
end

return setmetatable({}, {
  ---@return FylerQueue
  __call = function()
    return setmetatable({ items = List() }, Queue)
  end,
})

local M = {}

---@param str string
---@return string
function M.match_meta(str)
  assert(str, "str is required")
  return str:match(" /(%d%d%d%d%d)")
end

---@param str string
---@return string
function M.match_indent(str)
  assert(str, "str is required")
  return str:match("^(%s*)")
end

---@param str string
---@return string
function M.match_name(str)
  assert(str, "str is required")
  if str:match("/%d%d%d%d%d") then
    return str:match("/%d%d%d%d%d (.*)$")
  else
    return str:gsub("^%s*", ""):match(".*")
  end
end

return M

local components = require("fyler.lib.ui.components")

local Line = components.Line

local function MESSAGE(tbl)
  local lines = {}
  for _, line in ipairs(tbl or {}) do
    table.insert(lines, Line { words = line })
  end

  return lines
end

local M = {}

---@param tbl table
function M.Confirm(tbl)
  if not tbl then
    return {}
  end

  return MESSAGE(tbl)
end

return M

if vim.b.current_syntax then
  return
end

vim.cmd([[
  syntax match FylerMetaKey /\/\d\d\d\d\d / conceal
]])

vim.b.current_syntax = "fyler"

local M = {}

local cache_table = {}

---@param entry_key string
---@param val any
function M.set_entry(entry_key, val)
  cache_table[entry_key] = val
end

---@param entry_key string
---@return any
function M.get_entry(entry_key)
  return vim.deepcopy(cache_table[entry_key])
end

return M

local M = {}

---@param view table
function M.n_close_view(view, cb)
  return function()
    cb(false)
    view:close()
  end
end

function M.n_confirm(view, cb)
  return function()
    cb(true)
    view:close()
  end
end

function M.n_discard(view, cb)
  return function()
    cb(false)
    view:close()
  end
end

return M

This code segment functions to: Dynamically resolve external icon handling modules based on lookup keys, implement error resilience for module loading failures, and provide a fallback mechanism to default icon assignment when custom providers encounter execution issues.

```lua
[TASK_BEGIN]
{
  __index = {},
  __call = function(cls, ...)
    local self = setmetatable({}, { __index = cls })
    if cls.initialize then
      cls.initialize(self, ...)
    end
    return self
  end
}
[TASK_END]
```