local config = require "ivy.config"

-- Constent options that will be used for the keymaps
local opts = { noremap = true, silent = true, nowait = true }

-- All of the base chars that will be used for an "input" operation on the
-- prompt
-- stylua: ignore
local chars = {
    "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W",
    "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
    "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "<", ">", "`", "@", "#", "~", "!",
    "\"", "$", "%", "^", "&", "/", "(", ")", "=", "+", "*", "-", "_", ".", ",", ";", ":", "?", "\\", "|", "'", "{", "}",
    "[", "]", " ",
}

local function string_to_table(lines)
  local matches = {}
  for line in lines:gmatch "[^\r\n]+" do
    table.insert(matches, { content = line })
  end

  return matches
end

local function get_items_length(items)
  local mt = getmetatable(items)
  if mt ~= nil and mt.__len ~= nil then
    return mt.__len(items)
  end

  return #items
end

local function call_gc(items)
  local mt = getmetatable(items)
  if mt ~= nil and mt.__gc ~= nil then
    return mt.__gc(items)
  end
end

local callbacks = {
  destroy = "<cmd>lua vim.ivy.destroy()<CR>",
  clear = "<cmd>lua vim.ivy.search('')<CR>",
  next = "<cmd>lua vim.ivy.next()<CR>",
  previous = "<cmd>lua vim.ivy.previous()<CR>",
  next_checkpoint = "<cmd>lua vim.ivy.next(); vim.ivy.checkpoint()<CR>",
  previous_checkpoint = "<cmd>lua vim.ivy.previous(); vim.ivy.checkpoint()<CR>",

  complete = "<cmd>lua vim.ivy.complete(vim.ivy.action.EDIT)<CR>",
  vsplit = "<cmd>lua vim.ivy.complete(vim.ivy.action.VSPLIT)<CR>",
  split = "<cmd>lua vim.ivy.complete(vim.ivy.action.SPLIT)<CR>",

  backspace = "<cmd>lua vim.ivy.input('BACKSPACE')<CR>",
  left = "<cmd>lua vim.ivy.input('LEFT')<CR>",
  right = "<cmd>lua vim.ivy.input('RIGHT')<CR>",
  delete_word = "<cmd>lua vim.ivy.input('DELETE_WORD')<CR>",
}

local function get_mapping_function(key)
  if type(key) == "function" then
    return key
  end

  if callbacks[key] == nil then
    error("The mapping '" .. key .. "' is not a valid ivy callback")
  end

  return callbacks[key]
end

local window = {}

window.index = 0
window.origin = nil
window.window = nil
window.buffer = nil

window.initialize = function()
  window.make_buffer()
end

window.make_buffer = function()
  window.origin = vim.api.nvim_get_current_win()

  vim.api.nvim_command "botright split new"
  window.buffer = vim.api.nvim_win_get_buf(0)
  window.window = vim.api.nvim_get_current_win()

  vim.api.nvim_win_set_option(window.window, "number", false)
  vim.api.nvim_win_set_option(window.window, "relativenumber", false)
  vim.api.nvim_win_set_option(window.window, "signcolumn", "no")

  vim.api.nvim_buf_set_option(window.buffer, "filetype", "ivy")
  vim.api.nvim_buf_set_var(window.buffer, "bufftype", "nofile")

  for index = 1, #chars do
    local char = chars[index]
    if char == "'" then
      char = "\\'"
    end
    if char == "\\" then
      char = "\\\\\\\\"
    end
    vim.api.nvim_buf_set_keymap(window.buffer, "n", chars[index], "<cmd>lua vim.ivy.input('" .. char .. "')<CR>", opts)
  end

  local mappings = config:get { "mappings" }
  assert(mappings, "The mappings key is missing from the config, something has gone horribly wrong")
  for key, value in pairs(mappings) do
    local callback = get_mapping_function(value)
    vim.keymap.set({ "n" }, key, callback, vim.tbl_extend("force", opts, { buffer = window.buffer }))
  end
end

window.get_current_selection = function()
  local line = vim.api.nvim_buf_get_lines(window.buffer, window.index, window.index + 1, true)
  if line == nil then
    line = { "" }
  end

  return line[1]
end

window.get_buffer = function()
  if window.buffer == nil then
    window.make_buffer()
  end

  return window.buffer
end

window.update = function()
  vim.api.nvim_win_set_cursor(window.window, { window.index + 1, 0 })
end

window.set_items = function(items)
  if type(items) == "string" then
    items = string_to_table(items)
  end

  local items_length = get_items_length(items)

  -- TODO(ade): Validate the items are in the correct format. This also need to
  -- come with some descriptive messages and possible help.

  -- Display no items text if there are no items to dispaly
  if items_length == 0 then
    items_length = 1
    items = { { content = "-- No Items --" } }
  end

  window.index = items_length - 1

  for index = 1, items_length do
    vim.api.nvim_buf_set_lines(window.buffer, index - 1, -1, false, { items[index].content })
  end

  -- Limit the results window size to 10 so when there are lots of results the
  -- window does not take up the hole terminal
  local line_count = items_length
  if line_count > 10 then
    line_count = 10
  end

  vim.api.nvim_win_set_height(window.window, line_count)
  window.update()

  call_gc(items)
end

window.destroy = function()
  if type(window.buffer) == "number" then
    vim.api.nvim_buf_delete(window.buffer, { force = true })
  end

  window.buffer = nil
  window.window = nil
  window.origin = nil
  window.index = 0
end

return window
