-- lx-bundle:true
local core = require("core")
local common = require("core.common")
local style = require("core.style")
local TextDocument = require("core.text.document")
local TextEditor = require("core.text.editor")
local View = require("core.ui.view")

local SingleLineDoc = TextDocument:extend()

-- Inserts text into the single-line doc while stripping newline characters
function SingleLineDoc:insert(line, col, text)
	SingleLineDoc.super.insert(self, line, col, text:gsub("\n", ""))
end

local CommandView = TextEditor:extend()
local max_visible_suggestions = 10
local suggestion_scrollbar_width = math.max(3, math.floor(style.scrollbar_size * 0.35))

local noop = function(...) end

local default_state = {
	submit = noop,
	suggest = noop,
	cancel = noop,
}

-- Initializes command view state, animation fields, and prompt metadata
function CommandView:new()
	CommandView.super.new(self, SingleLineDoc())
	self.word_highlight_enabled = false -- Command input is not a document editing surface
	self.suggestion_idx = 1
	self.suggestions = {}
	self.suggestions_height = 0
	self.last_change_id = 0
	self.gutter_width = 0
	self.gutter_text_brightness = 0
	self.selection_offset = 0
	self.suggestion_view_start = 1
	self.state = default_state
	self.font = "font"
	self.size.y = 0
	self.label = ""
	self.label_color_override = nil
	self.line_text_color_override = nil
end

--- Return key binding contexts for the command palette surface
-- Command bindings bypass the generic doc context and fall back directly to global
-- @return table contexts ordered from most specific to broadest
function CommandView:get_keymap_contexts()
	return { "command_view", "global" }
end

-- Returns the view name from the base 'View' implementation
function CommandView:get_name()
	return View.get_name(self)
end

-- Returns the prompt line position centered vertically in the command bar
function CommandView:get_line_screen_position()
	local x = CommandView.super.get_line_screen_position(self, 1)
	local _, y = self:get_content_offset()
	local lh = self:get_line_height()
	return x, y + (self.size.y - lh) / 2
end

-- Reports no scrollable height because this view is single-line
function CommandView:get_scrollable_size()
	return 0
end

-- Disables inherited scroll-visibility behavior for the command bar
function CommandView:scroll_to_make_visible()
	-- no-op function to disable this functionality
end

-- Returns the current command text from the single-line document
function CommandView:get_text()
	return self.doc:get_text(1, 1, 1, math.huge)
end

-- Replaces command text and optionally selects it
function CommandView:set_text(text, select)
	self.doc:remove(1, 1, math.huge, math.huge)
	self.doc:text_input(text)
	if select then
		self.doc:set_selection(math.huge, math.huge, 1, 1)
	end
end

-- Moves suggestion selection by direction and applies completion text
function CommandView:move_suggestion_idx(dir)
	local n = self.suggestion_idx + dir
	self.suggestion_idx = common.clamp(n, 1, #self.suggestions)
	self:clamp_suggestion_window()
	self:complete()
	self.last_change_id = self.doc:get_change_id()
end

-- Fills the input with the currently selected suggestion text
function CommandView:complete()
	if #self.suggestions > 0 then
		local suggestion = self.suggestions[self.suggestion_idx]
		self:set_text(suggestion.complete_text or suggestion.text)
	end
end

-- Submits the current command text and selected suggestion, then exits
function CommandView:submit()
	local suggestion = self.suggestions[self.suggestion_idx]
	local text = self:get_text()
	local submit = self.state.submit
	self:exit(true)
	submit(text, suggestion)
end

--- Activates command mode with handlers and prompt label text
-- @param spec table enter specification table
-- @return boolean opened true when the prompt opened successfully
function CommandView:enter(spec)
	spec = type(spec) == "table" and spec or {}

	local text = spec.caption or ""
	local submit = spec.submit
	local suggest = spec.suggest
	local cancel = spec.cancel
	local urgency = spec.urgency

	if self.state ~= default_state then
		-- Preempt any stale/abandoned prompt state so a new prompt can always open
		self:exit(false, true)
	end
	self.state = {
		submit = submit or noop,
		suggest = suggest or noop,
		cancel = cancel or noop,
	}
	core.set_active_view(self)
	if not self:update_suggestions() then
		return false
	end

	-- Initialize popup geometry immediately to avoid one-frame collapsed artifacts
	local lh = self:get_suggestion_line_height()
	local visible_count = math.min(#self.suggestions, self:get_visible_suggestion_limit())

	self.suggestions_height = visible_count * lh
	self:clamp_suggestion_window()
	self.gutter_text_brightness = 100
	self.label = text .. ": "
	self.label_color_override = nil
	self.line_text_color_override = nil

	if type(urgency) == "string" and urgency ~= "" and urgency ~= "normal" then
		self.label_color_override = common.urgency_to_color(urgency)
	end

	return true
end

-- Exits command mode, resets state, and optionally triggers cancel callback
function CommandView:exit(submitted, inexplicit)
	if core.active_view == self then
		core.set_active_view(core.last_active_view)
	end

	local cancel = self.state.cancel

	self.state = default_state
	self.doc:reset()
	self.suggestions = {}
	self.suggestions_height = 0
	self.selection_offset = 0
	self.suggestion_view_start = 1
	self.last_change_id = 0
	self.suggestion_idx = 1
	self.label_color_override = nil
	self.line_text_color_override = nil

	if not submitted then
		cancel(not inexplicit)
	end
end

-- Returns the animated width used to render the prompt gutter
function CommandView:get_gutter_width()
	return self.gutter_width
end

-- Returns the pixel height of one suggestion row
function CommandView:get_suggestion_line_height()
	return self:get_font():get_height() + style.padding.y
end

-- Returns how many suggestion rows can be shown at once
function CommandView:get_visible_suggestion_limit()
	local line_height = self:get_suggestion_line_height()
	local available_height = math.max(0, self.position.y - style.divider_size)
	local visible_rows = math.max(1, math.floor(available_height / line_height))
	return math.min(visible_rows, max_visible_suggestions)
end

-- Keeps the selected suggestion inside the current visible suggestion window
function CommandView:clamp_suggestion_window()
	local total = #self.suggestions

	if total == 0 then
		self.suggestion_view_start = 1
		self.selection_offset = 0
		return
	end

	local limit = self:get_visible_suggestion_limit()
	local max_start = math.max(1, total - limit + 1)
	self.suggestion_view_start = common.clamp(self.suggestion_view_start, 1, max_start)

	if self.suggestion_idx < self.suggestion_view_start then
		self.suggestion_view_start = self.suggestion_idx
	elseif self.suggestion_idx >= self.suggestion_view_start + limit then
		self.suggestion_view_start = self.suggestion_idx - limit + 1
	end

	local visible_index = self.suggestion_idx - self.suggestion_view_start + 1
	self.selection_offset = visible_index * self:get_suggestion_line_height()
end

-- Moves selection by one suggestion-page while preserving in-view highlight behavior
function CommandView:move_suggestion_page(dir)
	if #self.suggestions == 0 then
		return
	end
	local step = self:get_visible_suggestion_limit()
	self:move_suggestion_idx(dir * step)
end

-- Moves selection to first or last suggestion
function CommandView:move_suggestion_edge(to_last)
	if #self.suggestions == 0 then
		return
	end
	self.suggestion_idx = to_last and #self.suggestions or 1
	self:clamp_suggestion_window()
	self:complete()
	self.last_change_id = self.doc:get_change_id()
end

--- Rebuild and normalize suggestions without letting a provider break the UI update
-- @return boolean updated true when the provider completed successfully
function CommandView:update_suggestions()
	local ok, res = core.try(function()
		local suggestions = self.state.suggest(self:get_text()) or {}
		if type(suggestions) ~= "table" then
			error("command suggestion provider must return a table")
		end

		local normalized = {}
		for i, item in ipairs(suggestions) do
			if type(item) == "string" then
				item = { text = item }
			elseif type(item) ~= "table" then
				error(string.format("command suggestion %d must be a string or table", i))
			end
			if type(item.text) ~= "string" then
				error(string.format("command suggestion %d text must be a string", i))
			end
			if item.complete_text ~= nil and type(item.complete_text) ~= "string" then
				error(string.format("command suggestion %d completion must be a string", i))
			end
			if item.info ~= nil and item.info ~= false and type(item.info) ~= "string" then
				error(string.format("command suggestion %d info must be a string", i))
			end
			normalized[i] = item
		end

		return normalized
	end)

	if not ok then
		self:exit(false, true)
		return false
	end

	self.suggestions = res
	self.suggestion_idx = 1
	self.suggestion_view_start = 1
	self:clamp_suggestion_window()

	return true
end

-- Updates command mode lifecycle, animations, and suggestion state
function CommandView:update()
	CommandView.super.update(self)

	if core.active_view ~= self and self.state ~= default_state then
		self:exit(false, true)
	end

	-- update suggestions if text has changed
	if self.last_change_id ~= self.doc:get_change_id() then
		if not self:update_suggestions() then
			return
		end
		self.last_change_id = self.doc:get_change_id()
	end

	-- update gutter text color brightness
	self:move_towards("gutter_text_brightness", 0, 0.1)

	-- update gutter width
	local dest = self:get_font():get_width(self.label) + style.padding.x

	if self.size.y <= 0 then
		self.gutter_width = dest
	else
		self:move_towards("gutter_width", dest)
	end

	-- update suggestions box height
	local lh = self:get_suggestion_line_height()
	local visible_count = math.min(#self.suggestions, self:get_visible_suggestion_limit())

	dest = visible_count * lh
	self:move_towards("suggestions_height", dest)

	-- update suggestion cursor offset
	self:clamp_suggestion_window()
	local selection_offset = self.selection_offset
	self:move_towards("selection_offset", selection_offset)

	-- update size based on whether this is the active_view
	dest = 0
	if self == core.active_view then
		dest = style.font:get_height() + style.padding.y * 2
	end

	self:move_towards(self.size, "y", dest)
end

-- Disables line-highlight drawing in the command bar
function CommandView:draw_line_highlight()
	-- no-op function to disable this functionality
end

-- Draws command input text with optional override color (used for find/replace errors)
function CommandView:draw_line_text(idx, x, y)
	local text = self.doc.lines[idx] or ""
	local color = self.line_text_color_override or style.input or style.text
	renderer.draw_text(self:get_font(), text, x, y + self:get_line_text_y_offset(), color)
end

-- Draws the animated prompt label in the gutter area
function CommandView:draw_line_gutter(idx, x, y)
	local yoffset = self:get_line_text_y_offset()
	local pos = self.position
	local color = common.lerp(style.prompt or style.text, style.accent, self.gutter_text_brightness / 100)
	color = self.label_color_override or color

	core.push_clip_rect(pos.x, pos.y, self:get_gutter_width(), self.size.y)
	x = x + style.padding.x
	renderer.draw_text(self:get_font(), self.label, x, y + yoffset, color)
	core.pop_clip_rect()
end

-- Draws the floating suggestions list and active selection highlight
local function draw_suggestions_box(self)
	local lh = self:get_suggestion_line_height()
	local dh = style.divider_size
	local x, _ = self:get_line_screen_position()
	local h = math.ceil(self.suggestions_height)

	if h <= 0 or #self.suggestions == 0 then
		return
	end

	local rx, ry, rw, rh = self.position.x, self.position.y - h - dh, self.size.x, h

	-- draw suggestions background
	renderer.draw_rect(rx, ry, rw, rh, style.background3)
	renderer.draw_rect(rx, ry - dh, rw, dh, style.divider)
	local y = self.position.y - self.selection_offset - dh
	renderer.draw_rect(rx, y, rw, lh, style.line_highlight)

	-- Draw a passive scroll indicator when the suggestion list exceeds visible rows
	local visible_limit = self:get_visible_suggestion_limit()
	if #self.suggestions > visible_limit then
		local track_x = rx + rw - suggestion_scrollbar_width
		local track_y = ry
		local track_h = rh

		renderer.draw_rect(track_x, track_y, suggestion_scrollbar_width, track_h, style.scrollbar)

		local thumb_h = math.max(6, math.floor(track_h * (visible_limit / #self.suggestions)))
		local max_start = math.max(1, #self.suggestions - visible_limit + 1)
		local scroll_ratio = 1 - ((self.suggestion_view_start - 1) / math.max(1, max_start - 1))
		local thumb_y = track_y + math.floor((track_h - thumb_h) * scroll_ratio)

		renderer.draw_rect(track_x, thumb_y, suggestion_scrollbar_width, thumb_h, style.scrollbar2)
	end

	-- draw suggestion text
	local text_clip_w = rw - ((#self.suggestions > visible_limit) and suggestion_scrollbar_width or 0)
	core.push_clip_rect(rx, ry, text_clip_w, rh)

	local start_index = self.suggestion_view_start
	local end_index = math.min(#self.suggestions, start_index + visible_limit - 1)
	local row = 1

	for item_index = start_index, end_index do
		local item = self.suggestions[item_index]
		local color = (item_index == self.suggestion_idx) and style.accent or style.text
		local y = self.position.y - row * lh - dh

		common.draw_text(self:get_font(), color, item.text, nil, x, y, 0, lh)

		if item.info then
			local w = text_clip_w - (x - rx) - style.padding.x
			common.draw_text(self:get_font(), style.dim, item.info, "right", x, y, w, lh)
		end

		row = row + 1
	end

	core.pop_clip_rect()
end

-- Draws the command view and schedules the suggestions overlay draw
function CommandView:draw()
	CommandView.super.draw(self)
	core.workspace_view:defer_draw(draw_suggestions_box, self)
end

return CommandView
