package fontface

import (
	
	
	
	

	
	gotextfont 
	
	
	
	
	
	
)

// ShapeFace couples a caller-owned identifier to one parsed OpenType face.
// ParsedFace.Shaping contains mutable lookup caches, so callers must not share
// one ShapeFace concurrently between shaping calls.
type ShapeFace struct {
	ID   string
	Face *ParsedFace
}

// ShapeLimits bounds all work which shaping cannot interrupt internally.
// Every field must be positive. Callers enforcing document-wide budgets pass
// the remaining aggregate allowance for CoverageChecks, Runs, and Glyphs.
type ShapeLimits struct {
	Runes          int
	Faces          int
	CoverageChecks int64
	Runs           int
	Glyphs         int
}

// ShapedGlyph is renderer-neutral placement for one glyph. Ink stays in the
// source font's fixed-point, Y-down coordinate system and is relative to the
// glyph position. Empty glyphs intentionally carry ID zero and no ink.
type ShapedGlyph struct {
	ID          uint32
	Face        int
	PositionX   float64
	PositionY   float64
	Advance     float64
	Ink         fixed.Rectangle26_6
	HasInk      bool
	Empty       bool
	Source      rune
	SourceIndex int
}

// ShapedText is one visually ordered, left-to-right placed line. Work reports
// the charged units so a caller can update aggregate document/frame budgets.
type ShapedText struct {
	Glyphs         []ShapedGlyph
	Advance        float64
	Runes          int
	CoverageChecks int64
	Runs           int
}

// ShapingWorkspace owns mutable scratch state for a sequence of shaping
// calls. Reusing one workspace for an operation lets go-text retain its
// HarfBuzz font cache and segmentation buffers without sharing mutable state
// between concurrent document builds or renders.
//
// A ShapingWorkspace must not be used concurrently. Returned ShapedText values
// borrow their Glyphs until the workspace is reused. ParsedFace
// pointers and their exported Outline and Shaping fields must remain unchanged
// between calls; the workspace deliberately caches immutable font answers.
type ShapingWorkspace struct {
	graphemes segmenter.Segmenter
	segmenter shaping.Segmenter
	shaper    shaping.HarfbuzzShaper

	runes              []rune
	faceAssignments    []int
	replacementGlyphs  []uint32
	inputs             []shaping.Input
	asciiInput         [1]shaping.Input
	runs               []shapedFontRun
	faceIndexes        map[*gotextfont.Face]int
	coverage           map[faceRune]bool
	outlines           map[glyphKey]glyphOutline
	glyphs             []ShapedGlyph
	asciiFace          *ParsedFace
	asciiValues        [utf8.RuneSelf]uint8
	asciiSeen          [utf8.RuneSelf]uint32
	coverageGeneration uint32
}

// missingGlyphPlaceholderRunes are ordered from the conventional replacement
// character through outline-box alternatives, with an ASCII last resort.
// Keeping a box before '?' makes an absent scalar visibly distinct from user
// text even when the selected font omits U+FFFD and U+25A1.
var missingGlyphPlaceholderRunes = [...]rune{'\ufffd', '\u25a1', '\u2610', '?'}

// ShapeTextTransient applies pure-Go HarfBuzz-compatible shaping with bidi, script,
// ligature, mark, and ordered font-fallback support. Font selection has
// grapheme-cluster affinity: one face must cover every visible rune in a UAX
// #29 extended grapheme cluster. This prevents a combining mark that happens
// to exist in the primary font from being detached from a fallback base.
//
// It reuses the workspace's output storage in addition to its
// internal scratch. The returned Glyphs remain valid only until the next call
// to ShapeTextTransient on this workspace. It is intended for document
// pipelines which immediately translate the neutral glyphs into owned scene
// or raster records.
func ( *ShapingWorkspace) ( context.Context,  string,  fixed.Int26_6,  []ShapeFace,  ShapeLimits) (ShapedText, error) {
	clear(.inputs)
	.inputs = .inputs[:0]
	clear(.faceIndexes)
	clear(.runs)
	.runs = .runs[:0]
	.asciiInput[0] = shaping.Input{}
	if  == nil {
		 = context.Background()
	}
	if  := .Err();  != nil {
		return ShapedText{}, 
	}
	if  <= 0 {
		return ShapedText{}, fmt.Errorf("d2fonts: shaping size must be positive")
	}
	if .Runes <= 0 || .Faces <= 0 || .CoverageChecks <= 0 || .Runs <= 0 || .Glyphs <= 0 {
		return ShapedText{}, fmt.Errorf("d2fonts: every shaping limit must be positive")
	}
	if len() == 0 {
		return ShapedText{}, fmt.Errorf("d2fonts: shaping requires at least one font face")
	}
	if len() > .Faces {
		return ShapedText{}, fmt.Errorf("d2fonts: font face count %d exceeds limit %d", len(), .Faces)
	}
	if len() > 1 {
		if .faceIndexes == nil {
			.faceIndexes = make(map[*gotextfont.Face]int, len())
		}
	}
	for ,  := range  {
		if .Face == nil || .Face.Outline == nil || .Face.Shaping == nil {
			return ShapedText{}, fmt.Errorf("d2fonts: font face %d (%q) is nil or incomplete", , .ID)
		}
		if len() > 1 {
			if ,  := .faceIndexes[.Face.Shaping];  {
				return ShapedText{}, fmt.Errorf("d2fonts: font face %d (%q) duplicates an earlier shaping face", , .ID)
			}
			.faceIndexes[.Face.Shaping] = 
		}
	}

	,  := boundedRuneCount(, , .Runes)
	if  != nil {
		return ShapedText{}, 
	}
	 := ShapedText{Runes: }
	if  == 0 {
		return , nil
	}
	.runes = .runes[:0]
	if cap(.runes) <  {
		.runes = make([]rune, 0, )
	}
	 := true
	 := false
	for ,  := range  {
		if len(.runes)&1023 == 0 {
			if  := .Err();  != nil {
				return ShapedText{}, 
			}
		}
		if  >= utf8.RuneSelf {
			 = false
		} else if  >= 'A' &&  <= 'Z' ||  >= 'a' &&  <= 'z' {
			 = true
		}
		.runes = append(.runes, )
	}
	 := .runes

	if .coverage != nil {
		clear(.coverage)
	}
	.coverageGeneration++
	if .coverageGeneration == 0 {
		clear(.asciiSeen[:])
		.coverageGeneration = 1
	}
	if .asciiFace != [0].Face {
		.asciiFace = [0].Face
		clear(.asciiValues[:])
		clear(.asciiSeen[:])
	}
	 := clusterFaceResolver{
		faces:              ,
		coverage:           .coverage,
		asciiFace:          .asciiFace,
		asciiValues:        &.asciiValues,
		asciiSeen:          &.asciiSeen,
		coverageGeneration: .coverageGeneration,
		coverageCapacity:   min(, 256),
		limits:             ,
	}
	var  []int
	if len() > 1 {
		if cap(.faceAssignments) <  {
			.faceAssignments = make([]int, )
		} else {
			.faceAssignments = .faceAssignments[:]
		}
		 = .faceAssignments
	}
	.replacementGlyphs = .replacementGlyphs[:0]
	.graphemes.Init()
	for  := .graphemes.GraphemeIterator(); .Next(); {
		if  := .Err();  != nil {
			return ShapedText{}, 
		}
		 := .Grapheme()
		, ,  := .resolveCluster(.Text, .Offset)
		if  != nil {
			return ShapedText{}, 
		}
		for  := range .Text {
			if  != nil {
				[.Offset+] = 
			}
			if  != 0 {
				if len(.replacementGlyphs) == 0 {
					if cap(.replacementGlyphs) <  {
						.replacementGlyphs = make([]uint32, )
					} else {
						.replacementGlyphs = .replacementGlyphs[:]
						clear(.replacementGlyphs)
					}
				}
				.replacementGlyphs[.Offset+] = 
			}
		}
	}
	.coverage = .coverage
	.CoverageChecks = .checks

	 := shaping.Input{
		Text: , RunEnd: len(), Direction: di.DirectionLTR, Size: ,
	}
	 := .splitSemanticInputs(, [0].Face.Shaping, , )
	if  := .Err();  != nil {
		return ShapedText{}, 
	}
	var  []shaping.Input
	if len() == 1 {
		if len() > .Runs {
			return ShapedText{}, fmt.Errorf("d2fonts: text shaping run count exceeds limit %d", .Runs)
		}
		 = 
	} else {
		.inputs,  = splitInputsByAssignedFace(.inputs[:0], , , , .Runs)
		if  != nil {
			return ShapedText{}, 
		}
		 = .inputs
	}
	if len() > .Runs {
		return ShapedText{}, fmt.Errorf("d2fonts: text shaping run count %d exceeds limit %d", len(), .Runs)
	}
	.Runs = len()

	 := 0
	for ,  := range  {
		if  := .Err();  != nil {
			return ShapedText{}, 
		}
		 := .shaper.Shape()
		if  := .Err();  != nil {
			return ShapedText{}, 
		}
		if len(.Glyphs) > .Glyphs- {
			return ShapedText{}, fmt.Errorf("d2fonts: shaped glyph count exceeds limit %d", .Glyphs)
		}
		 += len(.Glyphs)
		 := 0
		if len() == 1 {
			if .Face != [0].Face.Shaping {
				return ShapedText{}, fmt.Errorf("d2fonts: shaper run %d returned an unknown font face", )
			}
		} else {
			var  bool
			,  = .faceIndexes[.Face]
			if ! {
				return ShapedText{}, fmt.Errorf("d2fonts: shaper run %d returned an unknown font face", )
			}
		}
		.runs = append(.runs, shapedFontRun{output: , face: })
	}
	 := .runs
	if len() > 1 {
		orderShapedRunsLTR()
	}

	if cap(.glyphs) >=  {
		.Glyphs = .glyphs[:0]
	} else {
		.Glyphs = make([]ShapedGlyph, 0, )
	}
	 := 0.0
	if .outlines == nil {
		.outlines = make(map[glyphKey]glyphOutline, min(, 256))
	}
	var  map[glyphKey]glyphOutline
	for ,  := range  {
		 := [.face].Face
		 := 
		var  sfnt.Buffer
		for ,  := range .output.Glyphs {
			if  := .Err();  != nil {
				return ShapedText{}, 
			}
			,  := shapedGlyphSource(, .ClusterIndex)
			 :=  + fixedToFloat(.XOffset)
			 := -fixedToFloat(.YOffset)
			 := fixedToFloat(.XAdvance)
			if .GlyphID == gotextfont.EmptyGlyph {
				.Glyphs = append(.Glyphs, ShapedGlyph{
					Face: .face, PositionX: , PositionY: , Advance: ,
					Empty: true, Source: , SourceIndex: ,
				})
				 += 
				continue
			}
			 := uint32(.GlyphID)
			if .GlyphID == 0 {
				if  < 0 ||  >= len(.replacementGlyphs) || .replacementGlyphs[] == 0 {
					return ShapedText{}, missingShapedGlyphError(, .ClusterIndex, [.face].ID)
				}
				 = .replacementGlyphs[]
				 = 
				 = 0
				,  := .Outline.GlyphAdvance(
					&, sfnt.GlyphIndex(), , font.HintingNone,
				)
				if  != nil {
					return ShapedText{}, fmt.Errorf("d2fonts: measure missing-glyph placeholder %d in font %q: %w", , [.face].ID, )
				}
				 = fixedToFloat()
			}
			if  > math.MaxUint16 || int() >= .Outline.NumGlyphs() {
				return ShapedText{}, fmt.Errorf("d2fonts: shaped glyph ID %d at index %d is out of range for font %q", , , [.face].ID)
			}
			 := glyphKey{face: , id: sfnt.GlyphIndex(), size: }
			,  := .outlines[]
			if ! && len(.outlines) >= maxCachedGlyphOutlines {
				,  = []
			}
			if ! {
				, ,  := .GlyphRenderBounds(uint32(.id), )
				if  != nil {
					return ShapedText{}, unsupportedShapedGlyphError(.Shaping, gotextfont.GID(), , , [.face].ID, )
				}
				.bounds = 
				.hasInk = 
				if len(.outlines) < maxCachedGlyphOutlines {
					.outlines[] = 
				} else {
					if  == nil {
						 = make(map[glyphKey]glyphOutline, min(, 256))
					}
					[] = 
				}
			}
			.Glyphs = append(.Glyphs, ShapedGlyph{
				ID: , Face: .face, PositionX: , PositionY: , Advance: ,
				Ink: .bounds, HasInk: .hasInk, Source: , SourceIndex: ,
			})
			 += 
		}
		 = 
	}
	.Advance = 
	.glyphs = .Glyphs
	return , nil
}

func ( *ShapingWorkspace) ( shaping.Input,  *gotextfont.Face, ,  bool) []shaping.Input {
	if ! {
		return .segmenter.Split(, oneFaceMap{face: })
	}
	.Face = 
	.Language = "en"
	.Script = language.Common
	if  {
		.Script = language.Latin
	}
	.asciiInput[0] = 
	return .asciiInput[:]
}

func fixedToFloat( fixed.Int26_6) float64 {
	return float64() / 64
}

func boundedRuneCount( context.Context,  string,  int) (int, error) {
	 := 0
	for  := 0;  < len(); {
		if &1023 == 0 {
			if  := .Err();  != nil {
				return 0, 
			}
		}
		,  := utf8.DecodeRuneInString([:])
		if  == utf8.RuneError &&  == 1 {
			return 0, fmt.Errorf("d2fonts: text is not valid UTF-8 at byte %d", )
		}
		++
		if  >  {
			return 0, fmt.Errorf("d2fonts: text rune count exceeds per-run limit %d", )
		}
		 += 
	}
	if  := .Err();  != nil {
		return 0, 
	}
	return , nil
}

type faceRune struct {
	face  int
	value rune
}

type clusterFaceResolver struct {
	faces              []ShapeFace
	coverage           map[faceRune]bool
	coverageOverflow   map[faceRune]bool
	asciiFace          *ParsedFace
	asciiValues        *[utf8.RuneSelf]uint8
	asciiSeen          *[utf8.RuneSelf]uint32
	coverageGeneration uint32
	coverageCapacity   int
	checks             int64
	limits             ShapeLimits
}

func ( *clusterFaceResolver) ( int,  rune) (bool, error) {
	if  >= 0 &&  < utf8.RuneSelf && .faces[].Face == .asciiFace {
		 := int()
		if .asciiSeen[] == .coverageGeneration {
			return .asciiValues[] == 2, nil
		}
		if .checks >= .limits.CoverageChecks {
			return false, fmt.Errorf("d2fonts: text font coverage checks exceed limit %d", .limits.CoverageChecks)
		}
		.checks++
		 := .asciiValues[]
		if  == 0 {
			,  := .faces[].Face.SupportsRenderableRune()
			if  != nil {
				return false, 
			}
			if  {
				 = 2
			} else {
				 = 1
			}
			.asciiValues[] = 
		}
		.asciiSeen[] = .coverageGeneration
		return  == 2, nil
	}
	 := faceRune{face: , value: }
	if ,  := .coverage[];  {
		return , nil
	}
	if ,  := .coverageOverflow[];  {
		return , nil
	}
	if .checks >= .limits.CoverageChecks {
		return false, fmt.Errorf("d2fonts: text font coverage checks exceed limit %d", .limits.CoverageChecks)
	}
	.checks++
	,  := .faces[].Face.SupportsRenderableRune()
	if  != nil {
		return false, 
	}
	if .coverage == nil {
		.coverage = make(map[faceRune]bool, .coverageCapacity)
	}
	if len(.coverage) < maxCachedCoverageEntries {
		.coverage[] = 
	} else {
		if .coverageOverflow == nil {
			.coverageOverflow = make(map[faceRune]bool, .coverageCapacity)
		}
		.coverageOverflow[] = 
	}
	return , nil
}

func ( *clusterFaceResolver) ( []rune,  int) (int, uint32, error) {
	for  := range .faces {
		 := true
		for ,  := range  {
			if IsDefaultIgnorableRune() {
				continue
			}
			,  := .supports(, )
			if  != nil {
				return 0, 0, 
			}
			if ! {
				 = false
				break
			}
		}
		if  {
			return , 0, nil
		}
	}

	// Keep the grapheme on one face and ask the shaping loop to replace any
	// resulting glyph zero with that face's deterministic placeholder. This
	// covers both a genuinely absent scalar and a cluster whose visible runes
	// exist only across different faces.
	for ,  := range missingGlyphPlaceholderRunes {
		for  := range .faces {
			,  := .supports(, )
			if  != nil {
				return 0, 0, 
			}
			if ! {
				continue
			}
			,  := .faces[].Face.Shaping.NominalGlyph()
			if ! ||  == 0 ||  > math.MaxUint16 {
				continue
			}
			return , uint32(), nil
		}
	}
	return 0, 0, fmt.Errorf("d2fonts: no font face has a drawable missing-glyph placeholder for grapheme cluster beginning at rune %d", )
}

type oneFaceMap struct {
	face *gotextfont.Face
}

func ( oneFaceMap) (rune) *gotextfont.Face { return .face }

// splitInputsByAssignedFace preserves the bidi/script/language runs computed
// by go-text while imposing occurrence-specific grapheme face choices. A
// rune-keyed Fontmap cannot express that the same combining mark uses primary
// in one cluster and fallback in another, so this split must happen by index.
func splitInputsByAssignedFace(,  []shaping.Input,  []int,  []ShapeFace,  int) ([]shaping.Input, error) {
	if  := min(len(), ); cap() <  {
		 = make([]shaping.Input, 0, )
	}
	for ,  := range  {
		if .RunStart < 0 || .RunStart >= .RunEnd || .RunEnd > len() {
			return nil, fmt.Errorf("d2fonts: shaper segment has invalid rune range [%d:%d]", .RunStart, .RunEnd)
		}
		 := .RunStart
		 := []
		for  :=  + 1;  <= .RunEnd; ++ {
			if  < .RunEnd && [] ==  {
				continue
			}
			if  < 0 ||  >= len() {
				return nil, fmt.Errorf("d2fonts: invalid assigned font face %d", )
			}
			if len() >=  {
				return nil, fmt.Errorf("d2fonts: text shaping run count exceeds limit %d", )
			}
			 := 
			.RunStart = 
			.RunEnd = 
			.Face = [].Face.Shaping
			 = append(, )
			if  < .RunEnd {
				 = 
				 = []
			}
		}
	}
	return , nil
}

type shapedFontRun struct {
	output shaping.Output
	face   int
}

type glyphKey struct {
	face *ParsedFace
	id   sfnt.GlyphIndex
	size fixed.Int26_6
}

type glyphOutline struct {
	bounds fixed.Rectangle26_6
	hasInk bool
}

const maxCachedGlyphOutlines = 4_096

const maxCachedCoverageEntries = 4_096

// orderShapedRunsLTR mirrors go-text's UAX #9 run-order post-processing for
// D2/SVG's default left-to-right paragraph direction. HarfBuzz already emits
// each individual run in visual glyph order.
func orderShapedRunsLTR( []shapedFontRun) {
	 := -1
	 := func(,  int) {
		for ,  := , -1;  < ; ,  = +1, -1 {
			[], [] = [], []
		}
	}
	for  := range  {
		if [].output.Direction == di.DirectionLTR {
			if  != -1 {
				(, )
				 = -1
			}
		} else if  == -1 {
			 = 
		}
	}
	if  != -1 {
		(, len())
	}
}

func shapedGlyphSource( []rune,  int) (rune, int) {
	if  >= 0 &&  < len() {
		return [], 
	}
	return 0, 
}

func missingShapedGlyphError( []rune,  int,  string) error {
	if  >= 0 &&  < len() {
		return fmt.Errorf("d2fonts: missing shaped glyph for U+%04X at rune %d in font %q", [], , )
	}
	return fmt.Errorf("d2fonts: missing shaped glyph at cluster %d in font %q", , )
}

func unsupportedShapedGlyphError( *gotextfont.Face,  gotextfont.GID,  rune,  int,  string,  error) error {
	 := "non-outline"
	switch .GlyphData().(type) {
	case gotextfont.GlyphOutline:
		 = "outline"
	case gotextfont.GlyphColor:
		 = "color"
	case gotextfont.GlyphBitmap:
		 = "bitmap"
	case gotextfont.GlyphSVG:
		 = "SVG"
	case nil:
		 = "missing"
	}
	if  >= 0 {
		return fmt.Errorf("d2fonts: %s glyph U+%04X at rune %d in font %q cannot be rasterized: %w", , , , , )
	}
	return fmt.Errorf("d2fonts: %s glyph at cluster %d in font %q cannot be rasterized: %w", , , , )
}