// SPDX-License-Identifier: Unlicense OR BSD-3-Clause

package shaping

import (
	
	
	ot 
	
	ucd 
	
	
	
)

type Input struct {
	// Text is the body of text being shaped. Only the range Text[RunStart:RunEnd] is considered
	// for shaping, with the rest provided as context for the shaper. This helps with, for example,
	// cross-run Arabic shaping or handling combining marks at the start of a run.
	Text []rune
	// RunStart and RunEnd indicate the subslice of Text being shaped.
	RunStart, RunEnd int
	// Direction is the directionality of the text.
	Direction di.Direction
	// Face is the font face to render the text in.
	Face *font.Face

	// FontFeatures activates or deactivates optional features
	// provided by the font.
	// The settings are applied to the whole [Text].
	FontFeatures []FontFeature

	// Size is the requested size of the font.
	// More generally, it is a scale factor applied to the resulting metrics.
	// For instance, given a device resolution (in dpi) and a point size (like 14), the `Size` to
	// get result in pixels is given by : pointSize * dpi / 72
	Size fixed.Int26_6

	// Script is an identifier for the writing system used in the text.
	Script language.Script

	// Language is an identifier for the language of the text.
	Language language.Language
}

// FontFeature sets one font feature.
//
// A font feature is an optionnal behavior a font might expose,
// identified by a 4 bytes [Tag].
// Most features are disabled by default; setting a non zero [Value]
// enables it.
//
// An exemple of font feature is the replacement of fractions (like 1/2, 3/4)
// by specialized glyphs, which would be activated by using
//
//	FontFeature{Tag: ot.MustNewTag("frac"), Value: 1}
//
// See also https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
// and https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_fonts/OpenType_fonts_guide
type FontFeature struct {
	Tag   ot.Tag
	Value uint32
}

// Fontmap provides a general mechanism to select
// a face to use when shaping text.
type Fontmap interface {
	// ResolveFace is called by [SplitByFace] and [Segmenter.Split] for each input rune potentially
	// triggering a face change.
	// It must always return a valid (non nil) [*font.Face] value.
	ResolveFace(r rune) *font.Face
}

// FontmapScript is an optional interface supporting script hints.
type FontmapScript interface {
	Fontmap

	// SetScript set the script to which the (next) runes passed to [ResolveFace]
	// belong.
	//
	// Providing this information before calling [ResolveFace] is useful for two reasons :
	//	- optimizing [ResolveFace] for runes with a shared script
	//	- for runes with Common and Inherited script, the script resolved by a segmentation step
	//	  is more accurate
	SetScript(language.Script)
}

var _ Fontmap = fixedFontmap(nil)

type fixedFontmap []*font.Face

// ResolveFace panics if the slice is empty
func ( fixedFontmap) ( rune) *font.Face {
	for ,  := range  {
		if ,  := .NominalGlyph();  {
			return 
		}
	}
	return [0]
}

// SplitByFontGlyphs split the runes from 'input' to several items, sharing the same
// characteristics as 'input', expected for the `Face` which is set to
// the first font among 'availableFonts' providing support for all the runes
// in the item.
// Runes supported by no fonts are mapped to the first element of 'availableFonts', which
// must not be empty.
// The 'Face' field of 'input' is ignored: only 'availableFaces' are consulted.
// Rune coverage is obtained by calling the NominalGlyph() method of each font.
// See also [SplitByFace] for a more general approach of font selection.
func ( Input,  []*font.Face) []Input {
	return SplitByFace(, fixedFontmap())
}

// SplitByFace split the runes from 'input' to several items, sharing the same
// characteristics as 'input', expected for the `Face` which is set to
// the return value of the [Fontmap.ResolveFace] call.
// The 'Face' field of 'input' is ignored: only 'availableFaces' is used to select the face.
func ( Input,  Fontmap) []Input {
	return splitByFace(, , nil)
}

// Segmenter holds a state used to split input
// according to three caracteristics : text direction (bidi),
// script, and face.
type Segmenter struct {
	// pools of inputs, used to reduce allocations,
	// which are alternatively swapped between each step of the segmentation
	input, output []Input

	// used to handle Common script
	delimStack []delimEntry

	// buffer used for bidi segmentation
	bidiParagraph bidi.Paragraph
}

type delimEntry struct {
	index  int             // in the [pairedDelims] list
	script language.Script // resolved from the context
}

// Split segments the given pre-configured input according to:
//   - text direction
//   - script
//   - language
//   - (vertical text only) glyph orientation
//   - face, as defined by [faces]
//
// Only the input runes in the range [text.RunStart] to [text.RunEnd] will be split.
//
// As a consequence, it sets the following fields of the returned runs:
//   - Text, RunStart, RunEnd
//   - Direction
//   - Script
//   - Language
//   - Face
//
// [text.Direction] is used during bidi ordering, and should refer to the general
// context [text] is used in (typically the user system preference for GUI apps.)
//
// For vertical text, if its orientation is set, is copied as it is; otherwise, the
// orientation is resolved using the Unicode recommendations (see https://www.unicode.org/reports/tr50/).
//
// When possible, the language are resolved to match the current script. For instance,
// (language: 'fr', script: 'arabic') is resolved to language: 'arabic'.
//
// The returned sliced is owned by the [Segmenter] and is only valid until
// the next call to [Split].
func ( *Segmenter) ( Input,  Fontmap) []Input {
	.reset()
	.splitByBidi() // fills output

	.input, .output = .output, .input // output is empty
	.splitByScript()

	.enforceLanguages()

	// if needed, resolve text orientation for vertical text
	if .Direction.IsVertical() && !.Direction.HasVerticalOrientation() {
		.input, .output = .output, .input
		.output = .output[:0]
		.splitByVertOrientation()
	}

	.input, .output = .output, .input
	.output = .output[:0]
	.splitByFace()

	return .output
}

func ( *Segmenter) () {
	// zero the slices to avoid 'memory leak' on pointer slice fields
	for  := range .input {
		.input[].Text = nil
		.input[].FontFeatures = nil
	}
	for  := range .output {
		.output[].Text = nil
		.output[].FontFeatures = nil
	}
	.input = .input[:0]
	.output = .output[:0]

	// bidiParagraph is reset when using SetString

	.delimStack = .delimStack[:0]
}

func ( *Segmenter) ( Input) {
	// split vertical text like horizontal one
	if .RunStart >= .RunEnd {
		.output = append(.output, )
		return
	}
	 := bidi.LeftToRight
	if .Direction.Progression() == di.TowardTopLeft {
		 = bidi.RightToLeft
	}
	.bidiParagraph.SetString(string(.Text[.RunStart:.RunEnd]), bidi.DefaultDirection())
	,  := .bidiParagraph.Order()
	if  != nil || .NumRuns() == 0 {
		.output = append(.output, )
		return
	}

	 :=  // start a rune 0 of the run
	for  := 0;  < .NumRuns(); ++ {
		 := 
		 := .Run()
		 := .Direction()
		,  := .Pos()
		 += .RunStart // shift by the input run position
		.RunEnd =  + 1

		// override the direction
		if  == bidi.RightToLeft {
			.Direction.SetProgression(di.TowardTopLeft)
		} else {
			.Direction.SetProgression(di.FromTopLeft)
		}

		.output = append(.output, )
		.RunStart = .RunEnd
	}
}

// lookupDelimIndex binary searches in the list of the paired delimiters,
// and returns -1 if `ch` is not found
func lookupDelimIndex( rune) int {
	 := 0
	 := len(pairedDelims) - 1

	for  <=  {
		 := ( + ) / 2

		if  < pairedDelims[] {
			 =  - 1
		} else if  > pairedDelims[] {
			 =  + 1
		} else {
			return 
		}
	}

	return -1
}

// See https://unicode.org/reports/tr24/#Common for reference
func ( *Segmenter) () {
	for ,  := range .input {
		 := 
		.Script = language.Common

		for  := .RunStart;  < .RunEnd; ++ {
			 := .Text[]
			 := language.LookupScript()

			// to properly handle Common script,
			// we register paired delimiters

			 := -1
			if !.Strong() {
				 = lookupDelimIndex()
			}

			if  >= 0 { // handle paired characters
				if %2 == 0 {
					// this is an open character : push it onto the stack
					.delimStack = append(.delimStack, delimEntry{, .Script})
				} else {
					// this is a close character : try to look backward in the stack
					// for its counterpart
					 :=  - 1
					 := len(.delimStack) - 1
					for ;  >= 0; -- {
						if .delimStack[].index ==  { // found a match, use its script
							 = .delimStack[].script
							break
						}
					}
					// in any case, pop the open characters
					if  == -1 {
						 = 0
					}
					.delimStack = .delimStack[:]
				}
			}

			// check if we have a 'real' change of script, or not
			if !.Strong() ||  == .Script {
				// no change
				continue
			} else if .Script == language.Common {
				// update the pair stack to attribute the resolved script
				for  := range .delimStack {
					.delimStack[].script = 
				}
				// set the resolved script to the current run,
				// but do NOT create a new run
				.Script = 
			} else {
				// split to a new run
				if  != .RunStart { // push the existing one
					.RunEnd = 
					.output = append(.output, )
				}

				.RunStart = 
				.Script = 
			}
		}
		// close and add the last input
		.RunEnd = .RunEnd
		.output = append(.output, )
	}
}

// assume splitByScript has been called and enforce sane languages
func ( *Segmenter) () {
	 := .output[0].Language

	// if no language is specified, use a default
	// known by the library
	if  == "" {
		 = "en"
	}

	,  := language.NewLangID()
	if ! {
		// the language is unknown to the library, nothing to do
		return
	}

	for ,  := range .output {
		 := enforceLang(, .Script)
		.output[].Language = .Language()
	}
}

// assume the script has been resolved
func ( *Segmenter) () {
	for ,  := range .input {
		 := ucd.LookupVerticalOrientation(.Script)
		 := 

		for  := .RunStart;  < .RunEnd; ++ {
			 := .Text[]
			 := .Orientation()
			if  == .RunStart {
				// first run : update the orientation,
				// but do not create a new run
				.Direction.SetSideways()
				continue
			}

			if  != .Direction.IsSideways() {
				// create new run : push the current one ...
				.RunEnd = 
				.output = append(.output, )

				// ... and update the 'new'
				.RunStart = 
				.Direction.SetSideways()
			}
		}

		// close and add the last input
		.RunEnd = .RunEnd
		.output = append(.output, )
	}
}

// assume [splitByScript] has been called
func ( *Segmenter) ( Fontmap) {
	,  := .(FontmapScript)
	for ,  := range .input {
		if  {
			.SetScript(.Script)
		}
		.output = splitByFace(, , .output)
	}
}

func splitByFace( Input,  Fontmap,  []Input) []Input {
	 := 
	for  := .RunStart;  < .RunEnd; ++ {
		 := .Text[]
		// We can safely ignore characters if we have a face or if there is more text,
		// but we must force the choice of a face if we still don't have one and we reach
		// the final rune. Otherwise strings like all-whitespace are never assigned a face.
		if ignoreFaceChange() && (.Face != nil ||  < .RunEnd-1) {
			// add the rune to the current input
			continue
		}

		// select the first font supporting r
		 := .ResolveFace()

		// now that we have a font, apply it back,
		// but do NOT create a new run
		if .Face == nil {
			.Face = 
		}

		if .Face ==  {
			// add the rune to the current input
			continue
		}

		// new face needed

		if  != .RunStart {
			// close the current input ...
			.RunEnd = 
			// ... add it to the output ...
			 = append(, )
		}

		// ... and create a new one
		 = 
		.RunStart = 
		.Face = 
	}

	// close and add the last input
	.RunEnd = .RunEnd
	 = append(, )
	return 
}

// ignoreFaceChange returns `true` is the given rune should not trigger
// a change of font.
//
// We don't want space characters to affect font selection; in general,
// it's always wrong to select a font just to render a space.
// We assume that all fonts have the ASCII space, and for other space
// characters if they don't, HarfBuzz will compatibility-decompose them
// to ASCII space...
//
// We don't want to change fonts for line or paragraph separators.
//
// Finaly, we also don't change fonts for what Harfbuzz consider
// as ignorable (however, some Control Format runes like 06DD are not ignored).
//
// The rationale is taken from pango : see bugs
// https://bugzilla.gnome.org/show_bug.cgi?id=355987
// https://bugzilla.gnome.org/show_bug.cgi?id=701652
// https://bugzilla.gnome.org/show_bug.cgi?id=781123
// for more details.
func ignoreFaceChange( rune) bool {
	 := ucd.LookupGeneralCategory()
	return  == ucd.Cc || // control
		 == ucd.Cs || // surrogate
		 == ucd.Zl || // line separator
		 == ucd.Zp || // paragraph separator
		( == ucd.Zs &&  != '\u1680') || // space separator != OGHAM SPACE MARK
		harfbuzz.IsDefaultIgnorable()
}

// enforceLang makes sure the returned language is compatible with
// the given script, so that it maybe be usefull for Opentype shaping.
//
// A typical example is a Korean text found into a French paragraph :
//
//	enforceLang("fr", language.Hangul) -> "ko"
func enforceLang( language.LangID,  language.Script) language.LangID {
	if .UseScript() {
		// the language is consistent with the script, return it
		return 
	}

	// the language is not used for the script, try to replace it
	if  := language.ScriptToLang[];  != 0 {
		return 
	}

	// we do not have good replacement, leave it as it is
	return 
}