-- 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.

local core = require("core")
local common = require("core.common")
local style = require("core.style")
local doc_monitor = require("core.doc.monitor")
local keymap = require("core.keymap")
local Object = require("core.object")
local View = require("core.ui.view")

local EmptyView = View:extend()

-- Draws or measures centered empty-view title and command hints and returns bounds
local function draw_empty_view_text(x, y, color, measure)
	local th = style.big_font:get_height()
	local dh = th + style.padding.y * 2

	-- Use renderer measure path during layout pass to avoid hidden drawing
	if measure then
		x = renderer.measure_text(style.big_font, "Lx", x)
	else
		x = renderer.draw_text(style.big_font, "Lx", x, y + (dh - th) / 2, color)
	end
	x = x + style.padding.x

	if not measure then
		renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color)
	end

	local lines = {
		{ fmt = "%s   |   new document", cmd = "core:new-doc" },
		{ fmt = "%s   |   command palette", cmd = "core:find-command" },
		{ fmt = "%s   |   open file", cmd = "core:open-file" },
		{ fmt = "%s   |   open file from project", cmd = "core:find-file" },
		{ fmt = "%s   |   toggle log", cmd = "log:toggle" },
		{ fmt = "%s   |   close tab, pane, app", cmd = "core:escape2" },
		{ fmt = "" },
		{ fmt = "Lx is a project by Liqube." },
		{ fmt = "You are looking at an early alpha version.", type = "dimmed" },
	}
	th = style.font:get_height()
	y = y + (dh - th * 2 - style.padding.y) / 2

	local w = 0
--	local lcolor = color

	for _, line in ipairs(lines) do
		local text = string.format(line.fmt, keymap.get_binding(line.cmd))

--		if line.type == "dimmed" then
--			lcolor = style.error
--		else
--			lcolor = style.success
--		end
		-- Keep return value semantics identical by tracking the next x position
		if measure then
			w = math.max(w, renderer.measure_text(style.font, text, x + style.padding.x))
		else
			w = math.max(w, renderer.draw_text(style.font, text, x + style.padding.x, y, color))
		end
		y = y + th + style.padding.y
		dh = dh + th
	end
	return w, dh
end

-- Renders the empty view background and centered helper text
function EmptyView:draw()
	self:draw_background(style.root.bg) -- dedicated root background color
	local w, h = draw_empty_view_text(0, 0, nil, true)
	local x = self.position.x + math.max(style.padding.x, (self.size.x - w) / 2)
	local y = self.position.y + (self.size.y - h) / 2
	draw_empty_view_text(x, y, style.root.fg, false)
end

local Node = Object:extend()

--- Draw shared deleted-file styling above any file-backed view
-- @param view table active workspace view
local function draw_deleted_file_border(view)
	local doc = view and view.doc
	if not doc or doc.is_actual_file ~= true then
		return
	end

	local state = doc_monitor.get_state and doc_monitor.get_state(doc)
	if not state or state.deleted ~= true then
		return
	end

	local x, y, width, height = view.position.x, view.position.y, view.size.x, view.size.y
	local border = 1
	local color = style.deleted or style.error or style.text

	-- Keep deleted-file warning visible above every specialized file renderer
	renderer.draw_rect(x, y, width, border, color)
	renderer.draw_rect(x, y + height - border, width, border, color)
	renderer.draw_rect(x, y, border, height, color)
	renderer.draw_rect(x + width - border, y, border, height, color)
end

-- Initializes a layout node as a leaf or split container
function Node:new(type)
	self.type = type or "leaf"
	self.position = { x = 0, y = 0 }
	self.size = { x = 0, y = 0 }
	self.views = {}
	self.divider = 0.5
	if self.type == "leaf" then
		self:add_view(EmptyView())
	end
end

-- Calls the same method on both child nodes of a split
function Node:propagate(fn, ...)
	self.a[fn](self.a, ...)
	self.b[fn](self.b, ...)
end

-- Tracks hovered tabs and forwards mouse-move events to the active branch
function Node:on_mouse_move(x, y, ...)
	self.hovered_tab = self:get_tab_overlapping_point(x, y)
	if self.type == "leaf" then
		self.active_view:on_mouse_move(x, y, ...)
	else
		self:propagate("on_mouse_move", x, y, ...)
	end
end

-- Forwards mouse-release events to the active leaf or both split children
function Node:on_mouse_up(...)
	if self.type == "leaf" then
		self.active_view:on_mouse_up(...)
	else
		self:propagate("on_mouse_up", ...)
	end
end

function Node:on_mouse_left(...)
	if self.type == "leaf" then
		if self.active_view.on_mouse_left then
			self.active_view:on_mouse_left(...)
		end
	else
		self:propagate("on_mouse_left", ...)
	end
end

-- Replaces this node's fields with another node instance
function Node:consume(node)
	for k, _ in pairs(self) do
		self[k] = nil
	end
	for k, v in pairs(node) do
		self[k] = v
	end
end

local type_map = { up = "vsplit", down = "vsplit", left = "hsplit", right = "hsplit" }

-- Splits a leaf node in the requested direction and returns the original child
function Node:split(dir, view, locked)
	assert(self.type == "leaf", "Tried to split non-leaf node")
	local type = assert(type_map[dir], "Invalid direction")
	local last_active = core.active_view
	local child = Node()
	child:consume(self)
	self:consume(Node(type))
	self.a = child
	self.b = Node()
	if view then
		self.b:add_view(view)
	end
	if locked then
		self.b.locked = locked
		core.set_active_view(last_active)
	end
	if dir == "up" or dir == "left" then
		self.a, self.b = self.b, self.a
	end
	return child
end

-- Closes the active view and collapses layout nodes when needed
function Node:close_active_view(root, on_closed)
	local function reset_leaf_to_empty(node)
		local empty = EmptyView()
		node.views = { empty }
		node.active_view = empty
		core.set_active_view(empty)
	end

	local do_close = function()
		if #self.views > 1 then
			local idx = self:get_view_idx(self.active_view)
			table.remove(self.views, idx)
			self:set_active_view(self.views[idx] or self.views[#self.views])
		else
			local parent = self:get_parent_node(root)
			if not parent then
				reset_leaf_to_empty(self)
				core.last_active_view = nil
				if on_closed then
					on_closed()
				end
				return
			end
			local is_a = (parent.a == self)
			local other = parent[is_a and "b" or "a"]
			if other:get_locked_size() then
				reset_leaf_to_empty(self)
			else
				parent:consume(other)
				local p = parent
				while p.type ~= "leaf" do
					p = p[is_a and "a" or "b"]
				end
				p:set_active_view(p.active_view)
			end
		end
		core.last_active_view = nil
		-- Allow optional post-close callback
		if on_closed then
			on_closed()
		end
	end
	self.active_view:try_close(do_close)
end

-- Adds a view to a leaf node and makes it active
function Node:add_view(view)
	assert(self.type == "leaf", "Tried to add view to non-leaf node")
	assert(not self.locked, "Tried to add view to locked node")
	if self.views[1] and self.views[1]:is(EmptyView) then
		table.remove(self.views)
	end
	table.insert(self.views, view)
	self:set_active_view(view)
end

-- Sets and activates the current view for a leaf node
function Node:set_active_view(view)
	assert(self.type == "leaf", "Tried to set active view on non-leaf node")
	self.active_view = view
	core.set_active_view(view)
end

-- Returns the tab index of a view in this leaf node
function Node:get_view_idx(view)
	for i, v in ipairs(self.views) do
		if v == view then
			return i
		end
	end
end

-- Returns the leaf node that contains the given view
function Node:get_node_for_view(view)
	for _, v in ipairs(self.views) do
		if v == view then
			return self
		end
	end
	if self.type ~= "leaf" then
		return self.a:get_node_for_view(view) or self.b:get_node_for_view(view)
	end
end

-- Finds and returns this node's parent inside the given root tree
function Node:get_parent_node(root)
	if root.a == self or root.b == self then
		return root
	elseif root.type ~= "leaf" then
		return self:get_parent_node(root.a) or self:get_parent_node(root.b)
	end
end

-- Collects all descendant views under this node
function Node:get_children(t)
	t = t or {}
	for _, view in ipairs(self.views) do
		table.insert(t, view)
	end
	if self.a then
		self.a:get_children(t)
	end
	if self.b then
		self.b:get_children(t)
	end
	return t
end

-- Returns the split node whose divider overlaps the given point
function Node:get_divider_overlapping_point(px, py)
	if self.type ~= "leaf" then
		if not self.a or not self.b then
			return nil
		end
		local p = math.max(1, math.floor(2 * SCALE + 0.5))
		local x, y, w, h = self:get_divider_rect()
		x, y = x - p, y - p
		w, h = w + p * 2, h + p * 2
		if self:is_divider_resizable() and px > x and py > y and px < x + w and py < y + h then
			return self
		end
		return self.a:get_divider_overlapping_point(px, py) or self.b:get_divider_overlapping_point(px, py)
	end
end

-- Returns the tab index under the given point in a multi-tab leaf
function Node:get_tab_overlapping_point(px, py)
	if #self.views == 1 then
		return nil
	end
	local x, y, w, h = self:get_tab_rect(1)
	if px >= x and py >= y and px < x + w * #self.views and py < y + h then
		return math.floor((px - x) / w) + 1
	end
end

-- Descends the tree to find the leaf node under a screen point
function Node:get_child_overlapping_point(x, y)
	local child
	if self.type == "leaf" then
		return self
	elseif not self.a or not self.b then
		-- Split node may be transiently incomplete during layout mutation
		return self.a or self.b or self
	elseif self.type == "hsplit" then
		child = (x < self.b.position.x) and self.a or self.b
	elseif self.type == "vsplit" then
		child = (y < self.b.position.y) and self.a or self.b
	end
	return child:get_child_overlapping_point(x, y)
end

-- Computes the rectangle for a tab at the given index
function Node:get_tab_rect(idx)
	local tw = math.min(style.tab_width, math.ceil(self.size.x / #self.views))
	local h = style.font:get_height() + style.padding.y * 2
	return self.position.x + (idx - 1) * tw, self.position.y, tw, h
end

-- Returns the divider rectangle for this split node
function Node:get_divider_rect()
	local x, y = self.position.x, self.position.y
	if self.type == "hsplit" then
		return x + self.a.size.x, y, style.divider_size, self.size.y
	elseif self.type == "vsplit" then
		return x, y + self.a.size.y, self.size.x, style.divider_size
	end
end

-- Returns true only when divider movement can affect this split on its axis
function Node:is_divider_resizable()
	if self.type == "hsplit" then
		local x1 = self.a:get_locked_size()
		local x2 = self.b:get_locked_size()
		return not (x1 or x2)
	elseif self.type == "vsplit" then
		local _, y1 = self.a:get_locked_size()
		local _, y2 = self.b:get_locked_size()
		return not (y1 or y2)
	end
	return false
end

-- Returns fixed dimensions required by locked descendants, if any
function Node:get_locked_size()
	if self.type == "leaf" then
		if self.locked then
			local size = self.active_view.size
			return size.x, size.y
		end
	else
		local x1, y1 = self.a:get_locked_size()
		local x2, y2 = self.b:get_locked_size()
		if x1 and x2 then
			local dsx = (x1 < 1 or x2 < 1) and 0 or style.divider_size
			local dsy = (y1 < 1 or y2 < 1) and 0 or style.divider_size
			return x1 + x2 + dsx, y1 + y2 + dsy
		end
	end
end

-- Copies position and size coordinates from one view-like object to another
local function copy_position_and_size(dst, src)
	dst.position.x, dst.position.y = src.position.x, src.position.y
	dst.size.x, dst.size.y = src.size.x, src.size.y
end

--- Calculate child sizes for either split axis without producing negative space
-- The x and y field names are swapped for vertical splits
-- @param self table split node
-- @param x string split-axis field name
-- @param y string cross-axis field name
-- @param x1 number|nil first child's locked size
-- @param x2 number|nil second child's locked size
local function calc_split_sizes(self, x, y, x1, x2)
	local n
	local available = math.max(0, self.size[x])
	local cross_size = math.max(0, self.size[y])
	local requested_divider = (x1 and x1 < 1 or x2 and x2 < 1) and 0 or style.divider_size
	local ds = math.min(requested_divider, available)
	local child_space = available - ds

	if x1 then
		n = math.min(math.max(0, x1), child_space) + ds
	elseif x2 then
		n = available - math.min(math.max(0, x2), child_space)
	else
		n = common.clamp(math.floor(available * self.divider), ds, available)
	end
	self.a.position[x] = self.position[x]
	self.a.position[y] = self.position[y]
	self.a.size[x] = n - ds
	self.a.size[y] = cross_size
	self.b.position[x] = self.position[x] + n
	self.b.position[y] = self.position[y]
	self.b.size[x] = available - n
	self.b.size[y] = cross_size
end

-- Updates child positions and sizes recursively for this layout subtree
function Node:update_layout()
	if self.type == "leaf" then
		local av = self.active_view
		if #self.views > 1 then
			local _, _, _, th = self:get_tab_rect(1)
			av.position.x, av.position.y = self.position.x, self.position.y + th
			av.size.x, av.size.y = self.size.x, self.size.y - th
		else
			copy_position_and_size(av, self)
		end
	else
		local x1, y1 = self.a:get_locked_size()
		local x2, y2 = self.b:get_locked_size()
		if self.type == "hsplit" then
			calc_split_sizes(self, "x", "y", x1, x2)
		elseif self.type == "vsplit" then
			calc_split_sizes(self, "y", "x", y1, y2)
		end
		self.a:update_layout()
		self.b:update_layout()
	end
end

-- Updates all views contained in this node subtree
function Node:update()
	if self.type == "leaf" then
		for _, view in ipairs(self.views) do
			view:update()
		end
	else
		self.a:update()
		self.b:update()
	end
end

-- Draws the tab strip and tab titles for a multi-view leaf
function Node:draw_tabs()
	local x, y, _, h = self:get_tab_rect(1)
	local ds = style.divider_size
	core.push_clip_rect(x, y, self.size.x, h)
	renderer.draw_rect(x, y, self.size.x, h, style.background2)
	renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider)

	for i, view in ipairs(self.views) do
		local x, y, w, h = self:get_tab_rect(i)
		local text = view:get_name()
		local color = style.dim
		local state = (view and view.doc and doc_monitor.get_state) and doc_monitor.get_state(view.doc) or nil
		local is_deleted = state and state.deleted == true
		if view == self.active_view then
			color = style.text
			renderer.draw_rect(x, y, w, h, style.background)
			renderer.draw_rect(x + w, y, ds, h, style.divider)
			renderer.draw_rect(x - ds, y, ds, h, style.divider)
		end
		if i == self.hovered_tab then
			color = style.text
		end
		if is_deleted then
			color = style.deleted or style.error or color
		end
		local is_pinned = view and view.doc and (
			view.is_browser_preview ~= true
			or (type(view.doc.is_dirty) == "function" and view.doc:is_dirty())
		)
		if is_pinned then
			-- A short top mark identifies pinned documents without changing tab width or title
			local marker_width = math.min(math.ceil(10 * SCALE), math.max(0, w - style.padding.x * 2))
			local marker_height = math.ceil(1 * SCALE)
			local marker_color = view == self.active_view and style.accent or style.dim

			renderer.draw_rect(x + style.padding.x, y, marker_width, marker_height, marker_color)
		end
		core.push_clip_rect(x, y, w, h)
		x, w = x + style.padding.x, w - style.padding.x * 2
		local align = style.font:get_width(text) > w and "left" or "center"
		local text_w = style.font:get_width(text)
		local text_x = x
		if align == "center" then
			text_x = x + math.max(0, (w - text_w) / 2)
		end
		common.draw_text(style.font, color, text, align, x, y, w, h)
		if is_deleted then
			local strike_x = text_x
			local strike_w = math.max(0, math.min(text_w, x + w - strike_x))
			if strike_w > 0 then
				renderer.draw_rect(strike_x, y + math.floor(h * 0.56), strike_w, math.ceil(1 * SCALE), color)
			end
		end
		core.pop_clip_rect()
	end

	core.pop_clip_rect()
end

-- Draws either the active leaf view or split divider and children
function Node:draw()
	if self.type == "leaf" then
		if #self.views > 1 then
			self:draw_tabs()
		end
		local pos, size = self.active_view.position, self.active_view.size
		core.push_clip_rect(pos.x, pos.y, size.x + pos.x % 1, size.y + pos.y % 1)
		self.active_view:draw()
		draw_deleted_file_border(self.active_view)
		core.pop_clip_rect()
	else
		local x, y, w, h = self:get_divider_rect()
		renderer.draw_rect(x, y, w, h, style.divider)
		self:propagate("draw")
	end
end


return Node
