package d2scenebuild

import (
	
	
	
	
	
	
	
	

	
	
	
	
	
	

	
	
	
)

const (
	lightCodeStyle             = "github"
	darkCodeStyle              = "catppuccin-mocha"
	maxCodeSourceBytes         = 1 << 20
	maxCodeSourceRunes         = 100_000
	maxCodeRawTokens           = 100_000
	maxCodeExpandedTokenPieces = 100_000
)

type codeTokenBudget struct {
	maxRawTokens      int
	maxExpandedPieces int
	rawTokens         int
	expandedPieces    int
}

func newCodeTokenBudget( int) codeTokenBudget {
	 := maxCodeRawTokens
	 := maxCodeExpandedTokenPieces
	if  > 0 {
		 = min(, )
		 = min(, )
	}
	return codeTokenBudget{maxRawTokens: , maxExpandedPieces: }
}

// buildCodeShape translates a syntax-highlighted code shape into one
// active-theme scene without hidden duplicate theme content.
func ( *builder) ( d2target.Shape,  *d2scene.Stroke) ([]*d2scene.Node, error) {
	if  := validateCodeShape();  != nil {
		return nil, 
	}
	// drawShape guards every label branch with Label != "". ShapeCode's
	// ordinary outline branch is empty, so a label-free code target paints
	// nothing even when it carries a language.
	if .Label == "" {
		return nil, nil
	}
	// A code target without a language has no outline or syntax highlighting.
	if .Language == "" {
		return .buildShapeText()
	}

	 := fmt.Sprintf("shape %q", .ID)

	,  := .activeCodeStyle()
	if  != nil {
		return nil, 
	}
	 := .Get(chroma.Background).Background
	if !.IsSet() {
		return nil, fmt.Errorf("scene: %s code snippet style has no background", )
	}

	 := d2scene.Box{
		X: float64(.Pos.X), Y: float64(.Pos.Y),
		Width: float64(.Width), Height: float64(.Height),
	}
	// Code-shape backgrounds intentionally omit stroke dashes.
	 := cloneCodeStrokeWithoutDashes()
	 := d2scene.NewNode(d2scene.Rect{
		Box: , Fill: codeColourPaint(), Stroke: ,
	})
	.ID = .ID + ":code-background"
	 := []*d2scene.Node{}

	 := float64(.FontSize) / 2
	,  := .buildCodeTextRuns(
		, .ID+":code", .Text, ,
		d2scene.Point{X: .X + , Y: .Y + },
	)
	if  != nil {
		return nil, 
	}
	 = append(, ...)
	return , nil
}

func ( *builder) ( string) (*chroma.Style, error) {
	 := lightCodeStyle
	if .theme.IsDark() {
		 = darkCodeStyle
	}
	 := styles.Get()
	if  == nil {
		return nil, fmt.Errorf("scene: %s code snippet style %q not found", , )
	}
	return , nil
}

func ( *builder) (,  string,  d2target.Text,  *chroma.Style,  d2scene.Point) ([]*d2scene.Node, error) {
	// Build initializes this eagerly. Keep direct package-internal builders used
	// by focused shape tests subject to the same default limits.
	if .codeTokens.maxRawTokens == 0 && .codeTokens.maxExpandedPieces == 0 {
		.codeTokens = newCodeTokenBudget(.options.MaxNodes)
	}
	 := lexers.Get(.Language)
	if  == nil {
		 = lexers.Fallback
	}
	,  := tokeniseCode(, .Label)
	if  != nil {
		return nil, fmt.Errorf("scene: %s tokenize %s code: %w", , .Language, )
	}
	,  := collectCodeTokens(.ctx, , &.codeTokens)
	if  != nil {
		return nil, fmt.Errorf("scene: %s tokenize %s code: %w", , .Language, )
	}
	 := chroma.SplitTokensIntoLines()
	 := make(map[d2scene.AssetID]*codeFontMetrics, 3)
	 := float64(.FontSize) * textmeasure.CODE_LINE_HEIGHT
	var  []*d2scene.Node
	for ,  := range  {
		if  := .ctx.Err();  != nil {
			return nil, 
		}
		 := .Y + float64()*
		 :=  + float64(.FontSize)
		 := .X
		for ,  := range  {
			if  := .ctx.Err();  != nil {
				return nil, 
			}
			 := codeTokenText(.Value)
			if  == "" {
				continue
			}
			 := .Get(.Type)
			,  := .codeTokenFont(, )
			if  != nil {
				return nil, fmt.Errorf("scene: %s code token %d:%d: %w", , , , )
			}
			,  := [.Asset]
			if ! {
				,  = .newCodeFontMetrics(.Asset)
				if  != nil {
					return nil, fmt.Errorf("scene: %s code token %d:%d: %w", , , , )
				}
				[.Asset] = 
			}
			,  := .advance(.ctx, , .Size)
			if  != nil {
				return nil, fmt.Errorf("scene: %s code token %d:%d: %w", , , , )
			}

			// Whitespace advances the line cursor without painting ink. Avoid empty
			// raster work while retaining exact positions for following runs.
			if codeTokenPaintsInk(, ) {
				 := d2scene.TextRun{
					Text:   ,
					Origin: d2scene.Point{X: , Y: },
					Anchor: d2scene.AnchorStart,
					Font:   ,
					Fill:   codeForegroundPaint(.Colour),
					// Chroma's SVG formatter represents underlining on the token;
					// background and border token styles are intentionally ignored.
					Underline: .Underline == chroma.Yes,
					Ink:       d2scene.NewBounds(, , +, +),
				}
				 := d2scene.NewNode()
				.ID = fmt.Sprintf("%s-line:%d:token:%d", , , )
				 = append(, )
			}
			 += 
		}
	}
	return , nil
}

func validateCodeShape( d2target.Shape) error {
	return validateCodeText(fmt.Sprintf("shape %q", .ID), .Text)
}

func validateCodeText( string,  d2target.Text) error {
	if .Label == "" || .Language == "" {
		return nil
	}
	if !utf8.ValidString(.Label) {
		return invalidField(, "label", nil, "must be valid UTF-8")
	}
	if len(.Label) > maxCodeSourceBytes {
		return invalidField(, "label", len(.Label), fmt.Sprintf("code source bytes must not exceed %d", maxCodeSourceBytes))
	}
	 := utf8.RuneCountInString(.Label)
	if  > maxCodeSourceRunes {
		return invalidField(, "label", , fmt.Sprintf("code source runes must not exceed %d", maxCodeSourceRunes))
	}
	if .FontSize <= 0 {
		return invalidField(, "fontSize", .FontSize, "must be positive for a syntax-highlighted label")
	}
	if .FontSize > math.MaxInt32/64 {
		return invalidField(, "fontSize", .FontSize, "must fit raster font scaling")
	}
	return nil
}

func cloneCodeStrokeWithoutDashes( *d2scene.Stroke) *d2scene.Stroke {
	if  == nil {
		return nil
	}
	 := *
	.Dashes = nil
	.DashOffset = 0
	return &
}

func codeTokenText( string) string {
	 = strings.TrimSuffix(, "\n")
	 = strings.TrimSuffix(, "\r")
	return strings.ReplaceAll(, "\t", "    ")
}

func tokeniseCode( chroma.Lexer,  string) ( chroma.Iterator,  error) {
	defer func() {
		if  := recover();  != nil {
			 = nil
			 = fmt.Errorf("lexer panic: %v", )
		}
	}()
	return .Tokenise(nil, )
}

func collectCodeTokens( context.Context,  chroma.Iterator,  *codeTokenBudget) ( []chroma.Token,  error) {
	defer func() {
		if  := recover();  != nil {
			 = nil
			 = fmt.Errorf("lexer panic: %v", )
		}
	}()
	for {
		if  := .Err();  != nil {
			return nil, 
		}
		 := ()
		if  == chroma.EOF {
			break
		}
		if  := .Err();  != nil {
			return nil, 
		}
		if .rawTokens >= .maxRawTokens {
			return nil, fmt.Errorf("raw lexer token count exceeds limit %d", .maxRawTokens)
		}
		,  := countCodeTokenPieces(
			, .Value,
			.maxExpandedPieces-.expandedPieces,
			.maxExpandedPieces,
		)
		if  != nil {
			return nil, 
		}
		.rawTokens++
		.expandedPieces += 
		 = append(, )
	}
	return , nil
}

// countCodeTokenPieces accounts for every token that SplitTokensIntoLines will
// materialize before that helper is allowed to allocate them. It checks the
// context while scanning because a lexer may return one large token.
func countCodeTokenPieces( context.Context,  string, ,  int) (int, error) {
	 := 1
	if  <  {
		return 0, fmt.Errorf("newline-expanded token piece count exceeds limit %d", )
	}
	for  := 0;  < len(); ++ {
		if &4095 == 0 {
			if  := .Err();  != nil {
				return 0, 
			}
		}
		if [] != '\n' {
			continue
		}
		++
		if  >  {
			return 0, fmt.Errorf("newline-expanded token piece count exceeds limit %d", )
		}
	}
	if  := .Err();  != nil {
		return 0, 
	}
	return , nil
}

func codeWhitespaceOnly( string) bool {
	for ,  := range  {
		if !unicode.IsSpace() {
			return false
		}
	}
	return true
}

func codeTokenPaintsInk( string,  chroma.StyleEntry) bool {
	// Whitespace contributes no visible text pixels, but an underlined whitespace
	// token still paints its decoration across the token advance.
	return !codeWhitespaceOnly() || .Underline == chroma.Yes
}

func codeColourPaint( chroma.Colour) d2scene.Paint {
	return d2scene.SolidPaint{Color: color.NRGBA{R: .Red(), G: .Green(), B: .Blue(), A: 0xff}}
}

func codeForegroundPaint( chroma.Colour) d2scene.Paint {
	// An absent SVG fill inherits the SVG initial fill, black. This matters for
	// fallback lexers under the light GitHub style.
	if !.IsSet() {
		return d2scene.SolidPaint{Color: color.NRGBA{A: 0xff}}
	}
	return codeColourPaint()
}

func ( *builder) ( d2target.Text,  chroma.StyleEntry) (d2scene.Font, error) {
	.FontFamily = "mono"
	// The SVG stylesheet declares the italic mono class after the bold mono
	// class. When Chroma requests both, italic therefore wins its font-family.
	.Italic = .Italic == chroma.Yes
	.Bold = .Bold == chroma.Yes && !.Italic
	return .font()
}

type codeFontMetrics struct {
	font *sfnt.Font
}

func ( *builder) ( d2scene.AssetID) (*codeFontMetrics, error) {
	,  := .fontFace()
	if  != nil {
		return nil, 
	}
	return &codeFontMetrics{font: .Outline}, nil
}

func ( *codeFontMetrics) ( context.Context,  string,  float64) (float64, error) {
	if !utf8.ValidString() {
		return 0, fmt.Errorf("text is not valid UTF-8")
	}
	 := fixed.Int26_6(math.Round( * 64))
	var  sfnt.Buffer
	var  sfnt.GlyphIndex
	 := false
	 := 0.0
	for ,  := range  {
		if  := .Err();  != nil {
			return 0, 
		}
		,  := .font.GlyphIndex(&, )
		if  != nil {
			return 0, fmt.Errorf("look up glyph for U+%04X at byte %d: %w", , , )
		}
		if  == 0 {
			return 0, fmt.Errorf("missing glyph U+%04X at byte %d", , )
		}
		if  {
			,  := .font.Kern(&, , , , font.HintingNone)
			if  != nil && !errors.Is(, sfnt.ErrNotFound) {
				return 0, fmt.Errorf("kern glyph for U+%04X at byte %d: %w", , , )
			}
			if  == nil {
				 += float64() / 64
			}
		}
		,  := .font.GlyphAdvance(&, , , font.HintingNone)
		if  != nil {
			return 0, fmt.Errorf("measure glyph for U+%04X at byte %d: %w", , , )
		}
		 += float64() / 64
		,  = , true
	}
	return , nil
}