local conf = require("telescope.config").values
local Path = require "plenary.path"
local utils = require "telescope.utils"

local uv = vim.loop

---@tag telescope.actions.history
---@config { ["module"] = "telescope.actions.history" }

---@brief [[
--- A base implementation of a prompt history that provides a simple history
--- and can be replaced with a custom implementation.
---
--- For example: We provide an extension for a smart history that uses sql.nvim
--- to map histories to metadata, like the calling picker or cwd.
---
--- So you have a history for:
--- - find_files  project_1
--- - grep_string project_1
--- - live_grep   project_1
--- - find_files  project_2
--- - grep_string project_2
--- - live_grep   project_2
--- - etc
---
--- See https://github.com/nvim-telescope/telescope-smart-history.nvim
---@brief ]]

-- TODO(conni2461): currently not present in plenary path only sync.
-- But sync is just unnecessary here
local write_async = function(path, txt, flag)
  uv.fs_open(path, flag, 438, function(open_err, fd)
    assert(not open_err, open_err)
    uv.fs_write(fd, txt, -1, function(write_err)
      assert(not write_err, write_err)
      uv.fs_close(fd, function(close_err)
        assert(not close_err, close_err)
      end)
    end)
  end)
end

local append_async = function(path, txt)
  write_async(path, txt, "a")
end

local histories = {}

--- Manages prompt history
---@class History @Manages prompt history
---@field enabled boolean: Will indicate if History is enabled or disabled
---@field path string: Will point to the location of the history file
---@field limit string: Will have the limit of the history. Can be nil, if limit is disabled.
---@field content table: History table. Needs to be filled by your own History implementation
---@field index number: Used to keep track of the next or previous index. Default is #content + 1
---@field cycle_wrap boolean: Controls if history will wrap on reaching beginning or end
histories.History = {}
histories.History.__index = histories.History

--- Create a new History
---@param opts table: Defines the behavior of History
---@field init function: Will be called after handling configuration (required)
---@field append function: How to append a new prompt item (required)
---@field reset function: What happens on reset. Will be called when telescope closes (required)
---@field pre_get function: Will be called before a next or previous item will be returned (optional)
function histories.History:new(opts)
  local obj = {}
  if conf.history == false or type(conf.history) ~= "table" then
    obj.enabled = false
    return setmetatable(obj, self)
  end
  obj.enabled = true
  if conf.history.limit then
    obj.limit = conf.history.limit
  end
  obj.path = utils.path_expand(conf.history.path)
  obj.content = {}
  obj.index = 1
  obj.cycle_wrap = conf.history.cycle_wrap

  opts.init(obj)
  obj._reset = opts.reset
  obj._append = opts.append
  obj._pre_get = opts.pre_get

  return setmetatable(obj, self)
end

--- Shorthand to create a new history
function histories.new(...)
  return histories.History:new(...)
end

--- Will reset the history index to the default initial state. Will happen after the picker closed
function histories.History:reset()
  if not self.enabled then
    return
  end
  self._reset(self)
end

--- Append a new line to the history
---@param line string: current line that will be appended
---@param picker table: the current picker object
---@param no_reset boolean: On default it will reset the state at the end. If you don't want to do this set to true
function histories.History:append(line, picker, no_reset)
  if not self.enabled then
    return
  end
  self._append(self, line, picker, no_reset)
end

--- Will return the next history item. Can be nil if there are no next items
---@param line string: the current line
---@param picker table: the current picker object
---@return string: the next history item
function histories.History:get_next(line, picker)
  if not self.enabled then
    utils.notify("History:get_next", {
      msg = "You are cycling to next the history item but history is disabled. Read ':help telescope.defaults.history'",
      level = "WARN",
    })
    return false
  end
  if self._pre_get then
    self._pre_get(self, line, picker)
  end

  local next_idx = self.index + 1
  if next_idx > #self.content and self.cycle_wrap then
    next_idx = 1
  end

  if next_idx <= #self.content then
    self.index = next_idx
    return self.content[next_idx]
  end
  self.index = #self.content + 1
  return nil
end

--- Will return the previous history item. Can be nil if there are no previous items
---@param line string: the current line
---@param picker table: the current picker object
---@return string: the previous history item
function histories.History:get_prev(line, picker)
  if not self.enabled then
    utils.notify("History:get_prev", {
      msg = "You are cycling to next the history item but history is disabled. Read ':help telescope.defaults.history'",
      level = "WARN",
    })
    return false
  end
  if self._pre_get then
    self._pre_get(self, line, picker)
  end

  local next_idx = self.index - 1
  if next_idx < 1 and self.cycle_wrap then
    next_idx = #self.content
  end

  if self.index == #self.content + 1 then
    if line ~= "" then
      self:append(line, picker, true)
    end
  end
  if next_idx >= 1 then
    self.index = next_idx
    return self.content[next_idx]
  end
  return nil
end

--- A simple implementation of history.
---
--- It will keep one unified history across all pickers.
histories.get_simple_history = function()
  return histories.new {
    init = function(obj)
      local p = Path:new(obj.path)
      if not p:exists() then
        p:touch { parents = true }
      end

      obj.content = Path:new(obj.path):readlines()
      obj.index = #obj.content
      table.remove(obj.content, obj.index)
    end,
    reset = function(self)
      self.index = #self.content + 1
    end,
    append = function(self, line, _, no_reset)
      if line ~= "" then
        if self.content[#self.content] ~= line then
          table.insert(self.content, line)

          local len = #self.content
          if self.limit and len > self.limit then
            local diff = len - self.limit
            for i = diff, 1, -1 do
              table.remove(self.content, i)
            end
            write_async(self.path, table.concat(self.content, "\n") .. "\n", "w")
          else
            append_async(self.path, line .. "\n")
          end
        end
      end
      if not no_reset then
        self:reset()
      end
    end,
  }
end

return histories
