package d2scenebuild

import (
	
	
	

	
	
	libcolor 
	
)

const (
	animatedShapeDuration           = time.Second
	animatedShapeFilterExpansion    = 12.6 + 3*25.2
	animatedShapeMaxFilterExpansion = (12.6 + 3*25.2) + (7.56 + 3*15.12) + 2
)

var (
	svgStaticShapeShadow = d2scene.DropShadow{
		OffsetX: 3,
		OffsetY: 5,
		Color:   color.NRGBA{R: 0x3d, G: 0x45, B: 0x74, A: 102},
	}
	// Filter Effects Module Level 1 defines drop-shadow()'s optional third
	// length as the Gaussian standard deviation, unlike box-shadow's blur
	// radius. The CSS lengths therefore map directly to scene Sigma values.
	animatedShapeShadowMidpoints = [...]d2scene.DropShadow{
		{
			OffsetY: 12.6,
			SigmaX:  25.2,
			SigmaY:  25.2,
			Color:   color.NRGBA{R: 50, G: 50, B: 93, A: 64},
		},
		{
			OffsetY: 7.56,
			SigmaX:  15.12,
			SigmaY:  15.12,
			Color:   color.NRGBA{A: 26},
		},
	}
	// CSS shadow colors interpolate in premultiplied space. The scene IR's
	// NRGBA interpolation is component-wise, so retain each midpoint RGB at
	// alpha zero to produce the same fade without changing shared IR behavior.
	animatedShapeShadowEndpoints = [...]d2scene.DropShadow{
		{Color: color.NRGBA{R: 50, G: 50, B: 93}},
		{},
	}
	// Scene validation accounts declared filter geometry independently from
	// animation tracks. Keep the maximum animated deviations/offsets in the
	// transparent declarations so MaxFilterExpansion covers midpoint work;
	// AnimateDropShadow overrides them with the exact endpoint at render time.
	animatedShapeFilterDeclarations = [...]d2scene.DropShadow{
		{
			OffsetY: 12.6,
			SigmaX:  25.2,
			SigmaY:  25.2,
			Color:   animatedShapeShadowEndpoints[0].Color,
		},
		{
			OffsetY: 7.56,
			SigmaX:  15.12,
			SigmaY:  15.12,
			Color:   animatedShapeShadowEndpoints[1].Color,
		},
	}
)

// appendShapeGeometry nests shape geometry so static shadow and blend do not
// affect sibling icons or ordinary labels. Structured shapes remain wholly
// inside because their text and geometry form one unit.
func ( *builder) ( *d2scene.Node,  d2target.Shape,  []*d2scene.Node) {
	 := .Shadow && shapeSupportsShadow(.Type)
	if ! && !.Blend {
		.Children = append(.Children, ...)
		return
	}
	 := d2scene.NewNode(nil)
	.ID = .ID + ":shape"
	.Children = append(.Children, ...)
	if  {
		// The visible static shadow is a sharp offset/flood composite.
		.Filters = []d2scene.Filter{svgStaticShapeShadow}
	}
	if .Blend {
		.Blend = d2scene.BlendMultiply
		.Opacity = .5
	}
	.Children = append(.Children, )
}

func shapeSupportsShadow( string) bool {
	switch  {
	case d2target.ShapeText, d2target.ShapeCode, d2target.ShapeClass, d2target.ShapeSQLTable:
		return false
	default:
		return true
	}
}

func configureAnimatedShape( *d2scene.Node) {
	.Filters = []d2scene.Filter{animatedShapeFilterDeclarations[0], animatedShapeFilterDeclarations[1]}
	.Animations = []d2scene.Track{
		shapeEffectTrack(
			d2scene.AnimateTransform,
			0,
			d2scene.TransformValue(d2scene.Identity()),
			d2scene.TransformValue(d2scene.Translate(0, -4)),
			d2scene.TransformValue(d2scene.Identity()),
		),
		shapeEffectTrack(
			d2scene.AnimateDropShadow,
			0,
			d2scene.ShadowValue(animatedShapeShadowEndpoints[0]),
			d2scene.ShadowValue(animatedShapeShadowMidpoints[0]),
			d2scene.ShadowValue(animatedShapeShadowEndpoints[0]),
		),
		shapeEffectTrack(
			d2scene.AnimateDropShadow,
			1,
			d2scene.ShadowValue(animatedShapeShadowEndpoints[1]),
			d2scene.ShadowValue(animatedShapeShadowMidpoints[1]),
			d2scene.ShadowValue(animatedShapeShadowEndpoints[1]),
		),
	}
}

func ( *builder) ( *d2scene.Node,  d2target.Shape) (*d2scene.Node, error) {
	if !.Animated || len(.Children) == 0 {
		return , nil
	}
	if  := .ctx.Err();  != nil {
		return nil, 
	}
	 := 
	.Link = ""
	.PrettyLink = ""
	.Tooltip = ""
	.TooltipPosition = ""
	,  := (d2target.Diagram{Shapes: []d2target.Shape{}}).BoundingBox()
	if .X <= .X || .Y <= .Y {
		return , nil
	}
	// A CSS filter creates an isolated group even when its current value is a
	// transparent identity. d2raster correctly prunes transparent shadows, so
	// retain that stacking context with a no-op clip applied after the filter
	// chain. Its finite expansion covers both sequential shadows' complete
	// 3-sigma support plus one pixel of kernel rounding per filter.
	 := d2scene.NewBounds(float64(.X), float64(.Y), float64(.X), float64(.Y))
	.Clip = boxClip(.Expand(animatedShapeMaxFilterExpansion, animatedShapeMaxFilterExpansion).Box())
	return , .ctx.Err()
}

func shapeEffectTrack( d2scene.AnimationProperty,  int, , ,  d2scene.AnimationValue) d2scene.Track {
	return d2scene.Track{
		Property:    ,
		TargetIndex: ,
		Duration:    animatedShapeDuration,
		Repeat:      true,
		Keyframes: []d2scene.Keyframe{
			{Offset: 0, Value: },
			{Offset: .5, Value: },
			{Offset: 1, Value: },
		},
	}
}

func validateShapeEffects( string,  d2target.Shape) error {
	if .ThreeDee {
		switch .Type {
		case "", d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeHexagon:
		default:
			return unsupported(, "3d for shape type "+.Type)
		}
	}
	// 3D geometry takes precedence over multiple and double-border effects.
	if !.ThreeDee && .DoubleBorder {
		switch .Type {
		case "", d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeOval, d2target.ShapeCircle:
		default:
			return unsupported(, "double border for shape type "+.Type)
		}
		 := 2 * d2target.INNER_BORDER_OFFSET
		if .Width <  {
			return invalidField(, "width", .Width, fmt.Sprintf("must be at least %d for double border", ))
		}
		if .Height <  {
			return invalidField(, "height", .Height, fmt.Sprintf("must be at least %d for double border", ))
		}
	}
	if !.ThreeDee && .Multiple {
		switch .Type {
		case d2target.ShapeText, d2target.ShapeCode, d2target.ShapeClass, d2target.ShapeSQLTable, d2target.ShapeImage:
			return unsupported(, "multiple for shape type "+.Type)
		}
	}

	return validateShapeEffectIntegerBounds(, )
}

func validateShapeEffectIntegerBounds( string,  d2target.Shape) error {
	if !.ThreeDee && !.Multiple && !.Shadow {
		return nil
	}
	 := int64(^uint(0) >> 1)
	 := - - 1
	 := func( string,  int,  ...int) error {
		 := int64()
		for ,  := range  {
			var  bool
			,  = checkedAdd(, int64(), , )
			if ! {
				return invalidField(, , , "must fit raster effect bounds arithmetic")
			}
		}
		return nil
	}
	if .ThreeDee {
		 := d2target.THREE_DEE_OFFSET
		if .Type == d2target.ShapeHexagon {
			 /= 2
		}
		if  := ("pos.x", .Pos.X, .Width, d2target.THREE_DEE_OFFSET, .StrokeWidth);  != nil {
			return 
		}
		if  := ("pos.y", .Pos.Y, -, -.StrokeWidth);  != nil {
			return 
		}
	}
	if !.ThreeDee && .Multiple {
		if  := ("pos.x", .Pos.X, .Width, d2target.MULTIPLE_OFFSET, .StrokeWidth);  != nil {
			return 
		}
		if  := ("pos.y", .Pos.Y, -d2target.MULTIPLE_OFFSET, -.StrokeWidth);  != nil {
			return 
		}
	}
	if .Shadow {
		 := .StrokeWidth/2 + .StrokeWidth%2
		if  := ("pos.x", .Pos.X, .Width, , d2target.SHADOW_SIZE_X);  != nil {
			return 
		}
		if  := ("pos.y", .Pos.Y, .Height, , d2target.SHADOW_SIZE_Y);  != nil {
			return 
		}
	}
	return nil
}

func ( *builder) ( d2target.Shape,  d2scene.Paint,  *d2scene.Stroke) ([]*d2scene.Node, error) {
	if .ThreeDee {
		if .Type == d2target.ShapeHexagon {
			return .buildThreeDeeHexagon(, , )
		}
		return .buildThreeDeeRectangle(, , )
	}
	if .DoubleBorder {
		return .buildDoubleBorderShape(, , )
	}
	if .Multiple {
		 := 
		.Pos.X += d2target.MULTIPLE_OFFSET
		.Pos.Y -= d2target.MULTIPLE_OFFSET
		,  := .buildOrdinaryShapeOutline(, , , .ID+":multiple")
		if  != nil {
			return nil, 
		}
		,  := .buildOrdinaryShapeOutline(, , , .ID+":main")
		if  != nil {
			return nil, 
		}
		return append(, ...), nil
	}
	return nil, fmt.Errorf("scene: shape %q has no shape effect", .ID)
}

func ( *builder) ( d2target.Shape,  d2scene.Paint,  *d2scene.Stroke,  string) ([]*d2scene.Node, error) {
	 := d2scene.Box{
		X:      float64(.Pos.X),
		Y:      float64(.Pos.Y),
		Width:  float64(.Width),
		Height: float64(.Height),
	}
	var  []*d2scene.Node
	switch .Type {
	case d2target.ShapeText:
		// Text-only shapes deliberately have no outline primitive.
		return nil, nil
	case d2target.ShapeOval, d2target.ShapeCircle:
		 = append(, d2scene.NewNode(d2scene.Ellipse{
			Center:  d2scene.Point{X: .X + .Width/2, Y: .Y + .Height/2},
			RadiusX: .Width / 2,
			RadiusY: .Height / 2,
			Fill:    ,
			Stroke:  ,
		}))
	case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "":
		 := clampBorderRadius(float64(.BorderRadius), .Width, .Height)
		 = append(, d2scene.NewNode(d2scene.Rect{
			Box:     ,
			RadiusX: ,
			RadiusY: ,
			Fill:    ,
			Stroke:  ,
		}))
	default:
		 := targetGeometry()
		 := shape.GetPathCommands()
		if len() == 0 {
			return nil, unsupported(fmt.Sprintf("shape %q", .ID), "typed geometry for "+.Type)
		}
		for ,  := range  {
			,  := scenePath(, , )
			if  != nil {
				return nil, fmt.Errorf("scene: shape %q path %d: %w", .ID, , )
			}
			 := d2scene.NewNode()
			if  != "" {
				if len() == 1 {
					.ID = 
				} else {
					.ID = fmt.Sprintf("%s:path:%d", , )
				}
			}
			 = append(, )
		}
	}
	if  != "" && len() == 1 && [0].ID == "" {
		[0].ID = 
	}
	return , nil
}

func ( *builder) ( d2target.Shape,  d2scene.Paint,  *d2scene.Stroke) ([]*d2scene.Node, error) {
	var  []*d2scene.Node
	if .Multiple {
		 := 
		.Pos.X += d2target.MULTIPLE_OFFSET
		.Pos.Y -= d2target.MULTIPLE_OFFSET
		 = append(, doubleBorderPair(, , , , .ID+":multiple")...)
	}
	,  := .paint("transparent", fmt.Sprintf("shape %q double-border inner fill", .ID))
	if  != nil {
		return nil, 
	}
	 := 
	if .Type == d2target.ShapeOval || .Type == d2target.ShapeCircle {
		 = 
	}
	 = append(, doubleBorderPair(, , , , .ID+":double-border")...)
	return , nil
}

func doubleBorderPair( d2target.Shape, ,  d2scene.Paint,  *d2scene.Stroke,  string) []*d2scene.Node {
	 := d2scene.Box{
		X:      float64(.Pos.X),
		Y:      float64(.Pos.Y),
		Width:  float64(.Width),
		Height: float64(.Height),
	}
	 := float64(d2target.INNER_BORDER_OFFSET)
	 := d2scene.Box{
		X:      .X + ,
		Y:      .Y + ,
		Width:  .Width - 2*,
		Height: .Height - 2*,
	}
	var ,  *d2scene.Node
	if .Type == d2target.ShapeOval || .Type == d2target.ShapeCircle {
		 = d2scene.NewNode(d2scene.Ellipse{
			Center:  d2scene.Point{X: .X + .Width/2, Y: .Y + .Height/2},
			RadiusX: .Width / 2,
			RadiusY: .Height / 2,
			Fill:    ,
			Stroke:  ,
		})
		 = d2scene.NewNode(d2scene.Ellipse{
			Center:  d2scene.Point{X: .X + .Width/2, Y: .Y + .Height/2},
			RadiusX: .Width / 2,
			RadiusY: .Height / 2,
			Fill:    ,
			Stroke:  ,
		})
	} else {
		 := clampBorderRadius(float64(.BorderRadius), .Width, .Height)
		 := clampBorderRadius(float64(.BorderRadius), .Width, .Height)
		 = d2scene.NewNode(d2scene.Rect{
			Box: , RadiusX: , RadiusY: , Fill: , Stroke: ,
		})
		 = d2scene.NewNode(d2scene.Rect{
			Box: , RadiusX: , RadiusY: , Fill: , Stroke: ,
		})
	}
	.ID =  + ":outer"
	.ID =  + ":inner"
	return []*d2scene.Node{, }
}

func ( *builder) ( d2target.Shape,  d2scene.Paint,  *d2scene.Stroke) ([]*d2scene.Node, error) {
	 := float64(.Pos.X)
	 := float64(.Pos.Y)
	 := float64(.Width)
	 := float64(.Height)
	 := float64(d2target.THREE_DEE_OFFSET)

	 := d2scene.NewNode(d2scene.Rect{
		Box:  d2scene.Box{X: , Y: , Width: , Height: },
		Fill: ,
	})
	.ID = .ID + ":3d-main"

	,  := .threeDeeSidePaint()
	if  != nil {
		return nil, 
	}
	 := d2scene.NewNode(polygonPath([]d2scene.Point{
		{X: , Y: },
		{X:  + , Y:  - },
		{X:  +  + , Y:  - },
		{X:  +  + , Y:  +  - },
		{X:  + , Y:  + },
		{X:  + , Y: },
	}, , nil))
	.ID = .ID + ":3d-sides"

	 := d2scene.NewNode(d2scene.Path{
		Commands: []d2scene.PathCommand{
			d2scene.MoveTo(, ),
			d2scene.LineTo(+, -),
			d2scene.LineTo(++, -),
			d2scene.LineTo(++, +-),
			d2scene.LineTo(+, +),
			d2scene.LineTo(, +),
			d2scene.LineTo(, ),
			d2scene.LineTo(+, ),
			d2scene.LineTo(+, +),
			d2scene.MoveTo(+, ),
			d2scene.LineTo(++, -),
		},
		Stroke: ,
	})
	.ID = .ID + ":3d-border"
	return []*d2scene.Node{, , }, nil
}

func ( *builder) ( d2target.Shape,  d2scene.Paint,  *d2scene.Stroke) ([]*d2scene.Node, error) {
	 := .Pos.X
	 := .Pos.Y
	 := .Width
	 := .Height
	 := func( int,  float64) int { return int(float64() * ) }
	 := (, 0.25)
	 := (, 0.75)
	 := (, 43.6/87.3)
	 := d2target.THREE_DEE_OFFSET / 2
	 := func(,  int) d2scene.Point {
		return d2scene.Point{X: float64( + ), Y: float64( + )}
	}

	 := []d2scene.Point{
		(, 0),
		(, 0),
		(, ),
		(, ),
		(, ),
		(0, ),
	}
	 := d2scene.NewNode(polygonPath(, , nil))
	.ID = .ID + ":3d-main"

	,  := .threeDeeSidePaint()
	if  != nil {
		return nil, 
	}
	 := []d2scene.Point{
		(+d2target.THREE_DEE_OFFSET, -),
		(+d2target.THREE_DEE_OFFSET, -),
		(+d2target.THREE_DEE_OFFSET, -),
		(+d2target.THREE_DEE_OFFSET, -),
		(, ),
		(, ),
		(, 0),
		(, 0),
	}
	 := d2scene.NewNode(polygonPath(, , nil))
	.ID = .ID + ":3d-sides"

	 := []d2scene.PathCommand{d2scene.MoveTo([0].X, [0].Y)}
	for ,  := range []d2scene.Point{
		[0], [1], [2], [3],
		[3], [4], [5], [0], [1], [2], [3],
	} {
		 = append(, d2scene.LineTo(.X, .Y))
	}
	for ,  := range []d2scene.Point{[1], [2], [3]} {
		 = append(,
			d2scene.MoveTo(.X, .Y),
			d2scene.LineTo(.X+float64(d2target.THREE_DEE_OFFSET), .Y-float64()),
		)
	}
	 := d2scene.NewNode(d2scene.Path{Commands: , Stroke: })
	.ID = .ID + ":3d-border"
	return []*d2scene.Node{, , }, nil
}

func ( *builder) ( d2target.Shape) (d2scene.Paint, error) {
	,  := libcolor.Darken(.Fill)
	if  != nil {
		 = .Fill
	}
	return .paint(, fmt.Sprintf("shape %q 3d side fill", .ID))
}

func polygonPath( []d2scene.Point,  d2scene.Paint,  *d2scene.Stroke) d2scene.Path {
	 := d2scene.Path{Fill: , Stroke: }
	if len() == 0 {
		return 
	}
	.Commands = append(.Commands, d2scene.MoveTo([0].X, [0].Y))
	for ,  := range [1:] {
		.Commands = append(.Commands, d2scene.LineTo(.X, .Y))
	}
	.Commands = append(.Commands, d2scene.ClosePath())
	return 
}