-- lx syntax
-- markdown syntax <liqube>
local syntax = require("core.syntax")
local common = require("core.common")
local quotes = require("core.quotes")

local md_cycle_heading_prefix
local md_remove_heading_prefix
local md_comment_hook
local md_outline_hook
local md_blockquote_profile
local md_inline_profile

local function add_fence_patterns(patterns, marker, entries)
	for _, entry in ipairs(entries) do
		patterns[#patterns + 1] = {
			pattern = { "%s*(" .. marker .. "%s*" .. entry[1] .. "%f[%W])", marker },
			type = { "meta" },
			syntax = entry[2],
			line_start = true,
			caseless = true,
		}
	end
	patterns[#patterns + 1] = {
		pattern = { "%s*" .. marker, marker },
		type = "string",
		line_start = true,
	}
end

local md_fence_entries = {
	{ "c%+%+", ".cpp" },
	{ "cpp", ".cpp" },
	{ "cc", ".cpp" },
	{ "cxx", ".cpp" },
	{ "csharp", ".cs" },
	{ "cmake", ".cmake" },
	{ "c#", ".cs" },
	{ "cs", ".cs" },
	{ "c", ".c" },
	{ "go", ".go" },
	{ "golang", ".go" },
	{ "html", ".html" },
	{ "htm", ".html" },
	{ "ini", ".ini" },
	{ "cfg", ".cfg" },
	{ "css", ".css" },
	{ "scss", ".scss" },
	{ "sass", ".scss" },
	{ "less", ".css" },
	{ "json", ".json" },
	{ "json5", ".json" },
	{ "javascript", ".js" },
	{ "js", ".js" },
	{ "typescript", ".js" },
	{ "ts", ".js" },
	{ "jsx", ".js" },
	{ "tsx", ".js" },
	{ "lua", ".lua" },
	{ "php", ".php" },
	{ "pascal", ".pas" },
	{ "pas", ".pas" },
	{ "python", ".py" },
	{ "py", ".py" },
	{ "reg", ".reg" },
	{ "rust", ".rs" },
	{ "rs", ".rs" },
	{ "r", ".r" },
	{ "md", ".md" },
	{ "shell", ".sh" },
	{ "bash", ".sh" },
	{ "sh", ".sh" },
	{ "zsh", ".sh" },
	{ "batch", ".bat" },
	{ "bat", ".bat" },
	{ "cmd", ".cmd" },
	{ "text", ".txt" },
	{ "txt", ".txt" },
	{ "plain", ".txt" },
	{ "yaml", "module:yaml" },
	-- Use explicit syntax modules for dialects that cannot be resolved by file extension
	{ "mysql", "module:sql_mysql" },
	{ "psql", "module:sql_pgsql" },
	{ "pgsql", "module:sql_pgsql" },
	{ "postgresql", "module:sql_pgsql" },
	{ "sqlite", "module:sql_sqlite" },
	{ "sql", ".sql" },
	{ "xml", ".xml" },
	{ "csv", ".csv" },
	{ "tsv", ".tsv" },
}

local md_line_patterns = {
	-- Horizontal rule
	{ pattern = "%-%-%-+", type = "comment", consume_line = true },
	{ pattern = "%*%*%*+", type = "comment", consume_line = true },
	{ pattern = "%_%_%_+", type = "comment", consume_line = true },
}

local md_patterns = {
	-- Escape sequence
	{ pattern = "\\.", type = "normal" },

	-- HTML comment block
	{ pattern = { "<!%-%-", "%-%->" }, type = "comment" },

	-- Unordered item: "* "
	{ pattern = "%s*(%*)%s+", type = { "bold" }, line_start = true },

	-- Ordered item: 1. 2. 10. 99. 05. (two digits)
	{ pattern = "%s*(%d%d?%.)%s+", type = { "bold" }, line_start = true },

	-- Enumeration item: "- "
	{ pattern = "%s*(%-)%s+", type = { "operator" }, line_start = true },

	-- Line numbers: ":274"
--	{ pattern = { "%:(%d-)[%]%)]", type = { "dimmed" } } },

	-- Bold+italic span
	{ pattern = "(%*%*%*)([^%*\n]-)(%*%*%*)", type = { "operator", "bold_italic", "operator" } },

	-- Bold span
	{ pattern = "(%*%*)([^%*\n]-)(%*%*)", type = { "operator", "bold", "operator" } },

	-- Italic span (single-line)
	{ pattern = "(%*)([^%*\n]-)(%*)", type = { "operator", "italic", "operator" } },

	-- Underscore emphasis span
	{ pattern = { "%_", "[%_\n]", "\\" }, type = "keyword2" },

	-- Underline span
	{ pattern = "(%<u%>)([^%*\n]-)(%<%/u%>)", type = { "operator", "underline", "operator" } },

	-- Markdown reference link: [label][id]
	{
		pattern = "(%[)([^\n%]]-)(%])(%[)([^\n%]]-)(%])",
		type = { "operator", "bold", "operator", "operator", "label", "operator" },
	},
	-- Markdown inline link: [label](url)
	{
		pattern = "(%[)([^\n%]]-)(%])(%()([^\n%)]-)(%))",
		type = { "operator", "bold", "operator", "operator", "uri", "operator" }, -- TODO: uri color
	},
	-- Markdown image link: ![label](url)
	{
		pattern = "(!%[)([^\n%]]-)(%])(%()([^\n%)]-)(%))",
		type = { "operator", "constant", "operator", "operator", "uri", "operator" }, -- TODO: uri color
	},

	-- TODO: ``` ... ``` without type specifier (currently all red)

	-- References: [name]: link
	{ pattern = "%s*(%[)(.+)(%]:)", type = { "operator", "label", "operator" }, line_start = true },

	-- TODO: [![Luacheck](https://img.shields.io/github/actions/workflow/status/lunarmodules/lua_cliargs/luacheck.yml?branch=master&label=Luacheck&logo=Lua)](https://github.com/lunarmodules/lua_cliargs/actions?workflow=Luacheck)

	-- TODO: ([#1426](https://github.com/lite-xl/lite-xl/pull/1426))

	-- TODO: Turn on or off certain plugins per syntax, e.g. #xxx colorhighlighter here
	-- (#162)
	{ pattern = "%(%#%d+%)", type = "label" },

	-- TODO: User defined syntax extensions (custom highlights on top of existing ones)
	{ pattern = "#### You:", type = "quote", line_start = true },
	{ pattern = "#### ChatGPT:", type = "quote", line_start = true },

	-- ATX heading line, up to three spaces allowed
	{ pattern = "%s?%s?%s?#.-\n", type = "heading", line_start = true },
	{ pattern = "%s?%s?%s?%>.-\n", type = "quote", line_start = true },

	-- Numeric entity
	{ pattern = "&#%d+;", type = "literal" },

	-- Hex entity
	{ pattern = "&#x[%x]+;", type = "hex" },

	-- Markdown link or image
	{ pattern = "!?%[.-%]%(.-%)", type = "function" },

	-- Bare URL
	{ pattern = "https?://%S+", type = "function" },
}

-- Keep fenced blocks low in the list to avoid unnecessary matching work
add_fence_patterns(md_patterns, "```", md_fence_entries)
add_fence_patterns(md_patterns, "~~~", md_fence_entries)

-- Keep these after fences so block delimiters win over inline tokens

-- Inline code (double backtick)
md_patterns[#md_patterns + 1] = {
	pattern = "(``)([^`\n]-)(``)",
	type = { "operator", "bold", "operator" },
}
-- Inline code (single backtick)
md_patterns[#md_patterns + 1] = {
	pattern = "(`)([^`\n]-)(`)",
	type = { "operator", "bold", "operator" },
	priority = 10,
}
-- Strikethrough span
md_patterns[#md_patterns + 1] = {
	pattern = { "~~", "~~", "\\" },
	type = "error",
}

md_blockquote_profile = function()
	local function wrap_blockquote(text)
		local s = tostring(text or "")
		if s == "" then
			return "> ", nil
		end
		return common.map_lines_keep_eol(s, function(line)
			return "> " .. line
		end), nil
	end

	local function unwrap_blockquote(text)
		local s = tostring(text or "")
		local had_any = false
		local out = common.map_lines_keep_eol(s, function(line)
			local stripped = line:gsub("^%s*>%s?", "", 1)
			if stripped ~= line then
				had_any = true
			end
			return stripped
		end)
		if not had_any then
			return nil, "wrapper mismatch"
		end
		return out, nil
	end

	return {
		input_multiline = true,
		output_multiline = true,
		wrap = wrap_blockquote,
		unwrap = unwrap_blockquote,
	}
end

md_inline_profile = function()
	local function pick_inline_fence(text)
		local s = tostring(text or "")
		local max_run = 0
		for run in s:gmatch("`+") do
			if #run > max_run then
				max_run = #run
			end
		end
		return string.rep("`", max_run + 1)
	end

	local function wrap_inline(text)
		local s = tostring(text or "")
		local fence = pick_inline_fence(s)
		if s:match("^`") or s:match("`$") then
			return fence .. " " .. s .. " " .. fence, nil
		end
		return fence .. s .. fence, nil
	end

	local function unwrap_inline(text)
		local s = tostring(text or "")
		local fence, body = s:match("^(`+)(.*)%1$")
		if not fence then
			return nil, "wrapper mismatch"
		end
		return body, nil
	end

	return {
		input_multiline = false,
		output_multiline = false,
		wrap = wrap_inline,
		unwrap = unwrap_inline,
	}
end

-- Markdown syntax
syntax.add({
	name = "Markdown Document",
	shortname = "Markdown",
	extensions = { "md", "markdown" },
	author = "liqube",
	created = "2026-02-16",

	line_patterns = md_line_patterns,
	patterns = md_patterns,

	-- Custom comment hook that conveniently produces headings from lines
	comment_hook = function(action, text)
		return md_comment_hook(action, text)
	end,

	outline_hook = function(action, text)
		return md_outline_hook(action, text)
	end,

	outline_patterns = {
		{ pattern = "%s?%s?%s?#%s+", line_start = true, outline_level = 1 }, -- H1
		{ pattern = "%s?%s?%s?##%s+", line_start = true, outline_level = 2 }, -- H2
		{ pattern = "%s?%s?%s?###%s+", line_start = true, outline_level = 3 }, -- H3
		{ pattern = "%s?%s?%s?####%s+", line_start = true, outline_level = 4 }, -- H4
		{ pattern = "%s?%s?%s?#####%s+", line_start = true, outline_level = 5 }, -- H5
		{ pattern = "%s?%s?%s?######%s+", line_start = true, outline_level = 6 }, -- H6
		--	{ pattern = "%s?%s?%s?>%s?([^%s>].*)", line_start = true, block_start_only = true }, -- Level 1 quotes
		{ pattern = "%s?%s?%s?>%s?", line_start = true, block_start_only = true }, -- Quotes
	},

	quotes = {
		primary = "blockquote",
		secondary = "inline",
		profiles = {
			blockquote = md_blockquote_profile(),
			inline = md_inline_profile(),
		},
	},
	symbols = {},
})

-- Cycles a line through markdown heading levels:
-- plain text -> '#' -> '##' -> '###' -> '####' -> plain text
-- Leading whitespace is ignored so toggling is stable on indented lines
md_cycle_heading_prefix = function(line)
	local s = line:gsub("^%s*", "")
	local max_levels = 4
	if s == "" then
		return s
	end
	local hashes, body = s:match("^(#+)%s+(.*)$")
	if hashes then
		local level = #hashes
		if level >= max_levels then
			return body
		end
		if body == "" then
			return string.rep("#", level + 1) .. " "
		end
		return string.rep("#", level + 1) .. " " .. body
	end
	return "# " .. s
end

-- Removes leading heading markers and surrounding whitespace from one line
md_remove_heading_prefix = function(line)
	return (line:gsub("^[%s#]+", "", 1))
end

-- Reuses comment actions to provide markdown heading transforms
-- 'toggle-line'/'toggle-smart' cycle heading depth; 'uncomment' strips heading prefix
md_comment_hook = function(action, text)
	if action == "toggle-line" or action == "toggle-smart" then
		return common.map_lines_keep_eol(text, md_cycle_heading_prefix)
	end
	if action == "uncomment" then
		return common.map_lines_keep_eol(text, md_remove_heading_prefix)
	end
	return nil
end

-- Post-processes outliner row text for markdown:
-- when a line starts with '# '..'###### ' (after left trim), prefix equal spaces
-- to create pseudo-indentation by heading depth in the outline popup
md_outline_hook = function(action, text, level)
	if action ~= "format" then
		return nil
	end
	if type(level) == "number" and level > 0 and level <= 6 then
		return string.rep(" ", level) .. text:gsub("^%s*", "")
	end
	local len = #text
	local pos = 1
	while pos <= len and text:byte(pos) == 32 do
		pos = pos + 1
	end

	local hash_start = pos
	while pos <= len and (pos - hash_start) < 6 and text:byte(pos) == 35 do
		pos = pos + 1
	end

	local hash_count = pos - hash_start
	if hash_count < 1 or hash_count > 6 then
		return text
	end

	if text:byte(pos) ~= 32 then
		return text
	end

	return string.rep(" ", hash_count) .. text:sub(hash_start)
end
