-- lx-bundle:true

-- This file is part of Lx, created by Liqube <www.liqube.com>
-- Copyright Liqube Audio. All rights reserved.
-- Lx Interim License. See LICENSE for details.

--- Shared document navigation history
-- @module core.navigate

local core = require("core")
local Doc = require("core.doc")

local navigate = {}
local back_history = {}
local forward_history = {}
local history_limit = 100

--- Copy a flat table used for selections and view state
-- @param value table|nil source table
-- @return table|nil copy
local function copy_table(value)
	if type(value) ~= "table" then
		return nil
	end

	local copy = {}
	for key, item in pairs(value) do
		copy[key] = type(item) == "table" and copy_table(item) or item
	end

	return copy
end

--- Return whether one pane still belongs to the current workspace
-- @param node table|nil saved pane
-- @return boolean present
local function has_node(node)
	local active_view = node and node.active_view

	return active_view ~= nil
		and core.workspace_view.root_node:get_node_for_view(active_view) == node
end

--- Find a visible view for one saved document
-- @param doc table saved document
-- @return table|nil view
local function find_doc_view(doc)
	for _, view in ipairs(core.workspace_view.root_node:get_children()) do
		if view.doc == doc then
			return view
		end
	end
end

--- Activate one view through its owning pane
-- @param view table target view
-- @return table|nil view
local function activate_view(view)
	local node = core.workspace_view.root_node:get_node_for_view(view)
	if not node then
		return nil
	end

	node:set_active_view(view)

	return view
end

--- Return whether two positions describe the same visible place
-- @param first table first position
-- @param second table second position
-- @return boolean same
local function positions_match(first, second)
	if first.doc ~= second.doc or #first.selections ~= #second.selections then
		return false
	end

	for index = 1, #first.selections do
		local a = first.selections[index]
		local b = second.selections[index]
		if a[1] ~= b[1] or a[2] ~= b[2] or a[3] ~= b[3] or a[4] ~= b[4] then
			return false
		end
	end

	return first.scroll_x == second.scroll_x and first.scroll_y == second.scroll_y
end

--- Push one position while keeping session history bounded
-- @param history table target history stack
-- @param position table saved position
local function push(history, position)
	history[#history + 1] = position
	if #history > history_limit then
		table.remove(history, 1)
	end
end

--- Capture the active document, selections, pane, and scroll position
-- @return table|nil position
function navigate.capture()
	local view = core.active_view
	local doc = view and view.doc
	if not (doc and type(doc.get_selections) == "function") then
		return nil
	end

	local selections, primary = doc:get_selections()

	return {
		doc = doc,
		filename = doc.filename,
		view = view,
		node = core.workspace_view.root_node:get_node_for_view(view),
		selections = copy_table(selections),
		primary = primary,
		visual_caret = copy_table(doc.selection and doc.selection.visual_caret),
		scroll_x = view.scroll and view.scroll.x or 0,
		scroll_y = view.scroll and view.scroll.y or 0,
	}
end

--- Restore one saved position, reopening its file when its tab was closed
-- @param position table saved position
-- @return boolean restored
local function restore(position)
	local view = activate_view(position.view) or find_doc_view(position.doc)
	if view and core.active_view ~= view then
		view = activate_view(view)
	end

	if not view and type(position.filename) == "string" and position.filename ~= "" then
		if has_node(position.node) then
			position.node:set_active_view(position.node.active_view)
		end

		local doc = Doc.open_file(position.filename)
		view = doc and core.workspace_view:open_doc(doc) or nil
	end

	if not (view and view.doc and type(view.doc.set_selections) == "function") then
		return false
	end

	view.doc:set_selections(copy_table(position.selections), position.primary)
	if view.doc.selection then
		view.doc.selection.visual_caret = copy_table(position.visual_caret)
	end
	if view.scroll then
		view.scroll.x = position.scroll_x
		view.scroll.y = position.scroll_y
		view.scroll.to.x = position.scroll_x
		view.scroll.to.y = position.scroll_y
	end
	core.redraw = true

	return true
end

--- Record a position immediately before a successful navigation
-- @param previous table|nil position returned by 'capture'
-- @return boolean recorded
function navigate.record(previous)
	local current = navigate.capture()
	if not previous or not current or positions_match(previous, current) then
		return false
	end

	push(back_history, previous)
	forward_history = {}

	return true
end

--- Return to the previous recorded document position
-- @return boolean restored
function navigate.back()
	local current = navigate.capture()
	while #back_history > 0 do
		local previous = table.remove(back_history)
		if restore(previous) then
			if current then
				push(forward_history, current)
			end

			return true
		end
	end

	return false
end

--- Return to the next document position after moving back
-- @return boolean restored
function navigate.forward()
	local current = navigate.capture()
	while #forward_history > 0 do
		local next_position = table.remove(forward_history)
		if restore(next_position) then
			if current then
				push(back_history, current)
			end

			return true
		end
	end

	return false
end

--- Clear both navigation directions
function navigate.clear()
	back_history = {}
	forward_history = {}
end

return navigate
