local M = {}

--- Checks if the current buffer is modified.
--
-- Returns:
-- bool: True if the buffer is modified, False otherwise.
--
local function is_buffer_modified()
  local buf = vim.api.nvim_get_current_buf()
  return vim.api.nvim_buf_get_option(buf, "modified")
end

--- Switch to an existing buffer in Neovim.
--
-- This function checks if a buffer is already open in a window. If it is, it
-- switches to that window. If it is not, it switches to the existing buffer.
--
-- @param new_window (boolean) Whether to open the buffer in a new window. If
--   true, the buffer will be opened in a new split window. If false, the buffer
--   will be opened in the current window.
-- @param buffer_num (number) The buffer number of the buffer.
--
-- @return None
--
local function switch_to_existing_buffer(new_window, buffer_num)
  -- Check if the buffer number is valid
  if type(buffer_num) ~= "number" or buffer_num < 1 then
    error("Invalid buffer number provided to switch_to_existing_buffer")
    return
  end

  -- See if this buffer is already open in a window
  local window_num = vim.fn.bufwinnr(buffer_num)

  if window_num ~= -1 then
    -- Switch to that window (even if we're already there)
    vim.cmd(window_num .. " wincmd w")
  else
    -- Switch to the existing scratch buffer
    local cmd = new_window and "split +buffer " or "buffer "
    vim.cmd(cmd .. buffer_num)
  end
end

--- Creates a new scratch buffer in Neovim.
--
-- This function creates a new scratch buffer in Neovim, either in a new window
-- or by editing the current buffer. The buffer is configured to work like a
-- scratch buffer, meaning it is not backed by a file and has specific options
-- set.
--
-- @param new_window (boolean) Whether to create the scratch buffer in a new
--   window. If true, the buffer will be created in a new window. If false, the
--   buffer will be created by editing the current buffer.
-- @param buffer_name (string) The name of the buffer.
--
-- @return None
--
local function create_new_scratch_buffer(new_window, buffer_name)
  -- Check if the buffer name is set
  if not buffer_name or type(buffer_name) ~= "string" or string.len(buffer_name) == 0 then
    error("Buffer name not set or invalid in create_new_scratch_buffer")
    return
  end

  -- Either new (for split) or edit the new buffer with buffer_name
  local cmd = new_window and "new " or "edit "
  vim.cmd(cmd .. buffer_name)

  -- Sets the options for the buffer to work like a scratch buffer.
  local buf = vim.api.nvim_get_current_buf() -- should always be 0
  vim.api.nvim_buf_set_option(buf, "bufhidden", "hide") -- what to do when the buffer is hidden
  vim.api.nvim_buf_set_option(buf, "buflisted", true) -- include the buffer in the :bnext list
  vim.api.nvim_buf_set_option(buf, "buftype", "nofile") -- nofile means the buffer isn't backed by a file and we control its name
  vim.api.nvim_buf_set_option(buf, "swapfile", false) -- never swapfiles
end

--- Opens or splits a window based on the specified conditions.
--
-- This function checks if a new window needs to be opened or if it should
-- switch to an existing buffer. It first determines if a new window is
-- required based on the input parameter or if the current buffer is modified.
-- Then, it searches for an existing scratch buffer. If found, it switches to
-- that buffer. Otherwise, it creates a new scratch buffer.
--
-- @param new_window A boolean indicating whether a new window should be created. If not provided,
--                   it defaults to the state of the buffer's modification (true if modified).
--
-- @return None
--
local function open_or_split(new_window)
  -- Create new window if requested or if buffer is modified
  new_window = new_window or is_buffer_modified()

  -- Look for an existing scratch buffer
  local scratch_buffer_num = vim.fn.bufnr(M.config.buffer_name)
  if scratch_buffer_num ~= -1 then
    -- Found it, switch to it
    switch_to_existing_buffer(new_window, scratch_buffer_num)
  else
    -- Didn't find it, create one
    create_new_scratch_buffer(new_window, M.config.buffer_name)
  end
end

--- Splits the current window and opens or switches to the scratch buffer.
--
-- This function is designed to be a public interface for opening or switching
-- to the scratch buffer in a new split window. It internally calls the
-- `open_or_split` function with `true` to indicate the need for a split window.
--
-- Usage:
-- :lua require('scratch').split()
--
function M.split()
  open_or_split(true)
end

--- Opens or switches to the scratch buffer in the current window.
--
-- This function is a public interface for opening or switching to the scratch
-- buffer in the current window without splitting. It calls the `open_or_split`
-- function with `false`, indicating no new window is needed.
--
-- Usage:
-- :lua require('scratch').open()
--
function M.open()
  open_or_split(false)
end

--- Sets up the scratch buffer plugin with optional configuration.
--
-- This function initializes the scratch buffer plugin and binds Neovim
-- commands to the plugin's functionalities. It also allows for optional
-- configuration through the `opts` parameter.
--
-- @param opts (table) Optional configuration table. It can contain a key
--   `buffer_name` to specify the name of the scratch buffer. If not provided,
--   defaults to "_SCRATCH_". An error is raised if `buffer_name` is provided but
--   is not a string.
--
-- Usage:
-- :lua require('scratch').setup({ buffer_name = "my_scratch" })
--
function M.setup(opts)
  -- Default configuration
  M.config = {
    buffer_name = "_SCRATCH_",
  }

  -- Check for configuration overrides passed in via opts
  if opts ~= nil and type(opts) == "table" then
    if
      opts.buffer_name and (type(opts.buffer_name) ~= "string" or string.len(opts.buffer_name) == 0)
    then
      error("Invalid buffer name provided in setup options")
      return
    end
    M.config.buffer_name = opts.buffer_name or M.config.buffer_name
  end

  -- Bind commands to our lua functions
  vim.api.nvim_create_user_command("Scratch", M.open, {})
  vim.api.nvim_create_user_command("ScratchSplit", M.split, {})
end

return M
