---@class snacks.input
---@overload fun(opts: snacks.input.Opts, on_confirm: fun(value?: string)): snacks.win
local M = setmetatable({}, {
  __call = function(M, ...)
    return M.input(...)
  end,
})

M.meta = {
  desc = "Better `vim.ui.input`",
  needs_setup = true,
}

---@alias snacks.input.Pos "left"|"title"|false

---@class snacks.input.Config
---@field enabled? boolean
---@field win? snacks.win.Config|{}
---@field icon? string
---@field icon_pos? snacks.input.Pos
---@field prompt_pos? snacks.input.Pos
local defaults = {
  icon = " ",
  icon_hl = "SnacksInputIcon",
  icon_pos = "left",
  prompt_pos = "title",
  win = { style = "input" },
  expand = true,
}

Snacks.util.set_hl({
  Icon = "DiagnosticHint",
  Normal = "Normal",
  Border = "DiagnosticInfo",
  Title = "DiagnosticInfo",
  Prompt = "SnacksInputTitle",
}, { prefix = "SnacksInput", default = true })

Snacks.config.style("input", {
  backdrop = false,
  position = "float",
  border = "rounded",
  title_pos = "center",
  height = 1,
  width = 60,
  relative = "editor",
  noautocmd = true,
  row = 2,
  -- relative = "cursor",
  -- row = -3,
  -- col = 0,
  wo = {
    winhighlight = "NormalFloat:SnacksInputNormal,FloatBorder:SnacksInputBorder,FloatTitle:SnacksInputTitle",
    cursorline = false,
  },
  bo = {
    filetype = "snacks_input",
    buftype = "prompt",
  },
  --- buffer local variables
  b = {
    completion = false, -- disable blink completions in input
  },
  keys = {
    n_esc = { "<esc>", { "cmp_close", "cancel" }, mode = "n", expr = true },
    i_esc = { "<esc>", { "cmp_close", "stopinsert" }, mode = "i", expr = true },
    i_cr = { "<cr>", { "cmp_accept", "confirm" }, mode = { "i", "n" }, expr = true },
    i_tab = { "<tab>", { "cmp_select_next", "cmp" }, mode = "i", expr = true },
    i_ctrl_w = { "<c-w>", "<c-s-w>", mode = "i", expr = true },
    i_up = { "<up>", { "hist_up" }, mode = { "i", "n" } },
    i_down = { "<down>", { "hist_down" }, mode = { "i", "n" } },
    q = "cancel",
  },
})

local ui_input = vim.ui.input

---@class snacks.input.Opts: snacks.input.Config,{}
---@field prompt? string
---@field default? string
---@field completion? string
---@field highlight? fun()

---@class snacks.input.ctx
---@field opts? snacks.input.Opts
---@field win? snacks.win
local ctx = {}

---@param opts? snacks.input.Opts
---@param on_confirm fun(value?: string)
function M.input(opts, on_confirm)
  assert(type(on_confirm) == "function", "`on_confirm` must be a function")

  local history = require("snacks.picker.util.history").new("input", {
    filter = function(value)
      return value ~= ""
    end,
  })

  local parent_win = vim.api.nvim_get_current_win()
  local mode = vim.fn.mode()

  ---@param force? boolean
  local function record(force)
    if not ctx.win then
      return
    end
    if not force and not history:is_current() then
      return
    end
    local text = vim.trim(ctx.win:text())
    if text == "" then
      return
    end
    history:record(text)
  end

  local function confirm(value)
    record()
    ctx.win = nil
    ctx.opts = nil
    vim.cmd.stopinsert()
    vim.schedule(function()
      if vim.api.nvim_win_is_valid(parent_win) then
        vim.api.nvim_set_current_win(parent_win)
        if mode == "i" then
          vim.cmd("startinsert")
        end
      end
      on_confirm(value)
    end)
  end

  opts = Snacks.config.get("input", defaults, opts) --[[@as snacks.input.Opts]]
  opts.prompt = opts.prompt or "Input"
  opts.prompt = vim.trim(opts.prompt)
  opts.prompt = opts.prompt_pos == "title" and opts.prompt:gsub(":$", "") or opts.prompt

  local title, statuscolumn = {}, {} ---@type string[], string[]
  local function add(text, hl, pos)
    if pos == "title" then
      table.insert(title, { " " .. text, hl })
    else
      table.insert(statuscolumn, "%#" .. hl .. "#" .. text)
    end
  end

  if opts.icon_pos and (opts.icon or "") ~= "" then
    add(opts.icon, "SnacksInputIcon", opts.icon_pos)
  end
  add(opts.prompt, "SnacksInputTitle", opts.prompt_pos)

  if next(title) then
    table.insert(title, { " ", "SnacksInputTitle" })
  end

  ---@param text? string
  local function set(text)
    text = text or ""
    vim.api.nvim_buf_set_lines(ctx.win.buf, 0, -1, false, { text })
    vim.api.nvim_win_set_cursor(ctx.win.win, { 1, #text })
  end

  opts.win = Snacks.win.resolve("input", opts.win, {
    enter = true,
    title = next(title) and title or nil,
    bo = {
      modifiable = true,
      completefunc = "v:lua.Snacks.input.complete",
      omnifunc = "v:lua.Snacks.input.complete",
    },
    wo = {
      statuscolumn = next(statuscolumn) and " " .. table.concat(statuscolumn, " ") .. " " or " ",
    },
    actions = {
      cancel = function(self)
        confirm()
        self:close()
      end,
      stopinsert = function()
        vim.cmd("stopinsert")
      end,
      confirm = function(self)
        confirm(self:text())
        self:close()
      end,
      hist_up = function(self)
        record()
        set(history:prev())
      end,
      hist_down = function(self)
        record()
        set(history:next())
      end,
      cmp = function()
        return vim.fn.pumvisible() == 0 and "<c-x><c-u>"
      end,
      cmp_close = function()
        return vim.fn.pumvisible() == 1 and "<c-e>"
      end,
      cmp_accept = function()
        return vim.fn.pumvisible() == 1 and "<c-y>"
      end,
      cmp_select_next = function()
        return vim.fn.pumvisible() == 1 and "<c-n>"
      end,
      cmp_select_prev = function()
        return vim.fn.pumvisible() == 1 and "<c-p>"
      end,
    },
  })

  local parent_zindex = vim.api.nvim_win_get_config(parent_win).zindex
  opts.win.zindex = parent_zindex and parent_zindex + 1 or opts.win.zindex

  local min_width = opts.win.width or 60
  if opts.expand then
    ---@param self snacks.win
    opts.win.width = function(self)
      local w = type(min_width) == "function" and min_width(self) or min_width --[[@as number]]
      return math.max(w, vim.api.nvim_strwidth(self:text()) + 5)
    end
  end

  local win = Snacks.win(opts.win)
  ctx = { opts = opts, win = win }
  vim.fn.prompt_setprompt(win.buf, "")
  if opts.default then
    vim.api.nvim_buf_set_lines(win.buf, 0, -1, false, { opts.default })
  end

  vim.api.nvim_win_call(win.win, function()
    vim.cmd("startinsert!")
  end)

  vim.fn.prompt_setcallback(win.buf, function(text)
    confirm(text)
    win:close()
  end)
  vim.fn.prompt_setinterrupt(win.buf, function()
    confirm()
    win:close()
  end)

  win:on({ "TextChangedI", "TextChanged" }, function()
    if not win:valid() then
      return
    end
    vim.bo[win.buf].modified = false
    if opts.expand then
      if vim.api.nvim_win_is_valid(parent_win) then
        vim.api.nvim_win_call(parent_win, function()
          win:update()
        end)
      end
      vim.api.nvim_win_call(win.win, function()
        vim.fn.winrestview({ leftcol = 0 })
      end)
    end
  end, { buf = true })
  return win
end

---@param findstart number
---@param base string
---@private
function M.complete(findstart, base)
  local completion = ctx.opts.completion
  if findstart == 1 then
    return 0
  end
  if not completion then
    return {}
  end
  local ok, results = pcall(vim.fn.getcompletion, base, completion)
  return ok and results or {}
end

function M.enable()
  vim.ui.input = M.input
end

function M.disable()
  vim.ui.input = ui_input
end

---@private
function M.health()
  if Snacks.config.get("input", defaults).enabled then
    if vim.ui.input == M.input then
      Snacks.health.ok("`vim.ui.input` is set to `Snacks.input`")
    else
      Snacks.health.error("`vim.ui.input` is not set to `Snacks.input`")
    end
  end
end

return M
