if not pcall(require, "el") then
  return
end

local Job = require "plenary.job"
local el_sub = require "el.subscribe"

local M = {}

function M.extract_hl(spec)
  if not spec or vim.tbl_isempty(spec) then return end
  local hl_name, hl_opts = { "El" }, {}
  for attr, val in pairs(spec) do
    if type(val) == "table" then
      table.insert(hl_name, attr)
      assert(vim.tbl_count(val) == 1)
      local hl, what = next(val)
      local hlID = vim.fn.hlID(hl)
      if hlID > 0 then
        table.insert(hl_name, hl)
        local col = vim.fn.synIDattr(hlID, what)
        if col and #col > 0 then
          table.insert(hl_name, what)
          hl_opts[attr] = col
        end
      end
    else
      -- bold, underline, etc
      hl_opts[attr] = val
    end
  end
  hl_name = table.concat(hl_name, "_")
  -- if highlight exists, verify it has
  -- the correct colorscheme highlights
  local newID = vim.fn.hlID(hl_name)
  if newID > 0 then
    for what, expected in pairs(hl_opts) do
      local res = vim.fn.synIDattr(newID, what)
      if type(expected) == "boolean" then
        -- synIDattr returns '1' for boolean
        res = res and res == "1" and true
      end
      if res ~= expected then
        -- need to regen the highlight
        -- print("color mismatch", hl_name, what, "e:", expected, "c:", res)
        newID = 0
      end
    end
  end
  if newID == 0 then
    vim.api.nvim_set_hl(0, hl_name, hl_opts)
  end
  return hl_name
end

local function set_hl(hls, s)
  if not hls or not s then return s end
  hls = type(hls) == "string" and { hls } or hls
  for _, hl in ipairs(hls) do
    if vim.fn.hlID(hl) > 0 then
      return ("%%#%s#%s%%0*"):format(hl, s)
    end
  end
  return s
end

local function wrap_fnc(opts, fn)
  return function(window, buffer)
    -- buf_autocmd doesn't send win
    if not window and buffer then
      window = { win_id = vim.fn.bufwinid(buffer.bufnr) }
    end
    if opts.hide_inactive and window and
        window.win_id ~= vim.api.nvim_get_current_win() then
      return ""
    end
    return fn(window, buffer)
  end
end

M.mode = function(opts)
  opts = opts or {}
  return wrap_fnc(opts, function(_, _)
    local fmt = opts.fmt or "%s%s"
    local mode = vim.api.nvim_get_mode().mode
    local mode_data = opts.modes and opts.modes[mode]
    local hls = mode_data and mode_data[3]
    local icon = opts.hl_icon_only and set_hl(hls, opts.icon) or opts.icon
    mode = mode_data and mode_data[1]:upper() or mode
    mode = (fmt):format(icon or "", mode)
    return not opts.hl_icon_only and set_hl(hls, mode) or mode
  end)
end

M.try_devicons = function()
  if not M._has_devicons then
    M._has_devicons, M._devicons = pcall(require, "nvim-web-devicons")
  end
  return M._devicons
end

M.file_icon = function(opts)
  opts = opts or {}
  return el_sub.buf_autocmd("el_file_icon", "BufRead",
    wrap_fnc(opts, function(_, buffer)
      if not M.try_devicons() then return "" end
      local fmt = opts.fmt or "%s"
      local ext = vim.fn.fnamemodify(buffer.name, ":p:e")
      local icon, hl = M._devicons.get_icon(buffer.name, ext:lower(), { default = true })
      -- local icon = extensions.file_icon(_, bufnr)
      if icon then
        if opts.hl_icon then
          local hlgroup = M.extract_hl({
            bg = { StatusLine = "bg" },
            fg = { [hl] = "fg" },
          })
          icon = set_hl(hlgroup, icon)
        end
        return (fmt):format(icon)
      end
      return ""
    end))
end

M.git_branch = function(opts)
  opts = opts or {}
  return el_sub.buf_autocmd("el_git_branch", "BufEnter",
    wrap_fnc(opts, function(_, buffer)
      -- Try fugitive first as it's most reliable
      local branch = vim.g.loaded_fugitive == 1 and
          vim.fn.FugitiveHead() or nil
      -- buffer can be null and code will crash with:
      -- E5108: Error executing lua ... 'attempt to index a nil value'
      if not buffer or not (buffer.bufnr > 0) then
        return
      end
      -- fugitive is empty or not loaded, try gitsigns
      if not branch or #branch == 0 then
        local ok, res = pcall(vim.api.nvim_buf_get_var,
          buffer.bufnr, "gitsigns_head")
        if ok then branch = res end
      end
      -- last resort run git command
      if not branch then
        local j = Job:new {
          command = "git",
          args = { "branch", "--show-current" },
          cwd = vim.fn.fnamemodify(buffer.name, ":h"),
        }

        local ok, result = pcall(function()
          return vim.trim(j:sync()[1])
        end)
        if ok then
          branch = result
        end
      end

      if branch and #branch > 0 then
        local fmt = opts.fmt or "%s %s"
        local icon = opts.icon or ""
        return set_hl(opts.hl, (fmt):format(icon, branch))
      end
    end))
end

local git_changes_formatter = function(opts)
  local specs = {
    insert = {
      regex = "(%d+) insertions?",
      icon  = opts.icon_insert or "+",
      hl    = opts.hl_insert,
    },
    change = {
      regex = "(%d+) files? changed",
      icon  = opts.icon_change or "~",
      hl    = opts.hl_change,
    },
    delete = {
      regex = "(%d+) deletions?",
      icon  = opts.icon_delete or "-",
      hl    = opts.hl_delete,
    },
  }
  return function(_, _, s)
    local result = {}
    for k, v in pairs(specs) do
      local count = nil
      if type(s) == "string" then
        -- 'git diff --shortstat' output
        -- from 'git_changes_all'
        count = tonumber(string.match(s, v.regex))
      else
        -- map from 'git_changes_buf'
        count = s[k]
      end
      if count and count > 0 then
        table.insert(result, set_hl(v.hl, ("%s%d"):format(v.icon, count)))
      end
    end
    return table.concat(result, ", ")
  end
end

-- requires gitsigns
M.git_changes_buf = function(opts)
  opts = opts or {}
  local formatter = opts.formatter or git_changes_formatter(opts)
  return wrap_fnc(opts, function(window, buffer)
    local stats = {}
    if buffer and buffer.bufnr > 0 then
      local ok, res = pcall(vim.api.nvim_buf_get_var,
        buffer.bufnr, "vgit_status")
      if ok then stats = res end
    end
    if buffer and buffer.bufnr > 0 then
      local ok, res = pcall(vim.api.nvim_buf_get_var,
        buffer.bufnr, "gitsigns_status_dict")
      if ok then stats = res end
    end
    local counts = {
      insert = stats.added > 0 and stats.added or nil,
      change = stats.changed > 0 and stats.changed or nil,
      delete = stats.removed > 0 and stats.removed or nil,
    }
    if not vim.tbl_isempty(counts) then
      local fmt = opts.fmt or "%s"
      local out = formatter(window, buffer, counts)
      return out and fmt:format(out) or nil
    else
      -- el functions that return a
      -- string must not return nil
      return ""
    end
  end)
end

M.git_changes_all = function(opts)
  opts = opts or {}
  local formatter = opts.formatter or git_changes_formatter(opts)
  return el_sub.buf_autocmd("el_git_changes", "BufWritePost",
    wrap_fnc(opts, function(window, buffer)
      if not buffer or
          not (buffer.bufnr > 0) or
          vim.bo[buffer.bufnr].bufhidden ~= "" or
          vim.bo[buffer.bufnr].buftype == "nofile" or
          vim.fn.filereadable(buffer.name) ~= 1 then
        return
      end

      local j = Job:new {
        command = "git",
        args = { "diff", "--shortstat" },
        -- makes no sense to run for one file as
        -- 'file(s) changed' will always be 1
        -- args = { "diff", "--shortstat", buffer.name },
        cwd = vim.fn.fnamemodify(buffer.name, ":h"),
      }

      local ok, git_changes = pcall(function()
        return formatter(window, buffer, vim.trim(j:sync()[1]))
      end)

      if ok then
        local fmt = opts.fmt or "%s"
        return git_changes and fmt:format(git_changes) or nil
      end
    end))
end

local function lsp_srvname(bufnr)
  local buf_clients = vim.lsp.buf_get_clients(bufnr)
  if not buf_clients or #buf_clients == 0 then
    return nil
  end
  local names = ""
  for i, c in ipairs(buf_clients) do
    if i > 1 then names = names .. ", " end
    names = names .. c.name
  end
  return names
end

local function diag_formatter(opts)
  return function(_, buffer, counts)
    local items = {}
    local icons = {
      ["errors"]   = { opts.icon_err or "E", opts.hl_err },
      ["warnings"] = { opts.icon_warn or "W", opts.hl_warn },
      ["infos"]    = { opts.icon_info or "I", opts.hl_info },
      ["hints"]    = { opts.icon_hint or "H", opts.hl_hint },
    }
    for _, k in ipairs({ "errors", "warnings", "infos", "hints" }) do
      if counts[k] > 0 then
        table.insert(items,
          set_hl(icons[k][2], ("%s:%s"):format(icons[k][1], counts[k])))
      end
    end
    local fmt = opts.fmt or "%s"
    local lsp_name = opts.lsp and lsp_srvname(buffer.bufnr)
    if not lsp_name and vim.tbl_isempty(items) then
      return ""
    else
      local contents = lsp_name
      if not vim.tbl_isempty(items) then
        contents = ("%s %s"):format(lsp_name, table.concat(items, " "))
      end
      return fmt:format(contents)
    end
  end
end

local get_buffer_counts = function(diagnostic, _, buffer)
  local counts = { 0, 0, 0, 0 }
  local diags = diagnostic.get(buffer.bufnr)
  if diags and not vim.tbl_isempty(diags) then
    for _, d in ipairs(diags) do
      if tonumber(d.severity) then
        counts[d.severity] = counts[d.severity] + 1
      end
    end
  end
  return {
    errors   = counts[1],
    warnings = counts[2],
    infos    = counts[3],
    hints    = counts[4],
  }
end

M.diagnostics = function(opts)
  opts = opts or {}
  local formatter = opts.formatter or diag_formatter(opts)
  return el_sub.buf_autocmd("el_buf_diagnostic", "LspAttach,DiagnosticChanged",
    wrap_fnc(opts, function(window, buffer)
      return formatter(window, buffer, get_buffer_counts(vim.diagnostic, window, buffer))
    end))
end

return M