-- Copyright 2019-2022 Mitchell. See LICENSE.

local M = {}

--[[ This comment is for LuaDoc.
---
-- Records buffer positions within Textadept views over time and allows for navigating through
-- that history.
--
-- This module listens for text edit events and buffer switch events. Each time an insertion
-- or deletion occurs, its location is recorded in the current view's location history. If the
-- edit is close enough to the previous record, the previous record is amended. Each time a
-- buffer switch occurs, the before and after locations are also recorded.
-- @field minimum_line_distance (number)
--   The minimum number of lines between distinct history records.
--   The default value is `3`.
-- @field maximum_history_size (number)
--   The maximum number of history records to keep per view.
--   The default value is `100`.
module('textadept.history')]]

M.minimum_line_distance = 3
M.maximum_history_size = 100

-- Map of views to their history records.
-- Each record has a `pos` field that points to the current history position in the associated view.
-- @class table
-- @name view_history
local view_history = setmetatable({}, {
  __index = function(t, view)
    t[view] = {pos = 0}
    return t[view]
  end
})

local INSERT, DELETE = buffer.MOD_INSERTTEXT, buffer.MOD_DELETETEXT
local UNDO, REDO = buffer.PERFORMED_UNDO, buffer.PERFORMED_REDO
-- Listens for text insertion and deletion events and records their locations.
events.connect(events.MODIFIED, function(position, mod, text, length)
  if mod & (INSERT | DELETE) == 0 or buffer.length == (mod & INSERT > 0 and length or 0) then
    return -- ignore non-insertion/deletion, file loading, and replacing buffer contents
  end
  if mod & INSERT > 0 then position = position + length end
  if mod & (UNDO | REDO) > 0 then return end -- ignore undo/redo
  M.record(nil, buffer:line_from_position(position), buffer.column[position])
end)

-- Do not record positions during buffer switches when jumping backwards or forwards.
local jumping = false

-- Jumps to the given record in the current view's history.
-- @param record History record to jump to.
local function jump(record)
  jumping = true
  local filename = record.filename
  if lfs.attributes(filename) then
    io.open_file(filename)
  else
    for _, buffer in ipairs(_BUFFERS) do
      if buffer.filename == filename or buffer._type == filename or
        (not buffer.filename and not buffer._type and filename == _L['Untitled']) then
        view:goto_buffer(buffer)
        break
      end
    end
  end
  buffer:goto_pos(buffer:find_column(record.line, record.column))
  jumping = false
end

---
-- Navigates backwards through the current view's history.
-- @name back
function M.back()
  local history = view_history[view]
  if #history == 0 then return end -- nothing to do
  local record = history[history.pos]
  local line = buffer:line_from_position(buffer.current_pos)
  if buffer.filename ~= record.filename and buffer._type ~= record.filename or
    math.abs(record.line - line) > M.minimum_line_distance then
    -- When navigated away from the most recent record, and if that record is not a soft record,
    -- jump back to it first, then navigate backwards.
    if not record.soft then
      jump(record)
      return
    end
    -- Otherwise, update the soft record with the current position and immediately navigate
    -- backwards.
    M.record(record.filename, nil, nil, record.soft)
  end
  if history.pos > 1 then history.pos = history.pos - 1 end
  jump(history[history.pos])
end

---
-- Navigates forwards through the current view's history.
-- @name forward
function M.forward()
  local history = view_history[view]
  if history.pos == #history then return end -- nothing to do
  local record = history[history.pos]
  if record.soft then M.record(record.filename, nil, nil, record.soft) end
  history.pos = history.pos + 1
  jump(history[history.pos])
end

---
-- Records the given location in the current view's history.
-- @param filename Optional string filename, buffer type, or identifier of the buffer to store. If
--   `nil`, uses the current buffer.
-- @param line Optional Integer line number to store. If `nil`, uses the current line.
-- @param column Optional integer column number on line *line* to store. If `nil`, uses the
--   current column.
-- @param soft Optional flag that indicates whether or not this record should be skipped when
--   navigating backward towards it, and updated when navigating away from it. The default
--   value is `false`.
-- @name record
function M.record(filename, line, column, soft)
  if not assert_type(filename, 'string/nil', 1) then
    filename = buffer.filename or buffer._type or _L['Untitled']
  end
  if not assert_type(line, 'number/nil', 2) then
    line = buffer:line_from_position(buffer.current_pos)
  end
  if not assert_type(column, 'number/nil', 3) then column = buffer.column[buffer.current_pos] end
  local history = view_history[view]
  if #history > 0 then
    local record = history[history.pos]
    if filename == record.filename and
      (math.abs(record.line - line) <= M.minimum_line_distance or record.soft) then
      -- If the most recent record is close enough (distance-wise), or if that record is a soft
      -- record, update it instead of recording a new one.
      record.line, record.column = line, column
      record.soft = soft and record.soft
      return
    end
  end
  if history.pos < #history then
    for i = history.pos + 1, #history do history[i] = nil end -- clear forward
  end
  history[#history + 1] = {filename = filename, line = line, column = column, soft = soft}
  if #history > M.maximum_history_size then table.remove(history, 1) end
  history.pos = #history
end

-- Softly record positions when switching between buffers.
local function record_switch() if not jumping then M.record(nil, nil, nil, true) end end
events.connect(events.BUFFER_BEFORE_SWITCH, record_switch)
events.connect(events.BUFFER_AFTER_SWITCH, record_switch)
events.connect(events.FILE_OPENED, record_switch)

---
-- Clears all view history.
-- @name clear
function M.clear() for view in pairs(view_history) do view_history[view] = {pos = 0} end end

return M
