import toposort from 'toposort'

extern Map
extern Set

export : define [CreateEmptyTable] : new LayoutTable

class LayoutTable
	public [new] : begin
		set this.languages   {.}
		set this.features    {.}
		set this.lookups     {.}
		set this.lookupDep   {}
		set this.lookupOrder {}

	public [pickLanguage tag] : begin
		if this.languages.(tag) : return this.languages.(tag)
		define lang : new LayoutLanguage tag
		set this.languages.(tag) lang
		return lang

	public [copyLanguage tag tagFrom] : begin
		define langFrom : this.pickLanguage tagFrom
		define langTo : this.pickLanguage tag
		foreach [feat : items-of langFrom.features] : langTo.features.push feat
		return langTo

	public [createFeature tag] : begin
		local feature : new LayoutFeature tag
		local n 0
		while true : begin
			if [not this.features.(tag + '_' + n)] : begin
				set feature.name : tag + '_' + n
				set this.features.(tag + '_' + n) feature
				return feature
			set n : n + 1

	public [addCommonFeature fea] : begin
		define dfltDflt : this.pickLanguage 'DFLT_DFLT'
		define latnDflt : this.pickLanguage 'latn_DFLT'
		define grekDflt : this.pickLanguage 'grek_DFLT'
		define cyrlDflt : this.pickLanguage 'cyrl_DFLT'

		dfltDflt.addFeature fea
		latnDflt.addFeature fea
		grekDflt.addFeature fea
		cyrlDflt.addFeature fea

		return fea

	public [createLookup data _prefix] : begin
		local prefix : _prefix || 'lookup/'
		local n 0
		while true : begin
			if [not this.lookups.(prefix + n)] : begin
				return : this.addLookupNoCheck (prefix + n) data
			set n : n + 1

	public [pickLookup name fallback] : begin
		if this.lookups.(name) : return this.lookups.(name)
		return : this.addLookupNoCheck name fallback

	public [addLookupNoCheck name data] : begin
		local lookup : Object.fromEntries : Object.entries data
		set lookup.name name
		set this.lookups.(name) lookup
		return lookup

	public [setDependency a b] : begin
		if [not a.name] : throw : new Error "Invalid lookup"
		if [not b.name] : throw : new Error "Invalid lookup"

		this.lookupDep.push { a.name b.name }

	public [beginBlock] : begin
		return : object
			existingLookupNames : new Set : Object.keys this.lookups

	public [endBlock rec] : begin
		local currentLookupNames : new Set : Object.keys this.lookups
		foreach existing rec.existingLookupNames : foreach current currentLookupNames
			if (![IsUtilityLookupId existing] && ![IsUtilityLookupId current] && ![rec.existingLookupNames.has current])
				this.lookupDep.push { existing current }

	public [endBlockAtFront rec] : begin
		local currentLookupNames : new Set : Object.keys this.lookups
		foreach existing rec.existingLookupNames : foreach current currentLookupNames
			if (![IsUtilityLookupId existing] && ![IsUtilityLookupId current] && ![rec.existingLookupNames.has current])
				this.lookupDep.push { current existing }

	public [finalize] : begin
		local lns : new Set : Object.keys this.lookups
		foreach [front lns] : foreach [rear lns]
			if (![IsUtilityLookupId front] && [IsUtilityLookupId rear])
				this.lookupDep.push { front rear }

		set this.lookupOrder : toposort this.lookupDep
		foreach [{key lang} : pairs-of this.languages] : begin
			if lang.features : lang.features.sort

	public [ChainRuleBuilder] : begin
		local table this

		define [createNewLookup f t] : begin
			local subst {.}
			foreach [j : range 0 f.length] : set subst.(f.(j)) t.(j)
			return : table.createLookup {.type 'gsub_single' .substitutions subst} UtilityLookupPrefix

		define [getSubLookup left right] : piecewise
			[not right] null
			([typeof right] === "string") : throw : new Error "Invalid substitution"
			(right <@ Function) : getSubLookup left [right left]
			true : begin
				local found    null
				local maxMatch 0

				local lookupKeys : [Object.keys table.lookups].reverse
				foreach [name : items-of lookupKeys] : if [IsUtilityLookupId name] : begin
					local lookup table.lookups.(name)
					local st lookup.substitutions

					local compatible true
					local matchCount 0
					foreach [j : range 0 left.length] : begin
						if (st.(left.(j)) && st.(left.(j)) !== right.(j)) : set compatible false
						if (st.(left.(j)) === right.(j)) : inc matchCount

					if (compatible && (!found || matchCount > maxMatch)) : begin
						set found    lookup
						set maxMatch matchCount

				if found : begin
					local st found.substitutions
					foreach [j : range 0 left.length] : set st.(left.(j)) right.(j)
					return found

				return : createNewLookup left right

		define [chain-rule] : begin
			local terms : [{}.slice.call arguments 0].map (x -> [if x.left x {.left x .right null}])
			local rule {.match {} .apply {} .inputBegins 0 .inputEnds 0}
			local foundi false
			local founde false
			foreach [j : range 0 terms.length] : if (!foundi && terms.(j).right) : begin
				set rule.inputBegins j
				set foundi true
			foreach [j : range (terms.length - 1) downtill 0] : if (!founde && terms.(j).right) : begin
				set rule.inputEnds (j + 1)
				set founde true
			foreach [j : range 0 terms.length] : begin
				local term terms.(j)
				rule.match.push : Array.from : new Set term.left
				local lookup : getSubLookup term.left term.right
				if lookup : rule.apply.push {.at j .lookup {.name lookup.name}}
			return rule

		define [reverse-rule] : begin
			local terms : [{}.slice.call arguments 0].map (x -> [if x.left x {.left x .right null}])
			local rule {.match {} .to {} .inputIndex 0}
			local foundi false

			foreach [j : range 0 terms.length] : begin
				local term terms.(j)
				local subst : new Map
				foreach [g : items-of term.left] : subst.set g g

				if term.right : begin
					if foundi : throw : new Error "Duplicate substitutions in one reverse rule"
					set foundi true
					set rule.inputIndex j
					local toGlyphs : piecewise
						(term.right <@ Function) [term.right term.left]
						true                     term.right
					foreach [k : range 0 term.left.length]
						subst.set term.left.(k) (toGlyphs.(k) || term.left.(k))

					set rule.to : Array.from [subst.values]

				set rule.match.(j) : Array.from [subst.keys]

			return rule

		return {chain-rule reverse-rule}

class LayoutFeature
	public [new tag] : begin
		set this.tag     tag
		set this.name    tag
		set this.lookups {}

	public [addLookup lookup] : begin
		if [not lookup.name] : throw : new Error "Invalid lookup"
		define index : this.lookups.indexOf lookup.name
		if (index < 0) : this.lookups.push lookup.name
		return lookup

class LayoutLanguage
	public [new tag] : begin
		set this.tag       tag
		set this.features  {}

	public [addFeature feature] : begin
		if [not feature.name] : throw : new Error "Invalid feature"
		define index : this.features.indexOf feature.name
		if (index < 0) : this.features.push feature.name
		return feature

define UtilityLookupPrefix '.utility-single/'
define [IsUtilityLookupId name] : [name.slice 0 UtilityLookupPrefix.length] === UtilityLookupPrefix

export : define [FinalizeTable table] : begin
	set table.lookupOrder : toposort table.lookupDep
	foreach [{key lang} : pairs-of table.languages] : begin
		if lang.features : lang.features.sort

export : define [UkMapToLookup m] : begin
	local l {}
	local r {}
	foreach { k v } [Object.entries m] : begin
		l.push k
		r.push v
	return { .left l .right r }

export : define [UkLigatureToLookup data mapper] : begin
	local substitutions {}
	foreach { from to } [items-of data] : begin
		local allFound true
		local fromGlyphNames {}
		local toGlyph null

		foreach u [items-of from] : begin
			local gn : mapper u
			if gn
			: then : fromGlyphNames.push gn
			: else : set allFound false

		begin
			local gn : mapper to
			if gn
			: then : set toGlyph gn
			: else : set allFound false

		if allFound : substitutions.push {.from fromGlyphNames .to toGlyph}

	return substitutions
