local export = {}

local m_links = require('Module:links')
local m_adj = require('Module:pl-adj')
local m_g = require('Module:gender and number')
local lang = require("Module:languages").getByCode("pl")

-- local consonants = "[bcćdfghjklłmnńpqrsśtvwxzżź]";

-- case information
local cases = {
	{ key = "nom"; en = "กรรตุการก<br/>(nominative)"; pl = "mianownik (kto? co?)" },
	{ key = "gen"; en = "สัมพันธการก <br/> (genitive)"; pl = "dopełniacz (kogo? czego?)" },
	{ key = "dat"; en = "สัมปทานการก <br/> (dative)"; pl = "celownik (komu? czemu?)" },
	{ key = "acc"; en = "กรรมการก <br/> (accusative)"; pl = "biernik (kogo? co?)" },
	{ key = "ins"; en = "กรณการก <br/> (instrumental)"; pl = "narzędnik (kim? czym?)" },
	{ key = "loc"; en = "อธิกรณการก <br/> (locative)"; pl = "miejscownik (o kim? o czym?)" },
	{ key = "voc"; en = "สัมโพธนการก <br/> (vocative)"; pl = "wołacz (o!)" },
}

-- columns for normal nouns
local noun_cols = {
	{ key = "s"; title = "เอกพจน์" },
	{ key = "p"; title = "พหูพจน์" },
}

-- columns for Old Polish nouns
local noun_dual_cols = {
	{ key = "s"; title = "เอกพจน์" },
	{ key = "d"; title = "ทวิพจน์" },
	{ key = "p"; title = "พหูพจน์" },
}

-- columns for pronouns
local pronoun_cols = {
	{ key = "sm"; title = m_g.format_list({"m"}, lang) },
	{ key = "sf"; title = m_g.format_list({"f"}, lang) },
	{ key = "sn"; title = m_g.format_list({"n"}, lang) },
	{ key = "pm"; title = m_g.format_list({"vr-p"}, lang) },
	{ key = "po"; title = m_g.format_list({"nv-p"}, lang) },
}

local altsep = "/"

function empty_item(text)
	return (not text) or text == "" or text == "-" or text == "–" or text == "—"
end

-- add link markers, with links separated by any of splitchars
-- exported for use in testcases
-- normally the separator is altsep
function export.make_links(text, splitchars, title)
	if not title then
		title = mw.title.getCurrentTitle().fullText
	end
	if empty_item(text) then
		return "—"
	elseif not splitchars or splitchars == "" then
		return ("[[%s#ภาษาโปแลนด์|%s]]"):format(text, text)
	else
		items = {}
		for word in mw.ustring.gmatch(text, "[^" .. splitchars .. "]+") do
			add_archaic = ""
			if word:sub(-string.len(" (โบราณ)")) == " (โบราณ)" then
				word = (mw.text.split(word, "% %(โบราณ%)"))[1]
				add_archaic = " (โบราณ)"
			end
			
			if word == title then
				table.insert(items, ("[[%s]]"):format(word) .. add_archaic)
			else
				table.insert(items, ("[[%s#ภาษาโปแลนด์|%s]]"):format(word, word) .. add_archaic)
			end
		end
		
		if #items > 1 then
			require("Module:debug").track("pl-noun/splitchars")
		end
		
		return table.concat(items, "/")
	end
end

local function linkify_info(declinfo, splitchars, nolinks)
	if nolinks then
		require("Module:debug").track("pl-noun/nolinks")
	end
	
	local linked = {}
	local title = mw.title.getCurrentTitle().fullText
	for k, v in pairs(declinfo) do
		if v == title then
			linked[k] = "[[" .. v .. "]]"
		elseif nolinks then
			linked[k] = v
		else
			linked[k] = export.make_links(v, splitchars, title)
		end
	end
	return linked
end

local function nowiki_info(declinfo)
	require("Module:debug").track("pl-noun/nowiki")
	local nowikied = {}
	for k, v in pairs(declinfo) do
		nowikied[k] = mw.text.nowiki(v)
	end
	return nowikied
end

local function override_col_titles(heads, cols)
	if not heads then
		return cols or {}
	end
	local new_cols = {}
	local index = 1
	for word in mw.ustring.gmatch(heads, "[^,]+") do
		if cols[index] then
			table.insert(new_cols, { key = cols[index].key; title = word } )
		else
			table.insert(new_cols, { key = tostring(index); title = word } )
		end
		index = index + 1
	end
	for ci, col in ipairs(cols) do
		if ci >= index then
			table.insert(new_cols, cols[index])
		end
	end
	return new_cols
end

local function normalize_tantum(pargs)
	-- support "num" as fallback for "tantum" to match Latin templates
	local tantum = pargs.tantum or pargs.num
	if not tantum then
		return nil
	end
	if tantum == "sg" then
		tantum = "s"
	elseif tantum == "pl" then
		tantum = "p"
	end
	return tantum
end

local function guess_width(declinfo, cols)
	local maxl = 0
	for k, v in pairs(declinfo) do
		local l = mw.ustring.len(mw.text.trim(v))
		if maxl < l then
			maxl = l
		end
	end
	local width = math.floor(maxl * 0.78) -- number obtained by anecdotal evidence
	width = (width < 10) and 10 or width
	width = 9 + (width * #cols)
	return width
end

-- generate the HTML code of an inflection table
-- each entry in heads must have "key" and "title"
local function make_table(declinfo, cols, preproc, width, title, tantum, nolinks)
	local result = {}
	if not cols or not cols[1] then
		error("make_table: invalid cols parameter")
	end

	local lemma_key = cases[1].key .. cols[1].key
	local lemma = m_links.remove_links(declinfo[lemma_key])
	title = title or ('การผันรูปของ <span class="Latn mention" lang="pl" xml:lang="pl">%s</span>'):format(lemma)

	local emwidth = width or guess_width(declinfo, cols)

	if preproc == "nowiki" then
		declinfo = nowiki_info(declinfo)
	elseif preproc == "linkify" then
		declinfo = linkify_info(declinfo, altsep, nolinks)
	end

	if cols and (#cols > 0) then
		table.insert(result, '|-\n! style="background:#d9ebff; width: 8em;" |\n')
		for i, col in ipairs(cols) do
			table.insert(result, ('! style="background:#d9ebff;" scope="col" | %s\n'):format(col.title))
		end
	end

	local maxl = 0
	for i, case in ipairs(cases) do
		table.insert(result, ('|-\n! title="%s" style="background:#eff7ff;" scope="row" | %s\n'):format(case.pl, case.en))
		for _, col in ipairs(cols) do
			local declkey = case.key .. col.key
			local item = mw.text.trim(declinfo[declkey])
			if empty_item(item) then
				table.insert(result, '| —\n')
			else
				table.insert(result, ('| <span class="Latn" lang="pl" xml:lang="pl">%s</span>\n'):format(item))
			end
		end
	end

	local outtext = ([=[<div class="NavFrame" style="display: block;width: %uem;">
<div class="NavHead" style="background:#eff7ff" >%s</div>
<div class="NavContent">
{| style="background:#F9F9F9; text-align:center; width: %uem; margin: 0;" class="inflection-table"
]=]):format(emwidth, title, emwidth) .. table.concat(result, "") .. "|}</div></div>"

	if tantum == "s" then
		outtext = outtext .. "[[หมวดหมู่:คำนามเอกพจน์เท่านั้นภาษาโปแลนด์]]"
	elseif tantum == "p" then
		outtext = outtext .. "[[หมวดหมู่:คำนามพหูพจน์เท่านั้นภาษาโปแลนด์]]"
	end
	return outtext
end

local function get_mode()
	local frame = mw.getCurrentFrame()
	if mw.isSubsting() then
		return 'subst'
	elseif frame:getParent():getTitle() == mw.title.getCurrentTitle().fullText then
		return 'demo'
	else
		return 'xclude'
	end
end

local function make_table_from_pargs(pargs, cols, tantum)
	local width = pargs.width and tonumber(pargs.width)
	
	local declinfo = {}
	if get_mode() == 'demo' then
		if not cols then
			cols = {
				{ key = "1"; title = "column 1" },
				{ key = "2"; title = "column 2" },
				{ key = "3"; title = "column 3" },
			}
		end
		for i = 1, 7 do
			for j, col in ipairs(cols) do
				local case_key = cases[i].key .. col.key
				local argn = (i-1) * #cols + j
				declinfo[case_key] = '{{{' .. case_key .. '| {{{' .. argn .. '}}} }}}'
			end
		end
		return make_table(declinfo, cols, "nowiki", width, nil, nil, nil)
	else
		cols = override_col_titles(pargs.heads, cols or {})
		for i = 1, 7 do
			for j, col in ipairs(cols) do
				local case_key = cases[i].key .. col.key
				local argn = ((i-1) * #cols) + j
				declinfo[case_key] = m_links.remove_links(mw.text.trim(pargs[case_key] or pargs[argn] or "-"))
			end
		end
		return make_table(declinfo, cols, "linkify", pargs.width, pargs.title, normalize_tantum(pargs), pargs.nolinks)
	end
end

-- Generate declension table for a singulare tantum with all forms passed explicitly as parameters
function export.template_decl_noun_sg(frame)
	local pargs = frame:getParent().args
	local cols = { noun_cols[1] }
	return make_table_from_pargs(pargs, cols, "s")
end

-- Generate declension table for a plurale tantum with all forms passed explicitly as parameters
function export.template_decl_noun_pl(frame)
	local pargs = frame:getParent().args
	local cols = { noun_cols[2] }
	return make_table_from_pargs(pargs, cols, "p")
end

-- Generate declension table for a regular noun with all forms passed explicitly as parameters
function export.template_decl_noun(frame)
	local pargs = frame:getParent().args
	return make_table_from_pargs(pargs, noun_cols, nil)
end

function export.template_decl_pronoun(frame)
	local pargs = frame:getParent().args
	return make_table_from_pargs(pargs, pronoun_cols, nil)
end

-- this probably belongs in a module for Old Polish (zlw-opl).
-- which reminds me: someone should write [[WT:AZLW-OPL]].
-- Generate declension table for a noun with dual number with all forms passed explicitly as parameters
function export.template_decl_noun_dual(frame)
	local pargs = frame:getParent().args
	return make_table_from_pargs(pargs, noun_dual_cols, nil)
end

function export.template_decl_generic(frame)
	local pargs = frame:getParent().args
	local heads = pargs.heads
	if not heads then
		if get_mode() == "demo" then
			heads = "column 1,column 2"
		else
			error("No column headings defined!")
		end
	end
	local cols = override_col_titles(heads, {})
	return make_table_from_pargs(pargs, cols, nil)
end


-- -----------------------------------------------------------------------------
-- -----------------------------------------------------------------------------
-- ------------- Semi-automatic generation of inflected forms ------------------
-- -----------------------------------------------------------------------------
-- -----------------------------------------------------------------------------

local function nonempty(str)
	if not str or str == "" then
		return nil
	else
		return str
	end
end

-- Generate a table contains true for each space-separated word in str
local function make_lookup_table(str)
	local ret = {}
	for i in mw.ustring.gmatch(str, "%a+") do
		ret[i] = true
	end
	return ret
end

-- table that converts nominative soft endings to their genitive form
-- required for proper functioning of masc_common
local soft_ending_lookup = {
	["ć"] = "ci";
	["dź"] = "dzi";
	["ń"] = "ni";
	["ś"] = "si";
	["ź"] = "zi";
}

-- Given a word and a lookup table of accepted endings,
-- split the word into a stem and an ending.
-- Ending is returned in genitive form, i.e. soft consonants
-- and digraphs such as ć, ś, dź are converted to midword
-- i-forms ci, si, dzi.
local function split_stem(word, lookup)
	-- match the longest possible ending
	local last, last_gen
	local limit = math.min(mw.ustring.len(word), 5)
	for i = -limit, -1 do
		if not last_gen then
			last = mw.ustring.sub(word, i)
			if lookup[last] or soft_ending_lookup[last] then
				last_gen = soft_ending_lookup[last] or last
			end
		end
	end

	if last_gen then
		local stem = mw.ustring.sub(word, 1, -mw.ustring.len(last)-1)
		return stem, last_gen
	else
		return nil, nil
	end
end

local function check_split(stem, last)
	if not stem then
		error("nil stem encountered in declension pattern")
	end
	if not last then
		error("nil ending encountered in declension pattern")
	end
end

local function handle_overrides(pargs, declinfo, ovinfo)
	-- process cases in order
	local ret = {}
	local singpl = { "s", "p" }
	for i = 1, 14 do
		local caseno = math.floor((i+1)/2)
		local sp = 2 - (i % 2)
		local case_key = cases[caseno].key .. singpl[sp]
		if pargs[case_key] then
			ret[case_key] = pargs[case_key]
			if ovinfo[case_key] then
				for dummy, v in pairs(ovinfo[case_key]) do
					ret[v] = pargs[case_key]
				end
			end
		elseif not ret[case_key] then
			ret[case_key] = declinfo[case_key]
		end
	end
	return ret
end

-- This table will hold patterns
local patterns = {}


-- -----------------------------------------------------------------------------
-- -------------------------- Masculine declension -----------------------------
-- -----------------------------------------------------------------------------

-- accepted masculine endings in genitive singular form
local masc_endings = make_lookup_table(
	"acz b bi c ch ci ciec ciel cz d dz dzi dziec ek el eł f g giel h iec ieł ik j k kiel l ł m n ni niec p pi r rz rzeł " ..
	"s si seł siec sł sm sn st sz t unek w wi x z zi ż zd zeł ziec zł zm zn")

-- common function for masculine declensions
local function masc_common(pattern, stem, last, gens_ending, noms_form, nomp_form, altgenp, explicit_stem)
	-- verify that the word ending is supported
	if not masc_endings[last] then
		-- suppress module error on the template page
		if not last or not mw.ustring.match(last, "^{{{") then
			error("Unsupported word ending: " .. (last or "nil"))
		end
	end
	local initial_last = last

	-- nominative singular
	local noms_lookup = { bi = "b"; ci = "ć"; dzi = "dź"; ni = "ń"; ["pi"] = "p"; si = "ś"; wi = "w"; zi = "ź"; }
	if not noms_form then
		noms_form = stem .. (noms_lookup[last] or last)
	end

	-- fix the only exceptions to -iec vowel elision
	if last == "iec" and (stem == "w" or stem == "p") then
		if not gens_ending and stem == "p" then
			gens_ending = "a"
		end
		stem = stem .. "ie"
		last = "c"
	end

	-- limited guessing of masculine inanimate endings here
	-- personal and animate nouns (except for ''wół'') always have "a"
	local gens_a = make_lookup_table("acz ciec ciel dzi dziec ek el eł iec ieł ik kiel giel ni niec seł siec rz rzeł zeł")
	if not gens_ending then
		if gens_a[last] then
			gens_ending = "a"
		else
			gens_ending = "u"
		end
	end

	-- handle the following things here:
	--   regular vowel elisions
	--   overlong endings used only for genitive singular guessing, e.g. acz, unek
	--   x becoming ks in inflected forms
	--   stem-final ó becoming o in inflected forms
	local last_lookup = {
		acz = "cz";
		ciec = "c"; ciel = "l";
		dziec = "c";
		ek = "k"; el = "l"; ["eł"] = "ł";
		iec = "c"; ["ieł"] = "ł"; ik = "k";
		kiel = "l";
		giel = "l";
		niec = "c";
		["rzeł"] = "ł";
		["seł"] = "sł"; siec = "c";
		unek = "k";
		x = "s";
		["zeł"] = "zł"; ziec = "c";
	}
	local stem_add = {
		acz = "a";
		ciec = "ć"; ciel = "cie";
		dziec = "dź";
		ik = "i";
		kiel = "k";
		giel = "g";
		niec = "ń";
		["rzeł"] = "r";
		siec = "ś";
		unek = "un"; 
		x = "k";
		ziec = "ź";
	}
	stem = stem .. (stem_add[last] or "")
	last = last_lookup[last] or last

	if mw.ustring.match(stem, "ó$") and not explicit_stem then
		stem = mw.ustring.sub(stem, 1, -2) .. "o"
	end

	-- genitive singular
	local gens_form = stem .. last .. gens_ending

	-- accusative singular
	local accs_form = (pattern == "m-in") and noms_form or gens_form

	-- instrumental singular
	local inss_form
	if (last == "g") or (last == "k") then
		inss_form = stem .. last .. "iem"
	else
		inss_form = stem .. last .. "em"
	end

	-- locative singular
	local locs_lookup = {
		b = "bie"; bi = "biu";
		c = "cu"; ch = "chu"; ci = "ciu"; cz = "czu";
		d = "dzie"; dz = "dzu"; dzi = "dziu";
		f = "fie";
		g = "gu";
		h = "hu";
		j = "ju";
		k = "ku";
		l = "lu"; ["ł"] = "le";
		m = "mie";
		n = "nie"; ni = "niu";
		p = "pie"; ["pi"] = "piu";
		r = "rze"; rz = "rzu";
		s = "sie"; si = "siu"; ["sł"] = "śle"; sm = "śmie"; sn = "śnie";
			st = "ście"; sz = "szu";
		t = "cie";
		w = "wie"; wi = "wiu";
		z = "zie"; zd = "ździe"; zi = "ziu"; ["zł"] = "źle"; zm = "zmie";
			zn = "źnie"; ["ż"] = "żu";
		-- the -zm ending should not be palatalized, since it normally occurs
		-- in borrowed nouns such as "marazm", "komunizm", "faszyzm"
	}
	local locs_form = stem .. (locs_lookup[last] or last)

	-- vocative singular is the same as locative singular, with the exception of -iec
	local vocs_form = locs_form
	if pattern == "m-pr" and mw.ustring.match(initial_last, "iec$") then
		vocs_form = stem .. "cze"
	end

	-- nominative plural
	-- accusative and vocative plural are the same
	if not nomp_form then
		local nomp_e_ending = make_lookup_table("bi c ci cz dz dzi j l ni pi rz si sz wi zi ż")
		if (last == "g") or (last == "k") then
			nomp_form = stem .. last .. "i"
		elseif nomp_e_ending[last] then
			nomp_form = stem ..last .. "e"
		else
			nomp_form = stem .. last .. "y"
		end
	end

	-- genitive plural
	local genp_form
	if mw.ustring.match(last, "i$") and (last ~= "pi") then
		genp_form = stem .. last
	elseif (last == "l") then
		genp_form = stem .. last .. "i"
	elseif (last == "j") then
		genp_form = stem .. last .. "ów"
		if not altgenp or (altgenp == "") then
			altgenp = stem .. "i"
		end
	elseif (last == "cz") or (last == "rz") or (last == "sz") or (last == "ż") then
		genp_form = stem .. last .. "y"
	else
		genp_form = stem .. last .. "ów"
	end
	if nonempty(altgenp) then
		genp_form = genp_form .. "/" .. altgenp
	end

	-- accusative plural - same as genitive or nominative depending on animacy
	local accp_form = (pattern == "m-pr") and genp_form or nomp_form

	return {
		noms = noms_form;             nomp = nomp_form;
		gens = gens_form;             genp = genp_form;
		dats = stem .. last .. "owi"; datp = stem .. last .. "om";
		accs = accs_form;             accp = accp_form;
		inss = inss_form;             insp = stem .. last .. "ami";
		locs = locs_form;             locp = stem .. last .. "ach";
		vocs = vocs_form;             vocp = nomp_form;
	}
end

-- masculine inanimate nouns, e.g. bruk, beton, słup, szczaw, kurnik
-- default genitive singular ending is "u", but "a" endings are common as well
patterns["m-in"] = function (pargs, word)
	local stem, last = split_stem(word, masc_endings)
	local explicit_stem = false
	if nonempty(pargs[2]) then
		stem = pargs[1] or ""
		last = pargs[2]
		explicit_stem = true
	end
	local gens_ending = pargs[3] or gens_ending
	local noms_form = nonempty(pargs[4])
	local altgenp = nonempty(pargs[5])

	local declinfo = masc_common("m-in", stem, last, gens_ending, noms_form,
		nil, altgenp, explicit_stem)
	return handle_overrides(pargs, declinfo,
		{ noms = {"accs"}; locs = {"vocs"}; nomp = {"accp", "vocp"} })
end

-- masculine animate nouns, e.g. bocian, dzik, jeleń
patterns["m-an"] = function (pargs, word)
	local stem, last = split_stem(word, masc_endings)
	local explicit_stem = false
	if nonempty(pargs[2]) then
		stem = pargs[1] or ""
		last = pargs[2]
		explicit_stem = true
	end
	local noms_form = nonempty(pargs[3])
	local altgenp = nonempty(pargs[4])
	
	local declinfo =  masc_common("m-an", stem, last, "a", noms_form,
		nil, altgenp, explicit_stem)
	return handle_overrides(pargs, declinfo,
		{ gens = {"accs"}; locs = {"vocs"}; nomp = {"accp", "vocp"} })
end

-- masculine personal nouns, e.g. policjant, sknerus, polityk
patterns["m-pr"] = function (pargs, word)
	if not pargs[1] and not pargs[2] then
		if mw.ustring.match(word, "a$") then
			return patterns["m-pr-a"](pargs, word)
		elseif mw.ustring.match(word, "[yi]$") then
			return patterns["m-pr-adj"](pargs, word)
		elseif mw.ustring.match(word, "nin$") then
			return patterns["m-pr-nin"](pargs, word)
		end
	end
	local stem, last = split_stem(word, masc_endings)
	local explicit_stem = false
	if nonempty(pargs[2]) then
		stem = pargs[1] or ""
		last = pargs[2] or ""
		explicit_stem = true
	end
	local nomp_form = nonempty(pargs[3]) or nonempty(pargs.nomp)
	local noms_form = nonempty(pargs[4]) or nonempty(pargs.noms)
	local altgenp = nonempty(pargs[5])

	-- note: this is only a default, and will need to be overridden often
	if not nomp_form then
		nomp_lookup = {
			acz = "acze"; -- palacz
			ch = "chowie"; ci = "cie"; ciec = "ćcy"; cz = "cze";
				ciel = "ciele"; -- eunuch, cieć, ?, ?, nauczyciel
			d = "dzi", dziec = "dźcy"; -- kloszard, jeździec
			ek = "kowie"; el = "ele"; -- świadek, ?
			f = "fowie"; -- szeryf
			g = "dzy"; -- szpieg
			h = "howie"; -- druh
			iec = "cy"; ik = "icy"; -- pogrobowiec, czytelnik
			j = "je"; -- lokaj
			k = "cy"; -- alkoholik
			l = "le"; ["ł"] = "łowie"; -- ?, apostoł
			m = "mi"; -- pielgrzym
			n = "ni"; ni = "nie"; niec = "ńcy"; -- kompan, leń, jeniec
			p = "pi"; -- chłop
			r = "rzy"; rz = "rze"; -- konduktor, murarz
			s = "si"; si = "sie"; siec = "ścy"; sz = "sze"; -- ordynans, rabuś, ?, jarosz
			t = "ci"; -- policjant
			z = "zi"; zi = "zie"; ziec = "źcy"; ["ż"] = "żowie"; -- intruz, kniaź, ?, mąż
		}
		if not nomp_lookup[last] then
			error("Unsupported word ending: " .. last ..
				  "; please define the nominative plural form")
		end
		if last == "g" and mw.ustring.match(stem, "lo$") then
			nomp_form = stem .. "dzy/" .. stem .. "gowie"
		else
			nomp_form = stem .. nomp_lookup[last]
		end
	end

	local declinfo = masc_common("m-pr", stem, last, "a", noms_form,
		nomp_form, altgenp, explicit_stem)
	return handle_overrides(pargs, declinfo,
		{ gens = {"accs"}; locs = {"vocs"}; nomp = {"vocp"}; genp = {"accp"} })
end

-- masculine personal nouns with adjectival declension
-- e.g. radny, łowczy, salowy
patterns["m-pr-adj"] = function (pargs, title)
	local word = pargs[1] or title
	if mw.ustring.match(word, "^{{{") then
		word = "przykładowy"
	end
	local decl = m_adj.autoinflect(word)
	local declinfo = {
		noms = decl[1];  nomp = decl[4];
		gens = decl[6];  genp = decl[8];
		dats = decl[9];  datp = decl[10];
		accs = decl[6];  accp = decl[8];
		inss = decl[12]; insp = decl[13];
		locs = decl[12]; locp = decl[8];
		vocs = decl[1];  vocp = decl[4];
	}
	return handle_overrides(pargs, declinfo,
		{ noms = {"vocs"}; gens = {"accs"}; nomp = {"vocp"}; genp = {"accp", "locp"} })
end

-- masculine personal nouns that end in -a
-- e.g. stażysta, dawca, banita
patterns["m-pr-a"] = function (pargs, word)
	local word_no_a = mw.ustring.match(word, "^(.*)a$") or ""
	local endings = make_lookup_table("b bi c ch d g j p r st t zd zn ż")
	local stem, last = split_stem(word_no_a, endings)
	stem = nonempty(pargs[1]) or stem
	last = nonempty(pargs[2]) or last

	if not endings[last] then
		-- suppress module error on the template page
		if not mw.ustring.match(last, "^{{{") then
			error("Unsupported word ending: " .. last)
		end
	end

	local gens_form = stem .. last .. "y"
	if last == "j" then
		gens_form = stem .. "i"
    elseif last == "g" then
	    gens_form = stem .. "gi"
	end
	local dats_lookup = {
		b = "bie"; -- Barnaba
		bi = "bi";
		c = "cy"; -- zdrajca
		ch = "sze"; -- monarcha
		d = "dzie"; -- nomada (?)
		g = "dze"; -- sługa
		j = "i"; -- kaznodzieja
		p = "pie"; -- satrapa
		r = "rze"; -- sknera
		st = "ście"; -- starosta
		t = "cie"; -- idiota
		zd = "ździe"; -- gazda
		zn = "źnie"; -- mężczyzna
		["ż"] = "ży"; -- doża
	}
	local dats_form = stem .. (dats_lookup[last] or last)

	local nomp_lookup = {
		b = "bowie";
		bi = "biowie";
		c = "cy";
		ch = "chowie";
		d = "dzi";
		g = "dzy";
		j = "je";
		p = "powie";
		r = "rzy";
		st = "ści";
		t = "ci";
		zd = "zdowie";
		zn = "źni";
		["ż"] = "żowie";
	}
	local nomp_form = stem .. (nomp_lookup[last] or (last .. "i"))
	local genp_form = stem .. last .. "ów"
	if last == "zn" then
		genp_form = stem .. last
	end

	-- TODO: alternative adjectival declension for -bia
	local declinfo = {
		noms = stem .. last .. "a";  nomp = nomp_form;
		gens = gens_form;            genp = genp_form;
		dats = dats_form;            datp = stem .. last .. "om";
		accs = stem .. last .. "ę";  accp = genp_form;
		inss = stem .. last .. "ą";  insp = stem .. last .. "ami";
		locs = dats_form;            locp = stem .. last .. "ach";
		vocs = stem .. last .. "o";  vocp = nomp_form;
	}
	return handle_overrides(pargs, declinfo,
		{ dats = {"locs"}; nomp = {"vocp"}; genp = {"accp"} })
end

-- masculine nouns ending in -log, both personal and inanimate
-- inanimate form is chosen when the first parameter is empty
patterns["log"] = function (pargs, title)
	local stem = nonempty(pargs[1]) or mw.ustring.match(title, "^(.*)log$") or ""
	local nomp_form = nonempty(pargs[2])
	local inanimate = not nonempty(pargs[1])

	if not nomp_form then
		if inanimate then
			nomp_form = stem .. "logi"
		else
			nomp_form = stem .. "lodzy/" .. stem .. "logowie"
		end
	end

	local gens_form = stem .. "loga"
	if inanimate then
		gens_form = stem .. "logu"
	end

	local accs_form = stem .. "loga"
	if inanimate then
		accs_form = stem .. "log"
	end

	local declinfo = {
		noms = stem .. "log";     nomp = nomp_form;
		gens = gens_form;         genp = stem .. "logów";
		dats = stem .. "logowi";  datp = stem .. "logom";
		accs = accs_form;         accp = stem .. "logów";
		inss = stem .. "logiem";  insp = stem .. "logami";
		locs = stem .. "logu";    locp = stem .. "logach";
		vocs = stem .. "logu";    vocp = nomp_form;
	}
	return handle_overrides(pargs, declinfo,
		{ locs = {"vocs"}; nomp = {"vocp"}; genp = {"accp"} })
end

-- masculine personal nouns ending with -nin, e.g. Rosjanin, łodzianin
-- popular for demonyms
patterns["m-pr-nin"] = function (pargs, word)
	local stem = nonempty(pargs[1]) or mw.ustring.match(word, "^(.*)nin$")
	local genp_ending = pargs[2] or "n"
	local declinfo = {
		noms = stem .. "nin";     nomp = stem .. "nie";
		gens = stem .. "nina";    genp = stem .. genp_ending;
		dats = stem .. "ninowi";  datp = stem .. "nom";
		accs = stem .. "nina";    accp = stem .. genp_ending;
		inss = stem .. "ninem";   insp = stem .. "nami";
		locs = stem .. "ninie";   locp = stem .. "nach";
		vocs = stem .. "ninie";   vocp = stem .. "nie";
	}
	return handle_overrides(pargs, declinfo,
		{ gens = {"accs"}; locs = {"vocs"}; nomp = {"vocp"}; genp = {"accp"} })
end

-- masculine inanimate adjectives in noun phrases
patterns["adj-m-in"] = function (pargs, word)
	word = nonempty(pargs[1]) or word
	if mw.ustring.match(word, "^{{{") then
		word = "przykładowy"
	end
	local decl = m_adj.autoinflect(word)
	local declinfo = {
		noms = decl[1];  nomp = decl[5];
		gens = decl[6];  genp = decl[8];
		dats = decl[9];  datp = decl[10];
		accs = decl[1];  accp = decl[5];
		inss = decl[12]; insp = decl[13];
		locs = decl[12]; locp = decl[8];
		vocs = decl[1];  vocp = decl[5];
	}
	return handle_overrides(pargs, declinfo,
		{ noms = {"accs", "vocs"}; inss = {"locs"}; nomp = {"accp", "vocp"}; genp = {"locp"} })
end

-- masculine animate adjectives in noun phrases
patterns["adj-m-an"] = function (pargs, word)
	word = nonempty(pargs[1]) or word
	if mw.ustring.match(word, "^{{{") then
		word = "przykładowy"
	end
	local decl = m_adj.autoinflect(word)
	local declinfo = {
		noms = decl[1];  nomp = decl[5];
		gens = decl[6];  genp = decl[8];
		dats = decl[9];  datp = decl[10];
		accs = decl[6];  accp = decl[5];
		inss = decl[12]; insp = decl[13];
		locs = decl[12]; locp = decl[8];
		vocs = decl[1];  vocp = decl[5];
	}
	return handle_overrides(pargs, declinfo,
		{ noms = {"vocs"}; gens = {"accs"}; inss = {"locs"}; nomp = {"accp", "vocp"}; genp = {"locp"} })
end

-- masculine personal adjectives in noun phrases are identical
-- to masculine personal adjectival declension


-- -----------------------------------------------------------------------------
-- -------------------------- Feminine declension ------------------------------
-- -----------------------------------------------------------------------------

local fem_endings = make_lookup_table(
	"b bi c ch ci cz d dz dzi dż f g h j k l ł m n ni p pi r rz " ..
	"s si sł sm sn st sz t w wi z zd zi zł zm zn ż")

-- most feminine nouns ending in -a
-- note that some nouns ending in -ia have this declension and others
-- follow the "f-ia" pattern - it's impossible to tell from the word alone.
patterns["f"] = function (pargs, word)
	-- to handle pluralia tantum as well
	local word_no_a = mw.ustring.match(word, "^(.*)[aeiy]$")
	local pars3 = pargs[1] and pargs[2] and pargs[3]
	local nopars = not pargs[1] and not pargs[2] and not pargs[3]

	-- 2 positional parameters given OR a at the and -> use a-final declension
	-- 3+ positional parameters given OR consonant at the end -> use f-softcons
	if nopars then
		if mw.ustring.match(word, "ia$") then
			native_ia = mw.ustring.match(word, "[bcdjklłrśtwzź]nia$")
			native_ia = native_ia or mw.ustring.match(word, "[csz]ia$")
			if not native_ia then
				return patterns["f-ia"](pargs, word)
			end
		elseif mw.ustring.match(word, "ni$") then
			return patterns["f-ni"](pargs, word)
		end
	end
	if pars3 or not word_no_a then
		return patterns["f-softcons"](pargs, word)
	end

	word_no_a = word_no_a or word
	local stem, last = split_stem(word_no_a, fem_endings)
	stem = nonempty(pargs[1]) or stem
	last = nonempty(pargs[2]) or last

	if not fem_endings[last] then
		-- suppress module error on the template page
		if not mw.ustring.match(last, "^{{{") then
			error("Unsupported word ending: " .. last)
		end
	end

	local soft_endings = make_lookup_table("bi ci dzi ni pi si wi zi")

	local gens_form = stem .. last .. "y"
	if soft_endings[last] then
		gens_form = stem .. last
	elseif last == "j" then
		if mw.ustring.match(stem, "[aąeęioóuy]$") then
			gens_form = stem .. "i"
		else
			gens_form = stem .. last .. "i"
		end
	elseif (last == "g") or (last == "k") or (last == "l") then
		gens_form = stem .. last .. "i"
	end

	local dats_lookup = {
		b = "bie"; bi = "bi";
		c = "cy"; ch = "sze"; ci = "ci"; cz = "czy";
		d = "dzie"; dz = "dzy"; dzi = "dzi"; ["dż"] = "dży";
		f = "fie";
		g = "dze";
		h = "że";
		j = "ji";
		k = "ce";
		l = "li"; ["ł"] = "le";
		m = "mie";
		n = "nie"; ni = "ni";
		p = "pie"; ["pi"] = "pi";
		r = "rze"; rz = "rzy";
		s = "sie"; si = "si"; ["sł"] = "śle"; sm = "śmie"; sn = "śnie";
			st = "ście"; sz = "szy";
		t = "cie";
		w = "wie"; wi = "wi";
		z = "zie"; zd = "ździe"; zi = "zi"; ["zł"] = "źle"; zm = "zmie";
			zn = "źnie"; ["ż"] = "ży";
		-- -zm should not be palatalized
	}
	local dats_form = stem .. (dats_lookup[last] or last)
	
	if last == "j" and mw.ustring.match(stem, "[aąeęioóuy]$") then
		dats_form = stem .. "i"
	end

	local nomp_e_ending = make_lookup_table("bi c ci cz dz dzi dż j l ni pi rz si sz wi zi ż")
	
	local nomp_form = stem .. last .. "y"

	if (last == "g") or (last == "k") then
		nomp_form = stem .. last .. "i"
	elseif nomp_e_ending[last] then
		nomp_form = stem .. last .. "e"
	end

	local genp_form = stem .. last
	if last == "j" then
		if mw.ustring.match(stem, "[aąeęioóuy]$") then
			-- zgraja, breja, żmija, koja, szuja, chryja ->
			-- zgraj, brej, żmij, koj, szuj, chryj
			genp_form = stem .. last
		else
			-- e.g. gracja, torsja
			genp_form = stem .. "ji/" .. stem .. "yj" .. " (โบราณ)";
		end
	elseif last == "l" then
		if mw.ustring.match(stem, "[aąeęioóuy]$") then
			-- hala, tabela, mila, rola, kula
			genp_form = stem .. "l"
		else
			-- hodowla, grobla, bernikla
			genp_form = stem .. "li"
		end
	elseif last == "k" then
		local kstem, klast = split_stem(stem, soft_ending_lookup)
		if kstem then
			kstem = kstem .. klast
		else
			kstem = stem
		end
		if mw.ustring.match(stem, "[aąeęioóuy]$") then
			genp_form = kstem .. "k"
		else
			genp_form = kstem .. "ek"
		end
	end

	local declinfo = {
		noms = stem .. last .. "a";  nomp = nomp_form;
		gens = gens_form;            genp = genp_form;
		dats = dats_form;            datp = stem .. last .. "om";
		accs = stem .. last .. "ę";  accp = nomp_form;
		inss = stem .. last .. "ą";  insp = stem .. last .. "ami";
		locs = dats_form;            locp = stem .. last .. "ach";
		vocs = stem .. last .. "o";  vocp = nomp_form;
	}
	return handle_overrides(pargs, declinfo,
		{ dats = {"locs"}; nomp = {"accp", "vocp"} })
end

-- feminine nouns ending with a consonant, e.g. stal, sól, brew
patterns["f-softcons"] = function (pargs, title)
	local endings = make_lookup_table("c ć cz dz dź ew iew j l ń rz ś sz w z ż ź")
	local accepted_lasts = make_lookup_table("c ci cz dz dzi j l ni rz si sz wi zi ż")
	local stem, last = split_stem(title, endings)
	local explicit_stem = false
	stem = nonempty(pargs[1]) or stem
	last = nonempty(pargs[2]) or last
	if nonempty(pargs[2]) then
		stem = pargs[1] or ""
		last = pargs[2] or ""
		explicit_stem = true
	end
	local nomp_ending = pargs[3] -- may be empty
	local noms_form = nonempty(pargs[4])

	-- Narew, brukiew, żagiew, brew - elide -e- or -ie-
	if last == "iew" or last == "ew" then
		if not noms_form then
			noms_form = stem .. last
		end
		last = "wi"
	end

	if last == "w" or last == "z" then
		last = last .. "i"
	end
	if not accepted_lasts[last] then
		-- suppress module error on the template page
		if not last or not mw.ustring.match(last, "^{{{") then
			error("Unsupported word ending: " .. (last or "nil"))
		end
	end

	-- nominative singular
	if not noms_form or (noms_form == "") then
		local noms_lookup = {
			ci = "ć"; dzi = "dź"; ni = "ń"; si = "ś"; wi = "w"; zi = "ź"; }
		noms_form = stem .. (noms_lookup[last] or last)
	end
	
	-- Stem-final ó becoming o in inflected forms
	if mw.ustring.match(stem, "ó$") and not explicit_stem then
		stem = mw.ustring.sub(stem, 1, -2) .. "o"
	end

	-- genitive singular
	local gens_form = stem .. last
	local gens_y_ending = make_lookup_table("c cz dz sz rz ż")
	if last == "j" then
		gens_form = stem .. "i"
	elseif last == "l" then
		gens_form = gens_form .. "i"
	elseif gens_y_ending[last] then
		gens_form = gens_form .. "y"
	end

	-- nominative plural
	if not nomp_ending then
		if last == "ci" then
			-- some words have -e, but this is a lot more common
			nomp_ending = ""
		else
			nomp_ending = "e"
		end
	end
	-- this is trivial, but used in 3 places in the table
	local nomp_form = stem .. last .. nomp_ending;

	local declinfo = {
		noms = noms_form;            nomp = nomp_form;
		gens = gens_form;            genp = gens_form;
		dats = gens_form;            datp = stem .. last .. "om";
		accs = noms_form;            accp = nomp_form;
		inss = stem .. last .. "ą";  insp = stem .. last .. "ami";
		locs = gens_form;            locp = stem .. last .. "ach";
		vocs = gens_form;            vocp = nomp_form;
	}
	return handle_overrides(pargs, declinfo,
		{ noms = {"accs"}; gens = {"dats", "locs", "vocs", "genp"}; nomp = {"accp", "vocp"} })
end

-- feminine nouns with adjectival declension
patterns["f-adj"] = function (pargs, title)
	local word = nonempty(pargs[1]) or title
	if mw.ustring.match(word, "^{{{") then
		word = "przykładowa"
	end
	local decl = m_adj.autoinflect(word)
	local declinfo = {
		noms = decl[2];   nomp = decl[5];
		gens = decl[7];   genp = decl[8];
		dats = decl[7];   datp = decl[10];
		accs = decl[11];  accp = decl[5];
		inss = decl[11];  insp = decl[13];
		locs = decl[7];   locp = decl[8];
		vocs = decl[2];   vocp = decl[5];
	}
	return handle_overrides(pargs, declinfo,
		{ gens = {"dats", "locs"}; accs = {"inss"}; nomp = {"accp", "vocp"}; genp = {"locp"} })
		-- "vocs" may not always be equal to "noms" (e.g. "teściowa")
end

-- feminine nouns ending with -ja, e.g. fuzja, anomizja, animacja
-- obsolete - "f" handles them as well
patterns["f-ja"] = function (pargs, title)
	local stem = nonempty(pargs[1]) or mw.ustring.match(title, "^(.*)ja$")
	local yj = "yj"
	if pargs[2] and (pargs[2] ~= "") then
		yj = "ij"	
	end
	genp_form = stem .. "ji/" .. stem .. yj .. " (โบราณ)";

	local declinfo = {
		noms = stem .. "ja";  nomp = stem .. "je";
		gens = stem .. "ji";  genp = genp_form;
		dats = stem .. "ji";  datp = stem .. "jom";
		accs = stem .. "ję";  accp = stem .. "je";
		inss = stem .. "ją";  insp = stem .. "jami";
		locs = stem .. "ji";  locp = stem .. "jach";
		vocs = stem .. "jo";  vocp = stem .. "je";
	}
	return handle_overrides(pargs, declinfo,
		{ gens = {"dats", "locs"}; nomp = {"accp", "vocp"} })
end

-- most feminine nouns ending with -ia, e.g. mafia, kopia, balia
-- note that some nouns follow the "f" pattern instead, e.g. konopia, głębia
patterns["f-ia"] = function (pargs, title)
	local stem = nonempty(pargs[1]) or mw.ustring.match(title, "^(.*)ia$")
	local oldgenp = stem .. "ij"
	if mw.ustring.match(stem, "[dtr]$") then
		oldgenp = stem .. "yj"
	end
	local genp_form = stem .. "ii/" .. oldgenp .. " (โบราณ)";
	
	local declinfo = {
		noms = stem .. "ia";  nomp = stem .. "ie";
		gens = stem .. "ii";  genp = genp_form;
		dats = stem .. "ii";  datp = stem .. "iom";
		accs = stem .. "ię";  accp = stem .. "ie";
		inss = stem .. "ią";  insp = stem .. "iami";
		locs = stem .. "ii";  locp = stem .. "iach";
		vocs = stem .. "io";  vocp = stem .. "ie";
	}
	return handle_overrides(pargs, declinfo,
		{ gens = {"dats", "locs"}; nomp = {"accp", "vocp"} })
end

-- feminine nouns ending in -ni, e.g. mistrzyni, mędrczyni
patterns["f-ni"] = function (pargs, title)
	local stem = nonempty(pargs[1]) or mw.ustring.match(title, "^(.*)ni$")
	local declinfo = {
		noms = stem .. "ni";  nomp = stem .. "nie";
		gens = stem .. "ni";  genp = stem .. "ń";
		dats = stem .. "ni";  datp = stem .. "niom";
		accs = stem .. "nię"; accp = stem .. "nie";
		inss = stem .. "nią"; insp = stem .. "niami";
		locs = stem .. "ni";  locp = stem .. "niach";
		vocs = stem .. "ni";  vocp = stem .. "nie";
	}
	return handle_overrides(pargs, declinfo,
		{ noms = {"gens", "dats", "locs", "vocs"}; nomp = {"accp", "vocp"} })
end

-- feminine adjectives in noun phrases
patterns["adj-f"] = function (pargs, title)
	local word = nonempty(pargs[1]) or title
	if mw.ustring.match(word, "^{{{") then
		word = "przykładowa"
	end
	local decl = m_adj.autoinflect(word)
	local declinfo = {
		noms = decl[2];  nomp = decl[5];
		gens = decl[7];  genp = decl[8];
		dats = decl[7];  datp = decl[10];
		accs = decl[11]; accp = decl[5];
		inss = decl[11]; insp = decl[13];
		locs = decl[7];  locp = decl[8];
		vocs = decl[2];  vocp = decl[5];
	}
	return handle_overrides(pargs, declinfo,
		{ noms = {"vocs"}; gens = {"dats", "locs"}; accs = {"inss"}; nomp = {"accp", "vocp"}; genp = {"locp"} })
end


-- -----------------------------------------------------------------------------
-- -------------------------- Neuter declension -----------------------------
-- -----------------------------------------------------------------------------

-- neuter nouns ending in -o, e.g. jajko, jarzmo, gusło, cudo
patterns["n"] = function (pargs, word)
	if pargs[1] and mw.ustring.match(pargs[1], "^{{{") then
		word = "jajko"
	end

	-- forward to other patterns depending on the last letter
	if not nonempty(pargs[1]) and not nonempty(pargs[2]) then
		if mw.ustring.match(word, "e$") then
			return patterns["n-e"](pargs, word)
		elseif mw.ustring.match(word, "ę$") then
			return patterns["n-ę"](pargs, word)
		elseif mw.ustring.match(word, "um$") then
			return patterns["n-um"](pargs, word)
		end
	end

	local word_no_o = mw.ustring.match(word, "^(.*)o$")
	local endings = make_lookup_table(
		"b bn c ch d f g gn j k kn l ł m mn n nd p r rz rzm " ..
		"s sk sł sm sn st t tt tw w wn z zd zł zm zn zz")
	local stem, last
	if word_no_o then
		stem, last = split_stem(word_no_o, endings)
	end
	stem = nonempty(pargs[1]) or stem
	last = nonempty(pargs[2]) or last
	local genp_form = nonempty(pargs[3])

	if not endings[last] then
		-- suppress module error on the template page
		if not mw.ustring.match(last, "^{{{") then
			error("Unsupported word ending: " .. last)
		end
	end

	local inss_form = stem .. last .. "em"
	if last == "g" or last == "k" then
		inss_form = stem .. last .. "iem"
	end

	local locs_lookup = {
		b = "bie"; bn = "bnie";
		c = "cu"; ch = "chu";
		d = "dzie";
		f = "fie";
		g = "gu"; gn = "gnie";
		j = "ju";
		k = "ku"; kn = "knie";
		l = "lu"; ["ł"] = "le";
		m = "mie"; mn = "mnie";
		n = "nie"; nd = "ndzie";
		p = "pie";
		r = "rze"; rz = "rzu"; rzm = "rzmie";  -- piórze, scherzu, jarzmie
		s = "sie"; sk = "sku"; ["sł"] = "śle"; sm = "śmie"; sn = "śnie"; st = "ście";
		t = "cie"; tt = "tcie"; tw = "twie";
		w = "wie"; wn = "wnie";
		z = "zie"; zd = "ździe"; ["zł"] = "źle"; zm = "źmie"; zn = "źnie"; zz = "zzu";
	}

	locs_form = stem .. (locs_lookup[last] or (last .. "u"))

	if not genp_form then
		if last == "sn" or last == "zn" then
			genp_form = stem .. mw.ustring.sub(last, 1, 1) .. "en"  -- e.g. krosno -> krosen
		elseif mw.ustring.match(last, "^.n$") then
			genp_form = stem .. mw.ustring.sub(last, 1, 1) .. "ien"  -- e.g. bagno -> bagien
		elseif last == "tw" or last == "sk" or mw.ustring.match(stem, "[aąeęioóuy]$") then
			genp_form = stem .. last  -- e.g. bogactwo -> bogactw, dyktando -> dyktand (nd treated as digraph)
		else
			if mw.ustring.match(stem, "[gk]$") then
				genp_form = stem .. "ie" .. last  -- e.g. szkło -> szkieł
			else	
				genp_form = stem .. "e" .. last  -- e.g. jajko -> jajek
			end	
		end
	end
	
	local declinfo = {
		noms = stem .. last .. "o";  nomp = stem .. last .. "a";
		gens = stem .. last .. "a";  genp = genp_form;
		dats = stem .. last .. "u";  datp = stem .. last .. "om";
		accs = stem .. last .. "o";  accp = stem .. last .. "a";
		inss = inss_form;            insp = stem .. last .. "ami";
		locs = locs_form;            locp = stem .. last .. "ach";
		vocs = stem .. last .. "o";  vocp = stem .. last .. "a";
	}
	return handle_overrides(pargs, declinfo,
		{ noms = {"accs", "vocs"}; gens = {"nomp", "accp", "vocp"}; nomp = {"accp", "vocp"} })
end

-- neuter nouns ending in -e
patterns["n-e"] = function (pargs, word)
	local word_no_e = mw.ustring.match(word, "^(.*)e$")
	local endings = make_lookup_table("bi c ci cz dz dzi fi j l mi ni pi rz si sz wi zi ż")
	local stem, last
	if word_no_e then
		stem, last = split_stem(word_no_e, endings)
	end
	if nonempty(pargs[1]) then
		stem = pargs[1]
		last = ""
	end
	
	local genp_lookup = { ci = "ć"; cz = "czy"; ni = "ń"; rz = "rzy"; sz = "szy"; ["ż"] = "ży"; }
	local genp_form = pargs[3] or (stem .. (genp_lookup[last] or last))
	local sl = stem .. last

	local declinfo = {
		noms = sl .. "e";   nomp = sl .. "a";
		gens = sl .. "a";   genp = genp_form;
		dats = sl .. "u";   datp = sl .. "om";
		accs = sl .. "e";   accp = sl .. "a";
		inss = sl .. "em";  insp = sl .. "ami";
		locs = sl .. "u";   locp = sl .. "ach";
		vocs = sl .. "e";   vocp = sl .. "a";
	}
	return handle_overrides(pargs, declinfo,
		{ noms = {"accs", "vocs"}; gens = {"nomp", "accp", "vocp"}; dats = {"locs"}; nomp = {"accp", "vocp"} })
end

-- neuter nouns ending in -ę but not in -mię, e.g. kocię, szczenię, dziecię
patterns["n-ę"] = function (pargs, title)
	local mstem = mw.ustring.match(title, "^(.*)mię$")
	if mstem then
		pargs[1] = mstem
		return patterns["n-mię"](pargs, title)
	end
	local stem = nonempty(pargs[1]) or mw.ustring.match(title, "^(.*)ę$")
	local declinfo = {
		noms = stem .. "ę";     nomp = stem .. "ęta";
		gens = stem .. "ęcia";  genp = stem .. "ąt";
		dats = stem .. "ęciu";  datp = stem .. "ętom";
		accs = stem .. "ę";     accp = stem .. "ęta";
		inss = stem .. "ęciem"; insp = stem .. "ętami";
		locs = stem .. "ęciu";  locp = stem .. "ętach";
		vocs = stem .. "ę";     vocp = stem .. "ęta";
	}
	return handle_overrides(pargs, declinfo,
		{ noms = {"accs", "vocs"}; dats = {"locs"}; nomp = {"accp", "vocp"} })
end

-- neuter nouns ending in -mię, e.g. wymię, znamię, plemię
patterns["n-mię"] = function (pargs, title)
	local stem = nonempty(pargs[1]) or mw.ustring.match(title, "^(.*)mię$")
	local declinfo = {
		noms = stem .. "mię";      nomp = stem .. "miona";
		gens = stem .. "mienia";   genp = stem .. "mion";
		dats = stem .. "mieniu";   datp = stem .. "mionom";
		accs = stem .. "mię";      accp = stem .. "miona";
		inss = stem .. "mieniem";  insp = stem .. "mionami";
		locs = stem .. "mieniu";   locp = stem .. "mionach";
		vocs = stem .. "mię";      vocp = stem .. "miona";
	}
	return handle_overrides(pargs, declinfo,
		{ noms = {"accs", "vocs"}; dats = {"locs"}; nomp = {"accp", "vocp"} })
end

-- neuter nouns with adjectival declension, e.g. bykowe
patterns["n-adj"] = function (pargs, title)
	local word = nonempty(pargs[1]) or title
	if mw.ustring.match(word, "^{{{") then
		word = "przykładowe"
	end

	local decl = m_adj.autoinflect(word)
	local declinfo = {
		noms = decl[3];   nomp = decl[5];
		gens = decl[6];   genp = decl[8];
		dats = decl[9];   datp = decl[10];
		accs = decl[3];   accp = decl[5];
		inss = decl[12];  insp = decl[13];
		locs = decl[12];  locp = decl[8];
		vocs = decl[3];   vocp = decl[5];
	}
	return handle_overrides(pargs, declinfo,
		{ noms = {"accs", "vocs"}; inss = {"locs"}; nomp = {"accp", "vocp"}; genp = {"locp"} })
end

-- neuter nouns ending in -um, e.g. liceum, gimnazjum, kryterium
-- mostly nouns borrow from ancient Greek
patterns["n-um"] = function (pargs, title)
	local stem = nonempty(pargs[1]) or mw.ustring.match(title, "^(.*)um$")
	local declinfo = {
		noms = stem .. "um";  nomp = stem .. "a";
		gens = stem .. "um";  genp = stem .. "ów";
		dats = stem .. "um";  datp = stem .. "om";
		accs = stem .. "um";  accp = stem .. "a";
		inss = stem .. "um";  insp = stem .. "ami";
		locs = stem .. "um";  locp = stem .. "ach";
		vocs = stem .. "um";  vocp = stem .. "a";
	}
	return handle_overrides(pargs, declinfo,
		{ nomp = {"accp", "vocp"} })
end

-- highly irregular nouns
patterns["irreg"] = function (pargs, title)
	
	local word = make_lookup_table("brat dziecko ksiądz książę rok")
	
	if not word[title] then
		if not mw.ustring.match(title, "^{{{") then
			error("Unsupported word")
		end
	end
	
	-- brat	
	if title == "brat" then
		noms_form = "brat"
		gens_form = "brata"
		dats_form = "bratu"
		accs_form = "brata"
		inss_form = "bratem"
		locs_form = "bracie"
		vocs_form = "bracie"
		nomp_form = "bracia"
		genp_form = "braci"
		datp_form = "braciom"
		accp_form = "braci"
		insp_form = "braćmi"
		locp_form = "braciach"
		vocp_form = "bracia"
	end
	
	-- dziecko
	if title == "dziecko" then
		noms_form = "dziecko"
		gens_form = "dziecka"
		dats_form = "dziecku"
		accs_form = "dziecko"
		inss_form = "dzieckiem"
		locs_form = "dziecku"
		vocs_form = "dziecko"
		nomp_form = "dzieci"
		genp_form = "dzieci"
		datp_form = "dzieciom"
		accp_form = "dzieci"
		insp_form = "dziećmi"
		locp_form = "dzieciach"
		vocp_form = "dzieci"
	end
	
	--ksiądz
	if title == "ksiądz" then
		noms_form = "ksiądz"
		gens_form = "księdza"
		dats_form = "księdzu"
		accs_form = "księdza"
		inss_form = "księdzem"
		locs_form = "księdzu"
		vocs_form = "księże"
		nomp_form = "księża"
		genp_form = "księży"
		datp_form = "księżom"
		accp_form = "księży"
		insp_form = "księżmi"
		locp_form = "księżach"
		vocp_form = "księża"
	end
	
	--książę
	if title == "książę" then
		noms_form = "książę"
		gens_form = "księcia"
		dats_form = "księciu"
		accs_form = "księcia"
		inss_form = "księciem"
		locs_form = "księciu"
		vocs_form = "książę"
		nomp_form = "książęta"
		genp_form = "książąt"
		datp_form = "książętom"
		accp_form = "książąt"
		insp_form = "książętami"
		locp_form = "książętach"
		vocp_form = "książęta"
	end
	
	-- rok
	if title == "rok" then
		noms_form = "rok"
		gens_form = "roku"
		dats_form = "rokowi"
		accs_form = "rok"
		inss_form = "rokiem"
		locs_form = "roku"
		vocs_form = "roku"
		nomp_form = "lata"
		genp_form = "lat"
		datp_form = "latom"
		accp_form = "lata"
		insp_form = "latami"
		locp_form = "latach"
		vocp_form = "lata"
	end	
	
	local declinfo = {
		noms = noms_form;            nomp = nomp_form;
		gens = gens_form;            genp = genp_form;
		dats = dats_form;            datp = datp_form;
		accs = accs_form;            accp = accp_form;
		inss = inss_form;            insp = insp_form;
		locs = locs_form;            locp = locp_form;
		vocs = vocs_form;            vocp = vocp_form;
	}
	return handle_overrides(pargs, declinfo, {})
end	

-- indeclinable pattern, i.e. same word for all cases
patterns["indec"] = function (pargs, title)
	local word = nonempty(pargs[1]) or title
	local declinfo = {}
	for i = 1, 7 do
		for _, col in ipairs(noun_cols) do
			declinfo[cases[i].key .. col.key] = word
		end
	end
	return handle_overrides(pargs, declinfo, {})
end

-- shorthands
patterns["nin"] = patterns["m-pr-nin"]
patterns["ia"] = patterns["fem-ia"]

-- aliases for adjectives in phrases
patterns["adj-m-pr"] = patterns["m-pr-adj"]
patterns["adj-n"] = patterns["n-adj"]

-- used for autodetection of adjective genders in phrases
local function pattern_gender(pattern)
	if pattern == "m-in" or pattern == "adj-m-in" then
		return "m-in"
	elseif pattern == "m-an" or pattern == "adj-m-an" then
		return "m-an"
	elseif string.match(pattern, "^m-pr") or pattern == "adj-m-pr" then
		return "m-pr"
	elseif string.match(pattern, "^f") or pattern == "adj-f" then
		return "f"
	elseif string.match(pattern, "^n") or pattern == "adj-n" then
		return "n"
	end
	return nil
end

-- generate inflected forms given pattern, parameters, and full word
-- returned words do not contain links
function export.autoinflect(pattern, pargs, word)
	if not pattern then
		error("Declension pattern not specified")
	end
	if mw.ustring.match(pattern, "^{{{") then
		pattern = "m-in"
	end
	if not patterns[pattern] then
		error("Invalid declension pattern: " .. pattern)
	end
	return patterns[pattern](pargs, pargs["lemma"] or word)
end

local function make_substitutable_table(pargs, declinfo, preproc)
	local tantum = normalize_tantum(pargs)
	if mw.isSubsting() then
		if tantum == "s" or tantum == "p" then
			local tname = "sing"
			if tantum == "p" then
				tname = "pl"
			end
			local rows = {}
			for i = 1, 7 do
				local case_key = cases[i].key .. tantum
				table.insert(rows, ("| <!-- %s --> %s"):format(case_key, m_links.remove_links(declinfo[case_key])))
			end
			return "{{pl-decl-noun-" .. tname .. "\n" .. table.concat(rows, "\n") .. "\n}}"
		end

		local rows = {}
		for i = 1, 7 do
			local case_skey = cases[i].key .. "s"
			local case_pkey = cases[i].key .. "p"
			table.insert(rows, ("| <!-- %s --> %s"):format(case_skey, m_links.remove_links(declinfo[case_skey])))
			table.insert(rows, ("| <!-- %s --> %s"):format(case_pkey, m_links.remove_links(declinfo[case_pkey])))
		end
		return "{{pl-decl-noun\n" .. table.concat(rows, "\n") .. "\n}}"
	else
		local cols = noun_cols
		if tantum == "s" then
			cols = { noun_cols[1] }
		elseif tantum == "p" then
			cols = { noun_cols[2] }
		end
		cols = override_col_titles(pargs.heads, cols)

		if pargs.nocat then
			tantum = nil
		end
		return make_table(declinfo, cols, preproc, pargs.width, pargs.title, tantum, pargs.nolinks)
	end
end

-- Generate declension table for a specified declension pattern
function export.template_decl_pattern(frame)
	local args = frame.args
	local pargs = frame:getParent().args
	local pagetitle = mw.title.getCurrentTitle().fullText

	-- support "num" as fallback for "tantum" to match Latin templates
	local tantum = pargs.tantum or pargs.num
	if tantum == "sg" then
		tantum = "s"
	elseif tantum == "pl" then
		tantum = "p"
	end

	if get_mode() == 'demo' then
		pargs = { "{{{1}}}", "{{{2}}}", "{{{3}}}", "{{{4}}}", "{{{5}}}" }
	end

	-- if args is empty, use pargs[1] as the pattern name and shift pargs
	local offset = 0
	local pattern = args[1]
	if not pattern then
		-- extreme brokenness: table.remove(pargs, 1) has absolutely no effect!
		-- is this because pargs is somehow immutable?
		pattern = pargs[1]
		offset = -1
	end

	local fixed_pargs = {}
	for key, parg in pairs(pargs) do
		if type(key) == "number" then
			local i = key + offset
			if i >= 1 and parg then
				fixed_pargs[i] = m_links.remove_links(parg)
			end
		else
			fixed_pargs[key] = m_links.remove_links(parg)
		end
	end

	declinfo = export.autoinflect(pattern, fixed_pargs, pagetitle)
	return make_substitutable_table(pargs, declinfo, "linkify")
end

local function push_down_indices(tbl)
	local ret = {}
	for index, decl in ipairs(tbl) do
		for k, v in pairs(decl) do
			ret[k] = ret[k] or {}
			table.insert(ret[k], v)
		end
	end
	return ret
end

-- Generate declension table for a noun phrase
function export.template_decl_phrase(frame)
	local args = frame.args
	local pargs = frame:getParent().args
	local page_title = mw.title.getCurrentTitle().fullText
	local lemma = pargs["lemma"] or page_title
	local words = {}

	if get_mode() == "demo" then
		pargs = { "adj", "n", "adj" }
		lemma = "przykładowe wyrażenie rzeczownikowe"
	end

	-- split title into words
	for t in mw.ustring.gmatch(lemma, "[^ ]+") do
		table.insert(words, t)
	end

	local word_args = {}
	for index, word in ipairs(words) do
		-- parse arguments to declension patterns
		word_args[index] = {}
		local numkey = 1
		local arg = pargs[index] or ""
		for i in mw.ustring.gmatch(arg, "[^!]+") do
			local param = mw.text.trim(i)
			local k, v = mw.ustring.match(param, "^([^:]*):(.*)$")
			if k and v then
				word_args[index][k] = v
			else
				word_args[index][numkey] = param
				numkey = numkey + 1
			end
		end
	end

	local adj_gender = nil
	for index, word in ipairs(words) do
		-- find the first gendered pattern and use it to match adjectives
		if not adj_gender then
			adj_gender = pattern_gender(word_args[index][1])
		end
	end

	local result = {}
	local result_raw = {}

	for index, word in ipairs(words) do
		local word_result = {}
		local pattern = table.remove(word_args[index], 1)
		if pattern == "adj" then
			if adj_gender then
				pattern = "adj-" .. adj_gender
			else
				error("Unable to guess adjective gender")
			end
		end
		if not patterns[pattern] then
			-- indeclinable
			pattern = "indec"
		end
		local nolinks = word_args[index].nolinks or pargs.linkfull
		result_raw[index] = export.autoinflect(pattern, word_args[index], word)
		result[index] = linkify_info(result_raw[index], altsep, nolinks)
	end

	-- rearrange the table so that word indices are at the lower level
	result_raw = push_down_indices(result_raw)
	result = push_down_indices(result)

	-- make table entries
	for case, v in pairs(result_raw) do
		local phrase = table.concat(v, " ")
		if phrase == page_title then
			-- bold self-link if the phrase is equal to the page title
			result[case] = "[[" .. page_title .. "]]"
		else
			result[case] = table.concat(result[case], " ")
			if pargs.linkfull then
				-- do not split if linking full phrase declensions
				result[case] = export.make_links(result[case], nil)
			end
		end
	end

	-- width determination
	if not pargs.width then
		local result_concat = {}
		for case, v in pairs(result_raw) do
			result_concat[case] = table.concat(v, " ")
		end
		pargs.width = guess_width(result_concat, noun_cols)
	end

	-- disable linkifying, since we already made per-word links
	return make_substitutable_table(pargs, result, nil)
end

return export