-- lx-bundle:true

local Object = require("core.object")
local config = require("core.config")
local lifecycle = require("core.doc.lifecycle")
local undo = require("core.doc.undo")

local Doc = Object:extend()

-- Keep Doc method ownership explicit instead of routing through install/shared
for name, fn in pairs({
	undo = undo.undo,
	redo = undo.redo,
}) do
	Doc[name] = fn
end

-- Load integrated doc monitor module without auto-start side effects
require("core.doc.monitor")

--- Initializes a document and optionally opens it from disk
-- @param filename string|nil file path
-- @param opts table|nil lifecycle options
function Doc:new(filename, opts)
	self:reset()

	-- Base documents only open from disk when a subtype provides lifecycle I/O
	if filename and type(self.open) == "function" then
		self:open(filename, opts)
	end
end

--- Reset document content, undo state, and cursor selection
function Doc:reset()
	self.lines = { "\n" }
	self.longest_line_length = 0
	self.longest_line_index = 1
	self.selection = {
		id = 1,
		start = { line = 1, col = 1 },
		stop = { line = 1, col = 1 },
	}
	self.selections = { self.selection }
	self._next_selection_id = 2
	self.undo_stack = { idx = 1 }
	self.redo_stack = { idx = 1 }
	self._history_branch = 1
	self._cached_change_id = "1:1"
	self._cached_change_branch = 1
	self._cached_change_index = 1
	self.clean_change_id = self._cached_change_id
	self._text_revision = (self._text_revision or -1) + 1	-- Keep cache revisions moving across reloads
	self.content_size_bytes = 0	-- Exact loaded size becomes a cheap running estimate after edits
	self.content_size_change_time = 0
	self.content_size_known = false

	self.file_bom = false
	self.file_encoding = "utf8"
	self.file_codepage = nil
	self.file_decode_errors = false
	self.file_decode_error_source = nil
	self.is_actual_file = false
	self.source_filename = nil
	self.source_read_only = false
	self.is_binary = false
	self.is_image = false
	self.binary_source_data = nil -- Optional generated bytes for raw views without a backing file
	self._location_source_data = nil -- Request-scoped original bytes cleared after CLI location resolution
	self._location_source_error = nil
	self.kind = "text"
	self.encoding_label = "UTF-8"
	self.codepage_label = nil
	self.save_disabled = false
	self.save_disabled_reason = nil
	self.save_as_required = false
	self.initial_word_wrap = nil -- Optional document-local view default supplied by generated documents
	self.action_default_mode = nil -- Source action fallback used after result-format user settings
	self.action_default_handler = nil -- Source-format user handler carried to a generated result
	self.format = nil
	self.format_detect_meta = nil
	self.highlighter = nil
	self._last_text_change = nil

	-- Track line ending mode and exact EOF EOL details for round-trips
	self.crlf = false
	self.cr_only = false
	self.clean_crlf = false
	self.clean_cr_only = false
	self.eof_eol_sequence = "\n"
	self.eof_eol_count = 1
end

--- Reset the document format when a document type supports detection
-- @return nil
function Doc:reset_format()
	return nil
end

--- Replay one undo entry for this document type
-- Base documents do not define undo replay behavior
-- @param cmd table undo entry to replay
-- @param redo_stack table target stack for inverse entries
-- @return boolean|nil true when applied, false when unsupported
function Doc:apply_undo_entry(cmd, redo_stack)
	return false
end

-- Returns the filename or a fallback label for unsaved docs
function Doc:get_name()
	return self.filename or config.default_doc_title or "untitled"
end

--- Return whether text or line endings differ from the last clean mark
-- @return boolean dirty
function Doc:is_dirty()
	-- Empty in-memory documents have no content that needs saving
	if self.is_actual_file ~= true and #self.lines == 1 and self.lines[1] == "\n" then
		return false
	end

	return self.clean_change_id ~= self:get_change_id()
		or self.clean_crlf ~= self.crlf
		or self.clean_cr_only ~= self.cr_only
end

--- Mark current text and line endings as the clean state
function Doc:clean()
	self.clean_change_id = self:get_change_id()
	self.clean_crlf = self.crlf
	self.clean_cr_only = self.cr_only
end

-- Returns the current document history identity
function Doc:get_change_id()
	local branch = self._history_branch
	local index = self.undo_stack.idx

	if self._cached_change_branch ~= branch or self._cached_change_index ~= index then
		self._cached_change_id = string.format("%d:%d", branch, index)
		self._cached_change_branch = branch
		self._cached_change_index = index
	end

	return self._cached_change_id
end

--- Return one opening decision with its unchanged native byte probe
-- @param filename string|nil file path
-- @param opts table|nil detect options
-- @return table|nil detection
-- @return string|nil err
Doc.detect_type = lifecycle.detect_type

--- Create a new text document through the universal lifecycle owner
-- @param filename string|nil synthetic filename
-- @param opts table|nil lifecycle options
-- @return table|nil doc
Doc.create = lifecycle.create

--- Open or reuse a document through the universal lifecycle owner
-- @param filename string file path
-- @param opts table|nil lifecycle options
-- @return table|nil doc
Doc.open_file = lifecycle.open_file

return Doc
