---@diagnostic disable: param-type-mismatch
-- 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 keymap = require("core.keymap")
local open_mode = require("core.open_mode")
local CommandView = require("core.command.view")
local StatusView = require("core.status.view")
local fps_overlay = require("core.ui.fps_overlay")
local View = require("core.ui.view")
local Node = require("core.workspace.node")

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

local WorkspaceView = View:extend()

--- Return whether a named document may safely replace one unused scratch tab
-- @param view table|nil current active view
-- @param incoming_doc table document being opened
-- @return boolean replaceable
local function can_replace_untitled_view(view, incoming_doc)
	local current_doc = view and view.doc
	local incoming_filename = incoming_doc and incoming_doc.filename

	-- Only real named documents may consume an automatically created scratch tab
	return type(incoming_filename) == "string"
		and incoming_filename ~= ""
		and current_doc
		and current_doc.filename == nil
		and type(current_doc.is_dirty) == "function"
		and not current_doc:is_dirty()
end

-- Initializes the root view state and creates the initial layout node
function WorkspaceView:new()
	WorkspaceView.super.new(self)
	self.root_node = Node()
	self.deferred_draws = {}
	self.mouse = { x = 0, y = 0 }
end

--- Build the default workspace shell used at startup
-- Creates the workspace, command, and status views and installs the live-resize draw path
-- @return table shell
function WorkspaceView.build_default_shell()
	local workspace_view = WorkspaceView()
	local command_view = CommandView()
	local status_view = StatusView()

	workspace_view.root_node:split("down", command_view, true)
	workspace_view.root_node.b:split("down", status_view, true)

	-- Keep Windows modal resize drawing on the normal Lua render path so layout stays live while dragging the frame
	system.set_live_resize_callback(function()
		local width, height = renderer.get_size()

		workspace_view.size.x, workspace_view.size.y = width, height
		workspace_view:update()

		renderer.begin_frame()
		core.clip_rect_stack[1] = { 0, 0, width, height }
		renderer.set_clip_rect(table.unpack(core.clip_rect_stack[1]))
		workspace_view:draw()
		renderer.end_frame()
	end)

	return {
		workspace_view = workspace_view,
		command_view = command_view,
		status_view = status_view,
	}
end

-- Apply mouse cursor shape for current pointer position and active view state
function WorkspaceView:sync_mouse_cursor()
	if core.startup_cursor_lock == true then
		return
	end
	local x, y = self.mouse.x, self.mouse.y
	local ok_node, node = pcall(self.root_node.get_child_overlapping_point, self.root_node, x, y)
	if not ok_node then
		system.set_mouse_cursor("arrow")
		return
	end
	if not node then
		system.set_mouse_cursor("arrow")
		return
	end
	local ok_div, div = pcall(self.root_node.get_divider_overlapping_point, self.root_node, x, y)
	if not ok_div then
		system.set_mouse_cursor("arrow")
		return
	end
	if div then
		system.set_mouse_cursor(div.type == "hsplit" and "sizeh" or "sizev")
	elseif node:get_tab_overlapping_point(x, y) then
		system.set_mouse_cursor("arrow")
	else
		system.set_mouse_cursor(node.active_view.mouse_cursor)
	end
end

-- Queues a draw callback to run after the normal root draw pass
function WorkspaceView:defer_draw(fn, ...)
	table.insert(self.deferred_draws, 1, { fn = fn, ... })
end

-- Returns the layout node that contains the active view
function WorkspaceView:get_active_node()
	return self.root_node:get_node_for_view(core.active_view)
end

--- Opens or focuses a document in the active layout node
-- @param doc table document to show
-- @param opts table|nil view construction options
function WorkspaceView:open_doc(doc, opts)
	if not doc then
		return nil
	end
	open_mode.check(opts and opts.open_mode, 2)

	local node = self:get_active_node()
	if node.locked and core.last_active_view then
		core.set_active_view(core.last_active_view)
		node = self:get_active_node()
	end
	assert(not node.locked, "Cannot open doc on locked node")
	for i, view in ipairs(node.views) do
		if view.doc == doc then
			node:set_active_view(node.views[i])
			return view
		end
	end

	-- A clean unnamed tab is a startup placeholder, not a tab the user must close by hand
	if can_replace_untitled_view(node.active_view, doc) then
		return self:replace_doc_view(node.active_view, doc, opts)
	end

	-- Build the view through the core factory so plugins can choose view class
	local view = core.create_doc_view(doc, opts)
	node:add_view(view)
	self.root_node:update_layout()
	if view.scroll_to_line and view.doc and view.doc.get_selection then
		view:scroll_to_line(view.doc:get_selection(), true, true)
	end
	return view
end

--- Replace one document view through the normal view factory
-- @param target table existing document view
-- @param doc table replacement document
-- @param opts table|nil view construction options
-- @return table|nil view replacement view
function WorkspaceView:replace_doc_view(target, doc, opts)
	open_mode.check(opts and opts.open_mode, 2)

	local node = self.root_node:get_node_for_view(target)

	if not node or node.locked then
		return nil
	end

	local index = node:get_view_idx(target)
	local view = core.create_doc_view(doc, opts)
	node.views[index] = view
	node:set_active_view(view)
	self.root_node:update_layout()

	if view.scroll_to_line and view.doc and view.doc.get_selection then
		view:scroll_to_line(view.doc:get_selection(), true, true)
	end

	return view
end

--- Handle divider dragging, tab activation or closing, and view clicks
-- @param button string mouse button
-- @param x number mouse x
-- @param y number mouse y
-- @param clicks number|nil click count
-- @param modifier_state string|nil live modifiers from native event
function WorkspaceView:on_mouse_down(button, x, y, clicks, modifier_state)
	-- Use click-time state so mouse gestures cannot inherit stale keyboard state
	keymap.sync_modkeys(modifier_state)

	local div = self.root_node:get_divider_overlapping_point(x, y)
	if div and button == "left" then
		self.dragged_divider = div
		return
	end
	local node = self.root_node:get_child_overlapping_point(x, y)
	local idx = node:get_tab_overlapping_point(x, y)
	if idx then
		node:set_active_view(node.views[idx])
		if button == "middle" or (button == "left" and clicks == 2) then
			-- Use the normal close path so edited documents retain their confirmation prompt
			node:close_active_view(self.root_node)
		end
	else
		core.set_active_view(node.active_view)
		if keymap.on_mouse_down(button, x, y, clicks) then
			-- Apply cursor changes made by mouse bindings before the pointer moves
			self:sync_mouse_cursor()
			return
		end
		node.active_view:on_mouse_down(button, x, y, clicks)
		-- Apply cursor changes made directly by the active view before the pointer moves
		self:sync_mouse_cursor()
	end
end

-- Stops divider dragging and forwards mouse-release handling to the layout tree
function WorkspaceView:on_mouse_up(...)
	if self.dragged_divider then
		self.dragged_divider = nil
	end
	self.root_node:on_mouse_up(...)
	-- Apply a restored view cursor immediately instead of waiting for pointer movement
	self:sync_mouse_cursor()
end

-- Updates divider drag state, hover routing, and mouse cursor shape
function WorkspaceView:on_mouse_move(x, y, dx, dy)
	if self.dragged_divider then
		local node = self.dragged_divider
		if node.type == "hsplit" then
			node.divider = node.divider + dx / node.size.x
		else
			node.divider = node.divider + dy / node.size.y
		end
		node.divider = common.clamp(node.divider, 0.01, 0.99)
		return
	end

	self.mouse.x, self.mouse.y = x, y
	self.root_node:on_mouse_move(x, y, dx, dy)
	self:sync_mouse_cursor()
end

-- Routes wheel input through the view under the mouse without changing focus
function WorkspaceView:on_mouse_wheel(a, b, c, d)
	local x, y = self.mouse.x, self.mouse.y
	local node = self.root_node:get_child_overlapping_point(x, y)
	local target_view = node.active_view

	-- Modified wheel bindings use the hovered view's own controls
	if keymap.on_mouse_wheel(a, b, c, d, target_view) then
		return
	end
	target_view:on_mouse_wheel(a, b, c, d)
end

function WorkspaceView:on_mouse_left(...)
	self.root_node:on_mouse_left(...)
	if core.startup_cursor_lock == true then
		return
	end
	system.set_mouse_cursor("arrow")
end

-- Forwards text input to the currently active view
function WorkspaceView:on_text_input(...)
	core.active_view:on_text_input(...)
end

--- Forward unfinished native composition to the currently active view
-- @param ... any composition text and selection fields
function WorkspaceView:on_text_editing(...)
	core.active_view:on_text_editing(...)
end

-- Synchronizes root layout bounds and updates the layout tree
function WorkspaceView:update()
	copy_position_and_size(self.root_node, self)
	self.root_node:update()
	self.root_node:update_layout()
end

-- Draws the layout tree, deferred callbacks, and the final core overlay
function WorkspaceView:draw()
	self.root_node:draw()
	while #self.deferred_draws > 0 do
		local t = table.remove(self.deferred_draws)
		t.fn(table.unpack(t))
	end

	-- Draw after ordinary workspace content without replacing this method at runtime
	fps_overlay.draw(core.frame_start)
end

return WorkspaceView
