package d2isometricimg

import (
	
	
	
	
	
	
	
	
	

	
	
	

	
	
	
	
)

const (
	maxTextPixels       = 8 * 1024 * 1024
	maxTextLabels       = 19000
	maxLabelRunes       = 64 * 1024
	maxTextSourceBytes  = 8 * 1024 * 1024
	maxTextFontBytes    = 32 * 1024 * 1024
	maxTextGlyphWork    = 8_000_000
	maxTextOutlineWork  = 8_000_000
	textReferencePixels = 64
)

// labelTextStyle describes a physical print area, not a camera-facing caption.
// FontSize uses original D2 pixels; Width and Depth use the scene's world units.
// Color is unpremultiplied. The returned texture uses premultiplied image.RGBA.
type labelTextStyle struct {
	Width, Depth, FontSize, PixelScale float64
	FontFamily                         string
	Bold, Italic, Underline            bool
	Color                              color.NRGBA
	Background                         *color.NRGBA
	ForegroundPaint, BackgroundPaint   d2scene.Paint
	Foreground                         string
	Opacity                            float64
	Align                              string
	MaxLines                           int
}

type textLayout struct {
	Lines                []string
	FontSize, LineHeight float64 // world units
	Truncated            bool
}

type textPainter struct {
	fallbackFonts                      *d2scenebuild.FontFallbackOptions
	ctx                                context.Context
	count, used, tileWidth, tileHeight int
	pixels, glyphWork, outlineWork     int
	sourceBytes, fontBytes             int
	primary, mono                      d2fonts.FontFamily
	outputDensity                      float64
	faces                              map[d2fonts.Font]*fontface.ParsedFace
	shaper                             fontface.ShapingWorkspace
}

type shapedLabelLine struct {
	text       fontface.ShapedText
	minX, maxX float64
	minY, maxY float64
}

func newTextPainter( context.Context,  int) (*textPainter, error) {
	if  == nil {
		return nil, fmt.Errorf("isometric text requires a context")
	}
	if  := .Err();  != nil {
		return nil, 
	}
	if  < 0 ||  > maxTextLabels {
		return nil, fmt.Errorf("isometric text exceeds %d labels", maxTextLabels)
	}
	 := maxTextPixels / max(1, )
	 := max(1, int(math.Floor(math.Sqrt(float64()))))
	 := 
	return &textPainter{ctx: , count: , tileWidth: , tileHeight: , faces: make(map[d2fonts.Font]*fontface.ParsedFace)}, nil
}

func ( *textPainter) (,  *d2fonts.FontFamily) {
	.primary, .mono = nativeFontFamilies(, )
}

func ( *textPainter) ( *d2scenebuild.FontFallbackOptions) {
	.fallbackFonts = 
}

func ( *textPainter) ( float64) {
	.outputDensity = 
}

func ( *textPainter) ( labelTextStyle) (*fontface.ParsedFace, bool, error) {
	,  := nativeFontFamily(.FontFamily, .primary, .mono)
	if  != nil {
		return nil, false, 
	}
	 := d2fonts.FONT_STYLE_REGULAR
	if .Bold {
		 = d2fonts.FONT_STYLE_BOLD
	} else if .Italic {
		 = d2fonts.FONT_STYLE_ITALIC
	}
	 := d2fonts.Font{Family: , Style: }
	if  := .faces[];  != nil {
		return , .Bold && .Italic, nil
	}
	,  := d2fonts.FontFaces.Lookup()
	if ! {
		return nil, false, fmt.Errorf("isometric text font %s/%s is not loaded", , )
	}
	if len(.faces) >= 24 || len() > maxTextFontBytes-.fontBytes {
		return nil, false, fmt.Errorf("isometric text exceeds font asset budget")
	}
	// Registered CLI/custom font bytes are explicit inputs. Retain an owned
	// copy for non-bundled faces; never discover fonts on the host filesystem.
	, ,  := fontface.RegisteredBundledFace(, 0)
	if  != nil {
		return nil, false, fmt.Errorf("isometric text font: %w", )
	}
	var  *fontface.ParsedFace
	if  {
		,  = .CloneReadOnly()
	} else {
		,  = fontface.ParseFace(append([]byte(nil), ...), 0)
	}
	if  != nil {
		return nil, false, fmt.Errorf("isometric text font parse: %w", )
	}
	.faces[] = 
	.fontBytes += len()
	return , .Bold && .Italic, nil
}

func ( *textPainter) ( string,  labelTextStyle) (*image.RGBA, textLayout, error) {
	if  := .ctx.Err();  != nil {
		return nil, textLayout{}, 
	}
	if .used >= .count {
		return nil, textLayout{}, fmt.Errorf("isometric text label allocation exceeds its declared count")
	}
	for ,  := range []float64{.Width, .Depth, .PixelScale} {
		if math.IsNaN() || math.IsInf(, 0) ||  <= 0 {
			return nil, textLayout{}, fmt.Errorf("isometric text has an invalid physical print area or pixel scale")
		}
	}
	if .FontSize < 0 || math.IsNaN(.FontSize) || math.IsInf(.FontSize, 0) || math.IsNaN(.Opacity) || math.IsInf(.Opacity, 0) || .Opacity < 0 || .Opacity > 1 {
		return nil, textLayout{}, fmt.Errorf("isometric text has invalid font size or opacity")
	}
	if !utf8.ValidString() || utf8.RuneCountInString() > maxLabelRunes || len() > maxTextSourceBytes-.sourceBytes {
		return nil, textLayout{}, fmt.Errorf("isometric text exceeds source budget or contains invalid UTF-8")
	}
	.sourceBytes += len()
	, ,  := .face()
	if  != nil {
		return nil, textLayout{}, 
	}
	 := maxTextPixels / max(1, .count)
	,  := surfaceTextureDimensionsAtDensity(.Width, .Depth, 4096, , .outputDensity)
	// Keep layout independent of output sampling. In particular, a two-pixel
	// safety inset must not shrink authored text more in a small export.
	,  := surfaceTextureDimensions(.Width, .Depth, 4096, )
	if * > maxTextPixels-.pixels {
		return nil, textLayout{}, fmt.Errorf("isometric text exceeds its %d pixel texture budget", maxTextPixels)
	}
	.used++
	.pixels +=  * 
	,  := nativeTextNeedsFallback(.ctx, , )
	if  != nil {
		return nil, textLayout{}, 
	}
	if  {
		,  := nativeFontFamily(.FontFamily, .primary, .mono)
		if  != nil {
			return nil, textLayout{}, 
		}
		return nativeFallbackTextTexture(.ctx, , , , , , .mono, .fallbackFonts)
	}
	 := image.NewRGBA(image.Rect(0, 0, , ))
	if .Background != nil {
		 := *.Background
		.A = uint8(math.Round(float64(.A) * .Opacity))
		draw.Draw(, .Bounds(), image.NewUniform(), image.Point{}, draw.Src)
	}
	 := normalizeLabel()
	 := make(map[string]shapedLabelLine)
	 := func( string) (shapedLabelLine, error) {
		if ,  := [];  {
			return , nil
		}
		 := maxTextGlyphWork - .glyphWork
		if  <= 0 {
			return shapedLabelLine{}, fmt.Errorf("isometric text glyph shaping work exceeds %d", maxTextGlyphWork)
		}
		,  := .shaper.ShapeTextTransient(.ctx, , fixed.I(textReferencePixels), []fontface.ShapeFace{{ID: "surface", Face: }}, fontface.ShapeLimits{
			Runes: maxLabelRunes + 1, Faces: 1, CoverageChecks: int64(), Runs: maxLabelRunes + 1, Glyphs: min(, (maxLabelRunes+1)*4),
		})
		if  != nil {
			return shapedLabelLine{}, fmt.Errorf("isometric text shaping: %w", )
		}
		.glyphWork += max(len(.Glyphs), int(.CoverageChecks))
		// The line cache outlives the next shaping call's borrowed glyphs.
		.Glyphs = slices.Clone(.Glyphs)
		 := shapedLabelLine{text: , maxX: .Advance}
		for ,  := range .Glyphs {
			if !.HasInk {
				continue
			}
			for ,  := range []float64{float64(.Ink.Min.X) / 64, float64(.Ink.Max.X) / 64} {
				for ,  := range []float64{float64(.Ink.Min.Y) / 64, float64(.Ink.Max.Y) / 64} {
					,  := +.PositionX, +.PositionY
					if  {
						 -= float64( * .20)
					}
					.minX = math.Min(.minX, )
					.maxX = math.Max(.maxX, )
					.minY = math.Min(.minY, )
					.maxY = math.Max(.maxY, )
				}
			}
		}
		[] = 
		return , nil
	}
	 := float64() / .Width
	 := .FontSize * .PixelScale * 
	if .FontSize <= 0 {
		 = .235 * 
	}
	 := math.Min(2, math.Min(float64(), float64())*.05)
	,  := float64()-2*, float64()-2*
	 := .MaxLines
	if  <= 0 ||  > 64 {
		 = 64
	}
	 := func( string,  float64) (float64, error) {
		,  := ()
		return (.maxX - .minX) *  / textReferencePixels, 
	}
	,  := fitNativeLabel(, false, , , , , )
	if  != nil {
		return nil, textLayout{}, 
	}
	,  := .FontSize, .LineHeight
	 :=  / textReferencePixels
	,  := math.Inf(1), math.Inf(-1)
	 := make([]shapedLabelLine, len(.Lines))
	for ,  := range .Lines {
		,  := ()
		if  != nil {
			return nil, textLayout{}, 
		}
		[] = 
		 = math.Min(, float64(float64()*)+float64(.minY*))
		 = math.Max(, float64(float64()*)+float64(.maxY*))
	}
	 := (float64()-(-))/2 - 
	,  := float64()/float64(), float64()/float64()
	 := vector.NewRasterizer(, )
	var  d2scene.Path
	 := nativeVectorEnabled(.ctx) || .ForegroundPaint != nil || .BackgroundPaint != nil
	 := func( d2scene.PathCommand) {
		if  {
			.Commands = append(.Commands, )
		}
	}
	var  sfnt.Buffer
	// Round placement products before translation: SVG preserves these
	// outline coordinates, so fused arithmetic must not vary them by CPU.
	for ,  := range  {
		if  := .ctx.Err();  != nil {
			return nil, textLayout{}, 
		}
		 := (float64()-float64((.maxX-.minX)*))/2 - float64(.minX*)
		if .Align == "left" {
			 =  - float64(.minX*)
		}
		 :=  + float64(float64()*)
		for ,  := range .text.Glyphs {
			if .Empty || !.HasInk {
				continue
			}
			,  := .Outline.LoadGlyph(&, sfnt.GlyphIndex(.ID), fixed.I(textReferencePixels), nil)
			if  != nil {
				return nil, textLayout{}, fmt.Errorf("isometric text glyph outline: %w", )
			}
			.outlineWork += len()
			if .outlineWork > maxTextOutlineWork {
				return nil, textLayout{}, fmt.Errorf("isometric text outline work exceeds %d", maxTextOutlineWork)
			}
			 := func( fixed.Point26_6) (float32, float32) {
				,  := float64(.X)/64+.PositionX, float64(.Y)/64+.PositionY
				if  {
					 -= float64( * .20)
				}
				return float32(( + float64(*)) * ), float32(( + float64(*)) * )
			}
			 := false
			for ,  := range  {
				switch .Op {
				case sfnt.SegmentOpMoveTo:
					if  {
						.ClosePath()
						(d2scene.ClosePath())
					}
					,  := (.Args[0])
					.MoveTo(, )
					(d2scene.MoveTo(float64(), float64()))
					 = true
				case sfnt.SegmentOpLineTo:
					,  := (.Args[0])
					.LineTo(, )
					(d2scene.LineTo(float64(), float64()))
				case sfnt.SegmentOpQuadTo:
					,  := (.Args[0])
					,  := (.Args[1])
					.QuadTo(, , , )
					(d2scene.QuadraticTo(float64(), float64(), float64(), float64()))
				case sfnt.SegmentOpCubeTo:
					,  := (.Args[0])
					,  := (.Args[1])
					,  := (.Args[2])
					.CubeTo(, , , , , )
					(d2scene.CubicTo(float64(), float64(), float64(), float64(), float64(), float64()))
				}
			}
			if  {
				.ClosePath()
				(d2scene.ClosePath())
			}
		}
		if .Underline && .maxX > .minX {
			,  := +float64(.minX*), +float64(.maxX*)
			,  := +float64(*.10), +float64(*.15)
			,  = *, *
			,  = *, *
			.MoveTo(float32(), float32())
			.LineTo(float32(), float32())
			.LineTo(float32(), float32())
			.LineTo(float32(), float32())
			.ClosePath()
			(d2scene.MoveTo(, ))
			(d2scene.LineTo(, ))
			(d2scene.LineTo(, ))
			(d2scene.LineTo(, ))
			(d2scene.ClosePath())
		}
	}
	 := .Color
	.A = uint8(math.Round(float64(.A) * .Opacity))
	.Draw(, .Bounds(), image.NewUniform(), image.Point{})
	if  {
		.Fill = d2scene.SolidPaint{Color: }
		 := d2scene.NewNode(nil)
		 := d2scene.Box{Width: float64(), Height: float64()}
		if .Background != nil {
			 := *.Background
			.A = uint8(math.Round(float64(.A) * .Opacity))
			.Children = append(.Children, d2scene.NewNode(d2scene.Rect{Box: , Fill: d2scene.SolidPaint{Color: }}))
		}
		if .ForegroundPaint != nil || .BackgroundPaint != nil {
			// Keep authored paint spatial: apply it to the complete label outline,
			// then composite the caption fill and object opacity exactly once.
			.Children = nil
			.Opacity = .Opacity
			if .BackgroundPaint != nil {
				.Children = append(.Children, d2scene.NewNode(d2scene.Rect{Box: , Fill: .BackgroundPaint}))
			} else if .Background != nil {
				.Children = append(.Children, d2scene.NewNode(d2scene.Rect{Box: , Fill: d2scene.SolidPaint{Color: *.Background}}))
			}
			.Fill = d2scene.SolidPaint{Color: .Color}
			if .ForegroundPaint != nil {
				.Fill = .ForegroundPaint
			}
		}
		.Children = append(.Children, d2scene.NewNode())
		 := d2scene.NewDocument(, )
		if .ForegroundPaint != nil || .BackgroundPaint != nil {
			var  error
			,  = rasterNativeSurfaceDocument(.ctx, , , )
			if  != nil {
				return nil, textLayout{}, 
			}
		}
		if  := retainNativeVectorSurface(.ctx, , );  != nil {
			return nil, textLayout{}, 
		}
	}
	.FontSize /= 
	.LineHeight /= 
	return , , nil
}

// D2 has already allocated the label's physical dimensions. Preserve its
// authored lines and whitespace instead of reflowing or silently dropping text.
// A constrained surface scales the complete label uniformly; admission errors
// report resource limits explicitly instead of replacing the tail with dots.
func normalizeLabel( string) string {
	return strings.ReplaceAll(strings.ReplaceAll(, "\r\n", "\n"), "\r", "\n")
}

func fitNativeLabel( string,  bool,  func(string, float64) (float64, error), , ,  float64,  int) (textLayout, error) {
	,  = math.Max(, 1e-9), math.Max(, 1e-9)
	 := strings.Split(normalizeLabel(), "\n")
	 := math.Min(, /(float64(len())*1.18))
	for ,  := range  {
		,  := (, 1)
		if  != nil {
			return textLayout{}, 
		}
		if  > 0 {
			 = math.Min(, /)
		}
	}
	return textLayout{Lines: , FontSize: , LineHeight:  * 1.18}, nil
}