-- lx-bundle:true

--- Text editing surface for document-backed text panes
-- @module core.text.editor

local config = require("core.config")
local core = require("core")
local common = require("core.common")
local system = require("system")
local TextSurface = require("core.text.surface")
local surround = require("core.text.surround")

local TextEditor = TextSurface:extend()
local text_input_area_owner = nil	-- Editor whose IME area the operating system currently knows

--- Clip an editor input area to the visible Lua workspace
-- @param x integer area left edge
-- @param y integer area top edge
-- @param width integer area width
-- @param height integer area height
-- @param cursor integer caret offset from the left edge
-- @return boolean visible true when some of the input area remains visible
-- @return integer x clipped left edge
-- @return integer y clipped top edge
-- @return integer width clipped width
-- @return integer height clipped height
-- @return integer cursor clipped caret offset
local function clip_text_input_area_to_workspace(x, y, width, height, cursor)
	local workspace = core.workspace_view
	if not workspace or not workspace.position or not workspace.size then
		return true, x, y, width, height, cursor
	end

	local workspace_left = workspace.position.x
	local workspace_top = workspace.position.y
	local workspace_right = workspace_left + workspace.size.x
	local workspace_bottom = workspace_top + workspace.size.y
	local clipped_left = math.max(x, workspace_left)
	local clipped_top = math.max(y, workspace_top)
	local clipped_right = math.min(x + width, workspace_right)
	local clipped_bottom = math.min(y + height, workspace_bottom)

	if clipped_left >= clipped_right or clipped_top >= clipped_bottom then
		return false, 0, 0, 0, 0, 0
	end

	local absolute_cursor = x + cursor
	local clipped_width = clipped_right - clipped_left
	local clipped_cursor = common.clamp(absolute_cursor - clipped_left, 0, clipped_width)

	return true, clipped_left, clipped_top, clipped_width, clipped_bottom - clipped_top, clipped_cursor
end

--- Return key binding contexts for the text editor surface
-- The editor keeps its own context ahead of the shared document scope
-- @return table contexts ordered from most specific to broadest
function TextEditor:get_keymap_contexts()
	return { "text_editor", "doc_view", "global" }
end

--- Build one editable text surface
-- @param doc table document model
-- @param opts table|nil view construction options
function TextEditor:new(doc, opts)
	TextEditor.super.new(self, doc)
	self.gutter_visible = config.text_editor_gutter ~= false
	self.line_numbers_visible = config.text_editor_line_numbers ~= false
	self.wrap_enabled = TextSurface.get_initial_word_wrap(doc, opts, config.text_editor_word_wrap)
	self:invalidate_wrap_layout(1, #self.doc.lines, "editor_new")
end

--- Retain unfinished native text without changing the document
-- @param text string current composition text
-- @param start number selected composition character offset
-- @param length number selected composition character count
function TextEditor:on_text_editing(text, start, length)
	self.composition_text = text or ""
	self.composition_start = math.max(0, tonumber(start) or 0)
	self.composition_length = math.max(0, tonumber(length) or 0)
	core.redraw = true
end

--- Wrap selected text when its syntax owns the committed input character
-- Unsupported input continues through the ordinary text insertion path
-- @param text string committed input text
function TextEditor:on_text_input(text)
	if surround.handle_text_input(self.doc, text) then
		-- A handled pair replaces the temporary native composition
		self.composition_text = ""
		self.composition_start = 0
		self.composition_length = 0
		return
	end

	TextEditor.super.on_text_input(self, text)
end

--- Report the visible caret row to native IME composition and candidate windows
function TextEditor:update_text_input_area()
	if core.active_view ~= self then
		return
	end

	local line, col = self.doc:get_selection()
	local viewport_x, viewport_y, viewport_width, viewport_height = self:get_content_viewport_rect()
	local text_offset_x = self:get_text_x_offset()
	local line_height = self:get_line_height()
	local selection = self.doc.selection
	local preferred_row = selection and self._selection_visual_rows[selection.id] or nil
	local wrapped = self:is_wrap_enabled()
	local doc_change = self.doc.get_change_id and self.doc:get_change_id() or false
	local font = self:get_font()
	local scroll_x = self.scroll.x
	local scroll_y = self.scroll.y
	local previous = self._text_input_area_source

	-- Compare cheap source values first so an idle redraw does not measure wrapped text or call into C++
	if text_input_area_owner == self
		and previous
		and previous.line == line
		and previous.col == col
		and previous.preferred_row == preferred_row
		and previous.wrapped == wrapped
		and previous.doc_change == doc_change
		and previous.font == font
		and previous.line_height == line_height
		and previous.viewport_x == viewport_x
		and previous.viewport_y == viewport_y
		and previous.viewport_width == viewport_width
		and previous.viewport_height == viewport_height
		and previous.text_offset_x == text_offset_x
		and previous.scroll_x == scroll_x
		and previous.scroll_y == scroll_y then
		return
	end

	-- Use the same final caret position so unfinished text and native IME windows cannot drift apart
	local caret_x, row_y = self:get_caret_screen_position(line, col, preferred_row)

	local area_x = math.floor(viewport_x + text_offset_x + 0.5)
	local area_width = math.max(1, math.floor(viewport_width - text_offset_x + 0.5))
	local area_height = math.max(1, math.min(math.floor(line_height + 0.5), math.floor(viewport_height + 0.5)))
	local maximum_y = viewport_y + math.max(0, viewport_height - area_height)
	local area_y = math.floor(common.clamp(row_y, viewport_y, maximum_y) + 0.5)
	local cursor = math.floor(common.clamp(caret_x - area_x, 0, area_width) + 0.5)
	local visible
	visible, area_x, area_y, area_width, area_height, cursor = clip_text_input_area_to_workspace(
		area_x,
		area_y,
		area_width,
		area_height,
		cursor
	)
	-- A newly activated tab may draw once before the workspace has assigned its final bounds
	if not visible then
		return
	end

	local updated, update_error = system.set_text_input_area(area_x, area_y, area_width, area_height, cursor)

	-- Report a new platform failure once without flooding the log on every redraw
	if not updated and update_error ~= self._text_input_area_error then
		core.error("Native IME placement failed: %s", tostring(update_error))
	end

	if updated then
		-- Reuse one table per editor so steady drawing does not create garbage for the Lua collector
		local source = previous or {}
		source.line = line
		source.col = col
		source.preferred_row = preferred_row
		source.wrapped = wrapped
		source.doc_change = doc_change
		source.font = font
		source.line_height = line_height
		source.viewport_x = viewport_x
		source.viewport_y = viewport_y
		source.viewport_width = viewport_width
		source.viewport_height = viewport_height
		source.text_offset_x = text_offset_x
		source.scroll_x = scroll_x
		source.scroll_y = scroll_y
		self._text_input_area_source = source
		text_input_area_owner = self
	end

	self._text_input_area_error = updated and nil or update_error
end

--- Draw the editor and then publish the caret geometry from that completed layout
function TextEditor:draw()
	TextEditor.super.draw(self)
	self:update_text_input_area()
end

return TextEditor
