-- lx-bundle:true

local core = require("core")
local Doc = require("core.doc")
local style = require("core.style")
local View = require("core.ui.view")

local LogView = View:extend()

local function get_log_font()
	-- Resolve log font dynamically so post-user font reloads take effect
	return style.code_font
end

-- Returns index of the newest error item matching an optional predicate
local function get_latest_error_index(predicate)
	for i = #core.log_items, 1, -1 do
		local item = core.log_items[i]
		if item and item.level == "error" and (not predicate or predicate(item)) then
			return i
		end
	end
	return nil
end

-- Formats log timestamp as 'YYmmdd HHiiss,mmm'
local function format_log_time(item)
	local sec = tonumber(item and item.time) or os.time()
	local msec = tonumber(item and item.msec) or 0
	if msec < 0 then
		msec = 0
	end
	if msec > 999 then
		msec = 999
	end
	--return string.format("%s,%03d", os.date("%y/%m/%d %H:%M:%S", sec), msec)
	return string.format("%s.%03d", os.date("%H:%M:%S", sec), msec)
end

-- Initializes log view state and scroll behavior
function LogView:new()
	LogView.super.new(self)
	self.last_item = core.log_items[#core.log_items]
	self.scrollable = true
	self.yoffset = 0
	self.hovered_item = nil
	self.follow_latest = true
	self.pinned_item_id = nil
end

-- Returns the tab title for the log view
function LogView:get_name()
	return "Log"
end

-- Returns rendered line count for a possibly multi-line string
local function count_lines(text)
	local n = 0
	for _ in tostring(text or ""):gmatch("[^\n]+") do
		n = n + 1
	end
	return math.max(1, n)
end

-- Returns total row height consumed by a single log item
local function get_item_height(item, line_height)
	local h = line_height -- main text baseline row
	h = h + line_height * math.max(0, count_lines(item and item.text) - 1)
	if item and item.info then
		h = h + line_height -- spacing row before info block
		h = h + line_height * count_lines(item.info)
	end
	h = h + style.padding.y
	return h
end

-- Returns the full scrollable content height for keyboard/page navigation
function LogView:get_scrollable_size()
	local line_height = get_log_font():get_height()
	local h = style.padding.y
	for i = #core.log_items, 1, -1 do
		h = h + get_item_height(core.log_items[i], line_height)
	end
	return math.max(self.size.y, h)
end

-- Returns the log item under a mouse point, or nil when no item is hit
function LogView:get_item_at(px, py)
	local x1, y1 = self.position.x, self.position.y
	local x2, y2 = x1 + self.size.x, y1 + self.size.y
	if px < x1 or px >= x2 or py < y1 or py >= y2 then
		return nil
	end

	local line_height = get_log_font():get_height()
	local _, oy = self:get_content_offset()
	local y = oy + style.padding.y + self.yoffset

	for i = #core.log_items, 1, -1 do
		local item = core.log_items[i]
		local top = y
		local bottom = y + get_item_height(item, line_height)
		if py >= top and py < bottom then
			return item, i
		end
		y = bottom
	end
	return nil, nil
end

-- Formats one log item as clipboard-friendly plain text
local function format_item_for_clipboard(item)
	if not item then
		return ""
	end
	local out = {}
	out[#out + 1] = string.format("[%s] %s", format_log_time(item), tostring(item.text or ""))
	out[#out + 1] = "at " .. tostring(item.at or "?")
	if item.info and item.info ~= "" then
		out[#out + 1] = tostring(item.info)
	end
	local text = table.concat(out, "\n")
	-- Ensure copied entry always ends with a linefeed
	if text ~= "" and text:sub(-1) ~= "\n" then
		text = text .. "\n"
	end
	return text
end

-- Normalize candidate path and resolve absolute path when possible
local function resolve_openable_path(path)
	if type(path) ~= "string" then
		return nil
	end

	local p = path:gsub("^%s+", ""):gsub("%s+$", "")

	if p == "" then
		return nil
	end

	p = p:gsub('^["\']', ""):gsub('["\']$', "")

	if p == "" then
		return nil
	end

	local abs = system.absolute_path(p) or p

	if system.is_file(abs) == true then
		return abs
	end

	return nil
end

-- Open file reference in the editor and optionally jump to line/column
local function open_file_reference(path, line, col)
	local target = resolve_openable_path(path)

	if not target then
		return false
	end

	local doc = Doc.open_file(target)

	if not doc then
		return false
	end

	local view = core.workspace_view:open_doc(doc)

	if view then
		core.set_active_view(view)
	end

	if line then
		local target_line = math.max(1, math.floor(tonumber(line) or 1))
		local target_col = math.max(1, math.floor(tonumber(col) or 1))

		if type(doc.lines) == "table" and #doc.lines > 0 then
			target_line = math.min(target_line, #doc.lines)
		end

		if type(doc.set_selection) == "function" then
			doc:set_selection(target_line, target_col)
		end

		if view and type(view.scroll_to_line) == "function" then
			view:scroll_to_line(target_line, false, true)
		end
	end

	return true
end

-- Try to open the first file reference found in log item text/info/at
local function try_open_log_item_reference(item)
	if type(item) ~= "table" then
		return false
	end

	local chunks = {}

	if type(item.text) == "string" and item.text ~= "" then
		chunks[#chunks + 1] = item.text
	end

	if type(item.info) == "string" and item.info ~= "" then
		chunks[#chunks + 1] = item.info
	end

	if type(item.at) == "string" and item.at ~= "" then
		chunks[#chunks + 1] = item.at
	end

	for _, text in ipairs(chunks) do
		-- Pattern breakdown:
		-- ([%a]:[\\/][^:\n]+) captures absolute Windows path C:\... or C:/...
		-- :(%d+):(%d+):? captures line and column
		for path, line, col in text:gmatch("([%a]:[\\/][^:\n]+):(%d+):(%d+):?") do
			if open_file_reference(path, line, col) then
				return true
			end
		end

		-- Pattern breakdown:
		-- ([%a]:[\\/][^:\n]+) captures absolute Windows path
		-- :(%d+):? captures line number
		for path, line in text:gmatch("([%a]:[\\/][^:\n]+):(%d+):?") do
			if open_file_reference(path, line, 1) then
				return true
			end
		end

		-- Pattern breakdown:
		-- ([\\/][^:\n]+) captures absolute Unix-like path
		-- :(%d+):(%d+):? captures line and column
		for path, line, col in text:gmatch("([\\/][^:\n]+):(%d+):(%d+):?") do
			if open_file_reference(path, line, col) then
				return true
			end
		end

		-- Pattern breakdown:
		-- ([\\/][^:\n]+) captures absolute Unix-like path
		-- :(%d+):? captures line number
		for path, line in text:gmatch("([\\/][^:\n]+):(%d+):?") do
			if open_file_reference(path, line, 1) then
				return true
			end
		end

		-- Pattern breakdown:
		-- ([%w%._%-]+[\\/][^:\n]+) captures relative path with at least one slash
		-- :(%d+):(%d+):? captures line and column
		for path, line, col in text:gmatch("([%w%._%-]+[\\/][^:\n]+):(%d+):(%d+):?") do
			if open_file_reference(path, line, col) then
				return true
			end
		end

		-- Pattern breakdown:
		-- ([%w%._%-]+[\\/][^:\n]+) captures relative path with at least one slash
		-- :(%d+):? captures line number
		for path, line in text:gmatch("([%w%._%-]+[\\/][^:\n]+):(%d+):?") do
			if open_file_reference(path, line, 1) then
				return true
			end
		end

		-- Pattern breakdown:
		-- ['\"]([^'\"]+)['\"] captures quoted path-like values
		for quoted in text:gmatch("['\"]([^'\"]+)['\"]") do
			if open_file_reference(quoted) then
				return true
			end
		end

		-- Pattern breakdown:
		-- ([%w%._%-]+[\\/][^%s]+) captures path-like tokens with at least one separator
		for token in text:gmatch("([%w%._%-]+[\\/][^%s]+)") do
			if open_file_reference(token) then
				return true
			end
		end
	end

	return false
end

-- Updates hover target and cursor when moving over copyable log entries
function LogView:on_mouse_move(x, y, dx, dy)
	LogView.super.on_mouse_move(self, x, y, dx, dy)
	local item = select(1, self:get_item_at(x, y))
	self.hovered_item = item
	self.mouse_cursor = (item and not self:scrollbar_overlaps_point(x, y)) and "hand" or "arrow"
end

-- Copies clicked log entry text to clipboard while preserving scrollbar behavior
function LogView:on_mouse_down(button, x, y, clicks)
	local caught = LogView.super.on_mouse_down(self, button, x, y, clicks)
	if caught then
		return true
	end

	if button == "middle" then
		local item = select(1, self:get_item_at(x, y))
		if not item then
			return false
		end

		if try_open_log_item_reference(item) then
			return true
		end

		if core.status_view then
			core.status_view:show_warning("No file reference found in log entry")
		end
		return true
	end

	if button ~= "left" then
		return false
	end

	local item = select(1, self:get_item_at(x, y))
	if not item then
		return false
	end

	system.set_clipboard(format_item_for_clipboard(item))
	if core.status_view then
		core.status_view:show_success("Copied log entry")
	end
	return true
end

-- Resets scroll when new log items arrive and advances view animation state
function LogView:update()
	local item = core.log_items[#core.log_items]
	if self.last_item ~= item then
		self.last_item = item
		if self.pinned_item_id then
			local idx = self:get_item_index_by_id(self.pinned_item_id)
			if idx then
				self:scroll_to_item(idx)
			else
				self.pinned_item_id = nil
			end
		elseif self.follow_latest then
			self.scroll.to.y = 0
			self.yoffset = -(get_log_font():get_height() + style.padding.y)
		end
	end

	self:move_towards("yoffset", 0)

	LogView.super.update(self)
end

-- Scrolls by N rendered text rows
function LogView:scroll_rows(n)
	self.pinned_item_id = nil
	self.follow_latest = false
	local step = math.max(1, get_log_font():get_height())
	self.scroll.to.y = self.scroll.to.y + (step * n)
end

-- Scrolls by roughly one viewport page
function LogView:scroll_page(n)
	self.pinned_item_id = nil
	self.follow_latest = false
	local step = math.max(1, self.size.y - get_log_font():get_height())
	self.scroll.to.y = self.scroll.to.y + (step * n)
end

-- Jumps to newest entries at top
function LogView:scroll_home()
	self.pinned_item_id = nil
	self.follow_latest = true
	self.scroll.to.y = 0
end

-- Jumps to oldest entries at bottom
function LogView:scroll_end()
	self.pinned_item_id = nil
	self.follow_latest = false
	local max = math.max(0, self:get_scrollable_size() - self.size.y)
	self.scroll.to.y = max
end

-- Returns index of newest error-level log item, or nil when no errors exist
function LogView:get_latest_error_index()
	return get_latest_error_index()
end

-- Returns index of newest startup-tagged error log item, or nil when absent
function LogView:get_latest_startup_error_index()
	return get_latest_error_index(function(item)
		return item.startup_error == true
	end)
end

-- Returns current index for a specific log item ID
function LogView:get_item_index_by_id(target_id)
	if type(target_id) ~= "number" then
		return nil
	end
	for i = #core.log_items, 1, -1 do
		if core.log_items[i] and core.log_items[i].id == target_id then
			return i
		end
	end
	return nil
end

-- Scrolls so the target item is visible at the top of the log viewport
function LogView:scroll_to_item(index)
	if type(index) ~= "number" then
		return false
	end
	index = math.floor(index)
	if index < 1 or index > #core.log_items then
		return false
	end
	local line_height = get_log_font():get_height()
	local offset = 0
	for i = #core.log_items, index + 1, -1 do
		offset = offset + get_item_height(core.log_items[i], line_height)
	end
	self.yoffset = 0
	-- Place the focused item below the top edge (~1/3 down) for better context
	local anchor_y = self:get_scroll_viewport_height() / 3
	local target = style.padding.y + offset - anchor_y
	self.follow_latest = false
	self.scroll.to.y = target
	self.scroll.y = target
	self:clamp_scroll_position()
	return true
end

-- Pins a specific item ID and keeps it visible even as newer log entries arrive
function LogView:pin_item_by_id(target_id)
	local idx = self:get_item_index_by_id(target_id)
	if not idx then
		return false
	end
	self.pinned_item_id = target_id
	return self:scroll_to_item(idx)
end

-- Draws multi-line text and returns the final draw coordinates
local function draw_text_multiline(font, text, x, y, color)
	local th = font:get_height()
	local resx, resy = x, y
	for line in text:gmatch("[^\n]+") do
		resy = y
		resx = renderer.draw_text(font, line, x, y, color)
		y = y + th
	end
	return resx, resy
end

-- Renders log entries from newest to oldest with timestamps and metadata
function LogView:draw()
	local font = get_log_font()
	self:draw_background(style.background)

	local ox, oy = self:get_content_offset()
	local th = font:get_height()
	local y = oy + style.padding.y + self.yoffset

	for i = #core.log_items, 1, -1 do
		local x = ox + style.padding.x
		local item = core.log_items[i]
		local text_color = style.text -- Default log entry text color
		if item.level == "error" then
			text_color = style.error or style.text
		elseif item.level == "warning" then
			text_color = style.warning or style.warn or style.error or style.text
		elseif item.level == "notice" then
			text_color = style.notice or style.text
		elseif item.level == "success" then
			text_color = style.success or style.text
		end
		local item_h = get_item_height(item, th)
		if item == self.hovered_item then
			renderer.draw_rect(ox, y, self.size.x, item_h, style.line_highlight)
		end
		local time = format_log_time(item)
		x = renderer.draw_text(font, time, x, y, style.dim)
		x = x + style.padding.x
		local subx = x
		x, y = draw_text_multiline(font, item.text, x, y, text_color)
		renderer.draw_text(font, " at " .. item.at, x, y, style.dim)
		y = y + th
		if item.info then
			subx, y = draw_text_multiline(font, item.info, subx, y, style.dim)
			y = y + th
		end
		y = y + style.padding.y
	end
end

return LogView
