local ex = {} -- normally called `export` but there are so many references to exported functions in this module

local put_module = "Module:parse utilities"
local romut_module = "Module:romance utilities"
local strutil_module = "Module:string utilities"

local m_str_utils = require(strutil_module)

local u = m_str_utils.char
local rfind = m_str_utils.find
local rsubn = m_str_utils.gsub
local rsplit = m_str_utils.split
local toNFC = mw.ustring.toNFC
local toNFD = mw.ustring.toNFD

local prepositions = {
	-- a, da + optional article
	"d?al? ",
	"d?all[oae] ",
	"d?all'",
	"d?ai ",
	"d?agli ",
	-- di, in + optional article
	"di ",
	"d'",
	"in ",
	"[dn]el ",
	"[dn]ell[oae] ",
	"[dn]ell'",
	"[dn]ei ",
	"[dn]egli ",
	-- su + optional article
	"su ",
	"sul ",
	"sull[oae] ",
	"sull'",
	"sui ",
	"sugli ",
	-- others
	"come ",
	"con ",
	"per ",
	"tra ",
	"fra ",
}


-- version of rsubn() that discards all but the first return value
function ex.rsub(term, foo, bar)
	local retval = rsubn(term, foo, bar)
	return retval
end

-- version of rsubn() that returns a 2nd argument boolean indicating whether
-- a substitution was made.
function ex.rsubb(term, foo, bar)
	local retval, nsubs = rsubn(term, foo, bar)
	return retval, nsubs > 0
end

-- apply rsub() repeatedly until no change
function ex.rsub_repeatedly(term, foo, bar)
	while true do
		local new_term = ex.rsub(term, foo, bar)
		if new_term == term then
			return term
		end
		term = new_term
	end
end


---------------------- Pronunciation -----------------

ex.AC = u(0x301)
ex.GR = u(0x300)
ex.CFLEX = u(0x302)
ex.DOTOVER = u(0x0307) -- dot over =  ̇ = signal unstressed word
ex.DOTUNDER = u(0x0323) -- dot under =  ̣ = unstressed vowel with quality marker
ex.LINEUNDER = u(0x0331) -- line under =  ̱ = secondary-stressed vowel with quality marker
ex.DIA = u(0x0308) -- diaeresis = ̈
ex.TIE = u(0x0361) -- tie =  ͡
ex.stress = "ˈˌ"
ex.stress_c = "[" .. ex.stress .. "]"
ex.quality = ex.AC .. ex.GR
ex.quality_c = "[" .. ex.quality .. "]"
ex.accent = ex.stress .. ex.quality .. ex.CFLEX .. ex.DOTOVER .. ex.DOTUNDER .. ex.LINEUNDER
ex.accent_c = "[" .. ex.accent .. "]"

-- Apply canonical Unicode decomposition to text, e.g. è → e + ◌̀. But recompose ö and ü so we can treat them as single
-- vowels, and put ex.LINEUNDER/ex.DOTUNDER/ex.DOTOVER after acute/grave (canonical decomposition puts ex.LINEUNDER and ex.DOTUNDER
-- first).
function ex.decompose(text)
	text = toNFD(text)
	text = ex.rsub(text, "." .. ex.DIA, {
		["o" .. ex.DIA] = "ö",
		["O" .. ex.DIA] = "Ö",
		["u" .. ex.DIA] = "ü",
		["U" .. ex.DIA] = "Ü",
	})
	text = ex.rsub(text, "([" .. ex.LINEUNDER .. ex.DOTUNDER .. ex.DOTOVER .. "])(" .. ex.quality_c .. ")", "%2%1")
	return text
end

-- Apply canonical Unicode composition to text, e.g. e + ◌̀ → è.
function ex.compose(text)
	return toNFC(text)
end

-- Split into words. Hyphens separate words but not when used to denote affixes, i.e. hyphens between non-spaces
-- separate words. Return value includes alternating words and separators. Use table.concat(words) to reconstruct
-- the initial text.
function ex.split_but_rejoin_affixes(text)
	if not rfind(text, "[%s%-]") then
		return {text}
	end
	-- First replace hyphens separating words with a special character. Remaining hyphens denote affixes and don't
	-- get split. After splitting, replace the special character with a hyphen again.
	local TEMP_HYPH = u(0xFFF0)
	text = ex.rsub_repeatedly(text, "([^%s])%-([^%s])", "%1" .. TEMP_HYPH .. "%2")
	local words = rsplit(text, "([%s" .. TEMP_HYPH .. "]+)")
	for i, word in ipairs(words) do
		if word == TEMP_HYPH then
			words[i] = "-"
		end
	end
	return words
end

function ex.remove_secondary_stress(text)
	local words = ex.split_but_rejoin_affixes(text)
	for i, word in ipairs(words) do
		if (i % 2) == 1 then -- an actual word, not a separator
			-- Remove unstressed quality marks.
			word = ex.rsub(word, ex.quality_c .. ex.DOTUNDER, "")
			-- Remove secondary stresses. Specifically:
			-- (1) Remove secondary stresses marked with ex.LINEUNDER if there's a previously stressed vowel.
			-- (2) Otherwise, just remove the ex.LINEUNDER, leaving the accent mark, which will then be removed if there's
			--     a following stressed vowel, but left if it's the only stress in the word, as in có̱lle = con le.
			--     (In the process, we remove other non-stress marks.)
			-- (3) Remove stress mark if there's a following stressed vowel.
			word = ex.rsub_repeatedly(word, "(" .. ex.quality_c .. ".*)" .. ex.quality_c .. ex.LINEUNDER, "%1")
			word = ex.rsub(word, "[" .. ex.CFLEX .. ex.DOTOVER .. ex.DOTUNDER .. ex.LINEUNDER .. "]", "")
			word = ex.rsub_repeatedly(word, ex.quality_c .. "(.*" .. ex.quality_c .. ")", "%1")
			words[i] = word
		end
	end
	return table.concat(words)
end

-- Remove all accents. NOTE: `text` on entry must be decomposed using decompose().
function ex.remove_accents(text)
	return ex.rsub(text, ex.accent_c, "")
end

-- Remove non-word-final accents. NOTE: `text` on entry must be decomposed using decompose().
function ex.remove_non_final_accents(text)
	local words = ex.split_but_rejoin_affixes(text)
	for i, word in ipairs(words) do
		if (i % 2) == 1 then -- an actual word, not a separator
			word = ex.rsub_repeatedly(word, ex.accent_c .. "(.)", "%1")
			words[i] = word
		end
	end
	return table.concat(words)
end


---------------------- References -----------------

function ex.parse_abbreviated_references_spec(spec)
	local spec_before_modifiers, modifiers = spec:match("^(.-)(<<.*>>)$")
	if spec_before_modifiers then
		spec = spec_before_modifiers
	else
		modifiers = ""
	end
	local template_name, props = spec:match("^([^:]+):(.*)$")
	if not template_name then
		template_name = spec
		props = ""
	else
		if props:find(",%s") then
			props = require(put_module).split_on_comma(props)
		else
			props = rsplit(props, ",")
		end
		for i, prop in ipairs(props) do
			if prop:find("#") then
				local param, val = prop:match("^(.-)#(.*)$")
				props[i] = "|" .. param .. "=" .. val
			else
				props[i] = "|" .. prop
			end
		end
		props = table.concat(props)
	end
	if template_name == "" and props == "" then
		return modifiers
	else
		return mw.getCurrentFrame():preprocess(("{{R:it:%s%s}}"):format(template_name, props)) .. modifiers
	end
end


---------------------- Inflection -----------------

-- Given a term `term`, if the term is multiword (either through spaces or hyphens), handle inflection of the term by
-- calling handle_multiword() in [[Module:romance utilities]]. `special` indicates which parts of the multiword term to
-- inflect, and `inflect` is a function of one argument to inflect the individual parts of the term. As an optimization,
-- if the term is not multiword and `special` is not given, do nothing.
local function call_handle_multiword(term, special, inflect)
	if not special and not term:find("[ %-]") then
		return nil
	end
	local retval = require(romut_module).handle_multiword(term, special, inflect, prepositions)
	if retval and #retval > 0 then
		if #retval ~= 1 then
			error("Internal error: Should have only one return value from inflection function: " .. table.concat(retval, ","))
		end
		return retval[1]
	end
	return nil
end

-- Generate a default plural form, which is correct for most regular nouns and adjectives.
function ex.make_plural(term, gender, special)
	local plspec
	if special == "cap*" or special == "cap*+" then
		plspec = special
		special = nil
	end
	local retval = call_handle_multiword(term, special, function(term) return ex.make_plural(term, gender, plspec) end)
	if retval then
		return retval
	end

	local function check_no_mf()
		if gender == "mf" or gender == "mfbysense" or gender == "?" then
			error("With gender=" .. gender .. ", unable to pluralize term '" .. term .. "'"
				.. (special and " using special=" .. special or "") .. " because its plural is gender-specific")
		end
	end

	if plspec == "cap*" or plspec == "cap*+" then
		check_no_mf()
		if not term:find("^capo") then
			error("With special=" .. plspec .. ", term '" .. term .. "' must begin with capo-")
		end
		if gender == "m" then
			term = term:gsub("^capo", "capi")
		end
		if plspec == "cap*" then
			return term
		end
	end

	if term:find("io$") then
		term = term:gsub("io$", "i")
	elseif term:find("ologo$") then
		term = term:gsub("o$", "i")
	elseif term:find("[ia]co$") then
		term = term:gsub("o$", "i")
	-- Of adjectives in -co but not in -aco or -ico, there are several in -esco that take -eschi, and various
	-- others that take -chi: [[adunco]], [[anficerco]], [[azteco]], [[bacucco]], [[barocco]], [[basco]],
	-- [[bergamasco]], [[berlusco]], [[bianco]], [[bieco]], [[bisiacco]], [[bislacco]], [[bisulco]], [[brigasco]],
	-- [[brusco]], [[bustocco]], [[caduco]], [[ceco]], [[cecoslovacco]], [[cerco]], [[chiavennasco]], [[cieco]],
	-- [[ciucco]], [[comasco]], [[cosacco]], [[cremasco]], [[crucco]], [[dificerco]], [[dolco]], [[eterocerco]],
	-- [[etrusco]], [[falisco]], [[farlocco]], [[fiacco]], [[fioco]], [[fosco]], [[franco]], [[fuggiasco]], [[giucco]],
	-- [[glauco]], [[gnocco]], [[gnucco]], [[guatemalteco]], [[ipsiconco]], [[lasco]], [[livignasco]], [[losco]], 
	-- [[manco]], [[monco]], [[monegasco]], [[neobarocco]], [[olmeco]], [[parco]], [[pitocco]], [[pluriconco]], 
	-- [[poco]], [[polacco]], [[potamotoco]], [[prebarocco]], [[prisco]], [[protobarocco]], [[rauco]], [[ricco]], 
	-- [[risecco]], [[rivierasco]], [[roco]], [[roiasco]], [[sbieco]], [[sbilenco]], [[sciocco]], [[secco]],
	-- [[semisecco]], [[slovacco]], [[somasco]], [[sordocieco]], [[sporco]], [[stanco]], [[stracco]], [[staricco]],
	-- [[taggiasco]], [[tocco]], [[tosco]], [[triconco]], [[trisulco]], [[tronco]], [[turco]], [[usbeco]], [[uscocco]],
	-- [[uto-azteco]], [[uzbeco]], [[valacco]], [[vigliacco]], [[zapoteco]].
	--
	-- Only the following take -ci: [[biunivoco]], [[dieco]], [[equivoco]], [[estrinseco]], [[greco]], [[inequivoco]],
	-- [[intrinseco]], [[italigreco]], [[magnogreco]], [[meteco]], [[neogreco]], [[osco]] (either -ci or -chi),
	-- [[petulco]] (either -chi or -ci), [[plurivoco]], [[porco]], [[pregreco]], [[reciproco]], [[stenoeco]],
	-- [[tagicco]], [[univoco]], [[volsco]].
	elseif term:find("[cg]o$") then
		term = term:gsub("o$", "hi")
	elseif term:find("o$") then
		term = term:gsub("o$", "i")
	elseif term:find("[cg]a$") then
		check_no_mf()
		term = term:gsub("a$", (gender == "m" and "hi" or "he"))
	elseif term:find("logia$") then
		if gender ~= "f" then
			error("Term '" .. term .. "' ending in -logia should have gender=f if it is using the default plural")
		end
		term = term:gsub("a$", "e")
	elseif term:find("[cg]ia$") then
		check_no_mf()
		term = term:gsub("ia$", (gender == "m" and "i" or "e"))
	elseif term:find("a$") then
		check_no_mf()
		term = term:gsub("a$", (gender == "m" and "i" or "e"))
	elseif term:find("e$") then
		term = term:gsub("e$", "i")
	else
		return nil
	end
	return term
end

-- Generate a default feminine form.
function ex.make_feminine(term, special)
	local retval = call_handle_multiword(term, special, ex.make_feminine)
	if retval then
		return retval
	end

	-- Don't directly return gsub() because then there will be multiple return values.
	if term:find("o$") then
		term = term:gsub("o$", "a")
	elseif term:find("tore$") then
		term = term:gsub("tore$", "trice")
	elseif term:find("one$") then
		term = term:gsub("one$", "ona")
	end

	return term
end

-- Generate a default masculine form.
function ex.make_masculine(term, special)
	local retval = call_handle_multiword(term, special, ex.make_masculine)

	-- Don't directly return gsub() because then there will be multiple return values.
	if term:find("a$") then
		term = term:gsub("a$", "o")
	elseif term:find("trice$") then
		term = term:gsub("trice$", "tore")
	end

	return term
end

return ex