---@diagnostic disable: duplicate-set-field

-- 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 command = require("core.command")
local Doc = require("core.doc")
local style = require("core.style")
local DocView = require("core.text.editor")
local CommandView = require("core.command.view")
local WorkspaceView = require("core.workspace.view")
local TextDocument = require("core.text.document")

-- Gridlock + original-line bar plugin

-- Local option: highlight invalid Windows filename characters in gridlock
core.gridlock_filename_mode = (core.gridlock_filename_mode ~= false)

-- Fixed format for gridlock mode (numbers only by default)
local gridlock_format = {
	patterns = {
		{ pattern = "-?0x%x+", type = "number" },
		{ pattern = "-?%d+[%d%.eE]*", type = "number" },
		{ pattern = "-?%.?%d+", type = "number" },
	},
	symbols = {},
}

-- Gridlock filename mode format (numbers + invalid filename chars)
local gridlock_filename_format = {
	patterns = {
		-- Invalid Windows filename characters
		{ pattern = '[<>:"/\\|%?%*]', type = "error" },
		{ pattern = "[%z\1-\31]", type = "error" },
		{ pattern = "[%. ]+$", type = "error" },
		-- Keep numbers highlighted too
		{ pattern = "-?0x%x+", type = "number" },
		{ pattern = "-?%d+[%d%.eE]*", type = "number" },
		{ pattern = "-?%.?%d+", type = "number" },
	},
	symbols = {},
}

-- Override format selection while gridlock is enabled
do
	local _original_reset_format = TextDocument.reset_format

	function TextDocument:reset_format()
		if core.gridlock then
			if core.gridlock_filename_mode then
				self.format = gridlock_filename_format
			else
				self.format = gridlock_format
			end
			self.highlighter:reset()
			return
		end
		return _original_reset_format(self)
	end
end

-- Reapply the format for all open docs
local function reset_all_doc_formats()
	for _, doc in ipairs(core.docs) do
		doc:reset_format()
	end
end

if core.gridlock then
	-- Ensure the format override applies when plugin loads with gridlock already enabled
	reset_all_doc_formats()
end

-- Snapshot original line text for gridlock mode
local function snapshot_gridlock_doc(doc)
	if not doc or doc.gridlock_original_lines then
		return
	end
	local lines = {}
	for i, line in ipairs(doc.lines) do
		lines[i] = line
	end
	doc.gridlock_original_lines = lines
end

-- Snapshot all open docs when gridlock turns on
local function snapshot_all_docs()
	for _, doc in ipairs(core.docs) do
		snapshot_gridlock_doc(doc)
	end
end

--- Forget snapshots when Gridlock stops so its next session starts fresh
local function clear_all_doc_snapshots()
	for _, doc in ipairs(core.docs) do
		doc.gridlock_original_lines = nil
	end
end

-- Wrap Doc.open_file to snapshot docs opened during gridlock
do
	local original_open_file = Doc.open_file

	-- Preserve open options so startup CLI per-doc settings still apply
	function Doc.open_file(filename, opts)
		local doc = original_open_file(filename, opts)
		if core.gridlock and doc then
			snapshot_gridlock_doc(doc)
			doc:reset_format()
		end
		return doc
	end
end

-- Wrap core:set-gridlock to snapshot on enable (CLI path)
do
	local cmd = command.map["core:set-gridlock"]
	if cmd then
		local orig = cmd.perform
		cmd.perform = function(...)
			orig(...)
			if core.gridlock then
				snapshot_all_docs()
			else
				clear_all_doc_snapshots()
			end
			reset_all_doc_formats()
		end
	end
end

-- Toggle gridlock from command palette
command.register({
	["core:toggle-gridlock"] = function()
		core.gridlock = not core.gridlock
		if core.gridlock then
			snapshot_all_docs()
		else
			clear_all_doc_snapshots()
		end
		reset_all_doc_formats()
	end,
})

-- Slim top bar showing original line text while gridlock is enabled
do
	local _original_update = WorkspaceView.update
	local _original_draw = WorkspaceView.draw
	local original_mouse_down = WorkspaceView.on_mouse_down
	local _original_mouse_moved = WorkspaceView.on_mouse_move

	WorkspaceView.update = function(self)
		if core.gridlock then
			local h = style.code_font:get_height()
			local pad = math.max(1, style.padding.y - math.ceil(2 * SCALE))
			self.gridlock_bar_height = h + pad * 2
		else
			self.gridlock_bar_height = 0
		end

		self.root_node.position.x, self.root_node.position.y =
			self.position.x, self.position.y + self.gridlock_bar_height
		self.root_node.size.x, self.root_node.size.y = self.size.x, self.size.y - self.gridlock_bar_height
		self.root_node:update()
		self.root_node:update_layout()
	end

	WorkspaceView.draw = function(self)
		_original_draw(self)
		local bar_h = self.gridlock_bar_height or 0
		if bar_h <= 0 then
			return
		end

		-- Draw original-line bar
		renderer.draw_rect(self.position.x, self.position.y, self.size.x, bar_h, style.headerbar_background)

		local av = core.active_view
		if not av or not core.view_can_edit(av) or av:is(CommandView) then
			return
		end

		local doc = av.doc
		local line = doc:get_selection()
		local original = doc.gridlock_original_lines and doc.gridlock_original_lines[line]
		local text = original or doc.lines[line] or ""
		if text:sub(-1) == "\n" then
			text = text:sub(1, -2)
		end
		-- Track whether the current line differs from the original snapshot
		local current = doc.lines[line] or ""
		if current:sub(-1) == "\n" then
			current = current:sub(1, -2)
		end
		local changed = original and (current ~= text)

		local x = self.position.x + style.padding.x
		local y = self.position.y + (bar_h - style.code_font:get_height()) / 2
		-- Dim original line text by default; use full color when changed
		local text_color = changed and style.headerbar_text or style.line_number
		common.draw_text(style.code_font, text_color, text, "left", x, y, self.size.x - style.padding.x * 2, bar_h)
	end

	-- Restore original line text on click
	local function restore_original_line()
		local av = core.active_view
		if not av or not core.view_can_edit(av) or av:is(CommandView) then
			return
		end
		local doc = av.doc
		local line = doc:get_selection()
		local original = doc.gridlock_original_lines and doc.gridlock_original_lines[line]
		if not original then
			return
		end
		local current = doc.lines[line]
		if not current then
			return
		end
		if current == original then
			return
		end
		local line_len = #current
		if current:sub(-1) == "\n" then
			line_len = line_len - 1
		end
		-- Replace line contents with original text (undoable)
		doc:raw_remove(line, 1, line, line_len + 1, doc.undo_stack, system.get_time())
		local insert_text = original
		if insert_text:sub(-1) == "\n" then
			insert_text = insert_text:sub(1, -2)
		end
		if #insert_text > 0 then
			doc:raw_insert(line, 1, insert_text, doc.undo_stack, system.get_time())
		end
	end

	--- Restore current gridlocked line or forward workspace mouse press
	-- @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
	WorkspaceView.on_mouse_down = function(self, button, x, y, clicks, modifier_state)
		local bar_h = self.gridlock_bar_height or 0
		if button == "left"
			and core.gridlock
			and bar_h > 0
			and y >= self.position.y
			and y < self.position.y + bar_h
		then
			-- Only a deliberate left click restores the original line
			restore_original_line()
			return
		end
		return original_mouse_down(self, button, x, y, clicks, modifier_state)
	end

	WorkspaceView.on_mouse_move = function(self, x, y, dx, dy)
		local bar_h = self.gridlock_bar_height or 0
		if core.gridlock and bar_h > 0 and y >= self.position.y and y < self.position.y + bar_h then
			-- Hand cursor over original-line bar
			system.set_mouse_cursor("hand")
			return
		end
		return _original_mouse_moved(self, x, y, dx, dy)
	end
end

-- Draw gutter markers for lines changed in gridlock
do
	local draw_line_gutter = DocView.draw_line_gutter
	function DocView:draw_line_gutter(idx, x, y)
		draw_line_gutter(self, idx, x, y)
		if not core.gridlock then
			return
		end
		if self.are_line_numbers_visible and not self:are_line_numbers_visible() then
			return
		end
		local original = self.doc.gridlock_original_lines and self.doc.gridlock_original_lines[idx]
		if not original then
			return
		end
		local current = self.doc.lines[idx]
		if not current then
			return
		end
		local o = original
		local c = current
		if o:sub(-1) == "\n" then
			o = o:sub(1, -2)
		end
		if c:sub(-1) == "\n" then
			c = c:sub(1, -2)
		end
		if o == c then
			return
		end
		-- Use highlight color for the line number
		local text = tostring(idx)
		local yoffset = self:get_line_text_y_offset()
		local xtext = self.position.x + style.padding.x
		renderer.draw_text(self:get_font(), text, xtext, y + yoffset, style.gridlock_changed_line)
	end
end
