package d2isometricimg

import (
	
	
	
	
	
	
	
	
	

	
	
	
)

// SVG consumes the same physical scene as PNG. Visibility is resolved in
// projected geometry; neither a framebuffer nor a screen-sized image is used.
func renderNativeSVG( context.Context,  *d2target.Diagram,  Options) ([]byte, error) {
	if  == nil {
		 = context.Background()
	}
	,  := context.WithTimeout(, .Timeout)
	defer ()
	if  := .Err();  != nil {
		return nil, 
	}
	 = nativeVectorContext()
	if .LinkBudget == nil {
		.LinkBudget = &d2scenebuild.LinkBudget{MaxRegions: 4096, MaxStringBytes: 1 << 20}
	}
	if  := preflightSVGMetadata(, , *.LinkBudget);  != nil {
		return nil, 
	}
	,  := d2isometric.BuildScene(, &.Render)
	if  != nil {
		return nil, 
	}
	,  := newNativeSceneWithOptions(, , .Width, .Height, .Assets, .Fonts, nativeSceneOptions{
		deferRaster: true, vector: true, fitContent: .FitContent, camera: .camera,
		outputDensity: sceneOutputDensity(, .Width, .Height, .camera), links: .LinkBudget,
	})
	if  != nil {
		return nil, fmt.Errorf("initialize native SVG: %w", )
	}
	return nativeSceneSVG(, )
}

func preflightSVGMetadata( context.Context,  *d2target.Diagram,  d2scenebuild.LinkBudget) error {
	if  == nil {
		return fmt.Errorf("native SVG requires a diagram")
	}
	,  := 0, 0
	 := func(,  string) error {
		if  == "" &&  == "" {
			return .Err()
		}
		++
		if  > .MaxRegions {
			return fmt.Errorf("native SVG exceeds link region budget")
		}
		for ,  := range []string{, } {
			if len() > .MaxStringBytes- {
				return fmt.Errorf("native SVG exceeds link text budget")
			}
			 += len()
			if  := validatePageLinkText(, );  != nil {
				return 
			}
		}
		return validateNativeLink()
	}
	for ,  := range .Shapes {
		if  := (.Link, .Tooltip);  != nil {
			return 
		}
	}
	for ,  := range .Connections {
		if  := (.Link, .Tooltip);  != nil {
			return 
		}
	}
	return .Err()
}

type nativeSVGWriter struct {
	ctx      context.Context
	buf      bytes.Buffer
	err      error
	surfaces map[*nativeVectorSurface]string
}

func ( *nativeSVGWriter) ( string,  ...any) {
	if .err != nil {
		return
	}
	if .err = .ctx.Err(); .err != nil {
		return
	}
	fmt.Fprintf(&.buf, , ...)
	if .buf.Len() > MaxOutputBytes {
		.err = fmt.Errorf("native SVG exceeds %d output bytes", MaxOutputBytes)
	}
}

func svgNumber( float64) string {
	if math.Abs() < .0000005 {
		return "0"
	}
	return strings.TrimRight(strings.TrimRight(strconv.FormatFloat(, 'f', 6, 64), "0"), ".")
}

func svgColor(, ,  float64) string {
	return fmt.Sprintf("#%02x%02x%02x", uint8(math.Round(rasterClamp()*255)), uint8(math.Round(rasterClamp()*255)), uint8(math.Round(rasterClamp()*255)))
}

func svgPolygonPath( [][]svgPoint) string {
	var  strings.Builder
	for ,  := range  {
		// Serialize a canonical loop. Clipping can retain a different starting
		// vertex at an exactly shared edge, without changing its silhouette.
		// Discard vertices that coincide at SVG precision before choosing it.
		 := make([]svgPoint, 0, len())
		for ,  := range  {
			 = svgPoint{x: math.Round(.x*1e6) / 1e6, y: math.Round(.y*1e6) / 1e6}
			if len() == 0 ||  != [len()-1] {
				 = append(, )
			}
		}
		if len() > 1 && [0] == [len()-1] {
			 = [:len()-1]
		}
		 = svgOmitCollinearSplits()
		if len() < 3 {
			continue
		}
		 := 2 * svgPolygonArea()
		if math.Abs() < 1e-10 {
			continue
		}
		// A nonzero compound path is a union, including overlapping pieces.
		 := 0
		for  := range  {
			if [].x < [].x || [].x == [].x && [].y < [].y {
				 = 
			}
		}
		for  := range  {
			 := ( + ) % len()
			if  < 0 {
				 = ( + len() - ) % len()
			}
			if  == 0 {
				.WriteByte('M')
			} else {
				.WriteByte('L')
			}
			.WriteString(svgNumber([].x))
			.WriteByte(' ')
			.WriteString(svgNumber([].y))
		}
		.WriteByte('Z')
	}
	return .String()
}

// Clipping nearly coincident planes can place a redundant intersection at
// different positions along the same straight edge on different CPUs. Drop
// such splits only after projection, at the SVG's six-decimal output precision;
// the visibility mesh and its depth values remain unchanged. Work in place on
// the serializer's private, rounded copy, retaining at least three vertices.
func svgOmitCollinearSplits( []svgPoint) []svgPoint {
	if len() <= 3 {
		return 
	}
	 := func(, ,  svgPoint) bool {
		,  := .x-.x, .y-.y
		,  := .x-.x, .y-.y
		 := float64(*) + float64(*)
		 := float64(*) + float64(*)
		if  == 0 ||  < 0 ||  >  {
			return false
		}
		 := float64(*) - float64(*)
		return math.Abs() <= 1e-6*math.Sqrt()
	}
	 := [:0]
	for ,  := range  {
		for len() > 1 && len()+len()- > 3 && ([len()-2], [len()-1], ) {
			 = [:len()-1]
		}
		 = append(, )
	}
	for len() > 3 {
		if ([len()-1], [0], [1]) {
			 = [1:]
		} else if ([len()-2], [len()-1], [0]) {
			 = [:len()-1]
		} else {
			break
		}
	}
	return 
}

type svgPaintBatch struct {
	cover    *svgPaintCover
	triangle Triangle
	polygons [][]svgPoint
	depth    float64
	first    int
	affine   [6]float64
	texture  bool
	color    string
	gradient [3]Vec
	points   [3]svgPoint
}

type svgPaintUnit struct {
	batches []*svgPaintBatch
	depth   float64
	count   int
	first   int
	opacity float64
	opaque  bool
}

type svgBatchKey struct {
	material *Material
	affine   [6]int64
	color    string
	unique   int
}

func nativeSceneSVG( context.Context,  *nativeScene) ([]byte, error) {
	if  == nil {
		return nil, fmt.Errorf("native SVG requires a scene")
	}
	if  := rasterValidate(, .triangles);  != nil {
		return nil, 
	}
	 := .camera
	 := make([]svgVisibilityFace, len(.triangles))
	for ,  := range .triangles {
		 := &[]
		.order, .group = , .OpacityGroup
		.owner = .PaintOwner
		 := .Material
		.contour =  != nil && .svgContour
		 := rasterMaterial()
		if .alpha == 0 {
			continue
		}
		if .alpha < .999 || .multiply {
			 := nadd(nadd(.V[0].Normal, .V[1].Normal), .V[2].Normal)
			if ndot(, ) > 1e-20 && ndot(, .direction) <= 0 {
				continue
			}
		}
		.opaque = !.NoDepthWrite && .alpha == 1 && (.texture == nil || .svgSolidTexture) && !.multiply
		for ,  := range .V {
			 := .project(.Position)
			.points = append(.points, svgProjectedPoint(.x, .y, .z+.DepthBias))
		}
	}
	,  := svgVisibleFaces(, )
	if  != nil {
		return nil, 
	}
	type  struct {
		    *nativeOpacityGroup
		    *nativePaintOwner
		 *Material
	}
	 := []*svgPaintUnit{}
	 := make(map[]int)
	 := []map[svgBatchKey]*svgPaintBatch{}
	 := &Raster{camera: }
	for ,  := range .triangles {
		if .svgCoverageOnly || len([]) == 0 || .Material != nil && .Material.Color.A == 0 {
			continue
		}
		if  := .Err();  != nil {
			return nil, 
		}
		 := .Material
		if  == nil {
			 = &Material{Color: color.NRGBA{240, 243, 249, 255}, Roughness: .35}
			.Material = 
		}
		 := {: .OpacityGroup}
		if . == nil {
			. = .PaintOwner
		}
		if . == nil && . == nil {
			. = 
		}
		,  := []
		if ! {
			 = len()
			[] = 
			 := &svgPaintUnit{first: , opacity: 1, opaque: [].opaque}
			if . != nil {
				.opaque = ..Opaque
			}
			if . != nil {
				.opacity, .opaque = ..Opacity, false
			}
			 = append(, )
			 = append(, make(map[svgBatchKey]*svgPaintBatch))
		}
		 := []
		 := 0.
		for ,  := range [].points {
			 += .z / 3
		}
		.depth, .count = .depth+, .count+1
		 := &svgPaintBatch{triangle: , polygons: [], depth: , first: }
		copy(.points[:], [].points)
		 := svgBatchKey{material: }
		if .Texture != nil {
			if .Vector == nil {
				return nil, fmt.Errorf("native SVG surface %d has no retained vector source", )
			}
			.texture = true
			.affine,  = svgTextureAffine(, .points)
			if  != nil {
				return nil, 
			}
			for ,  := range .affine {
				.affine[] = int64(math.Round( * 1e8))
			}
		} else {
			 := true
			 := nunit(ncross(nsub(.V[1].Position, .V[0].Position), nsub(.V[2].Position, .V[0].Position)))
			var  Vec
			for ,  := range .V {
				 := .Normal
				if nlen() < 1e-12 {
					 = 
				}
				, ,  := .shade(rasterMaterial(), .Position, )
				.gradient[] = nv(, , )
				if  == 0 {
					 = nunit()
				} else if !.Unlit && ndot(, nunit()) < 1-1e-10 {
					 = false
				}
				if  > 0 && nlen(nsub(.gradient[0], .gradient[])) > 1./510 {
					 = false
				}
			}
			if  {
				 := .gradient[0]
				.color = svgColor(.X, .Y, .Z)
				.color = .color
			} else {
				.unique =  + 1
			}
		}
		if  := [][];  != nil {
			.polygons = append(.polygons, .polygons...)
		} else {
			.batches = append(.batches, )
			[][] = 
		}
	}
	for ,  := range  {
		.depth /= float64(.count)
		sort.SliceStable(.batches, func(,  int) bool {
			,  := .batches[], .batches[]
			,  := svgIsDecal(.triangle), svgIsDecal(.triangle)
			if  !=  {
				return !
			}
			if  {
				return .first < .first
			}
			return .depth < .depth
		})
		for ,  := range svgPreparePaintCovers(.batches) {
			.cover = 
		}
	}
	if  := svgOrderPaintUnits(, );  != nil {
		return nil, 
	}

	 := &nativeSVGWriter{ctx: }
	.write(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d" viewBox="0 0 %d %d" role="img"><title>D2 isometric diagram</title>`, .width, .height, .width, .height)
	.write(`<rect width="100%%" height="100%%" fill="%s"/>`, svgColor(float64(.background.R)/255, float64(.background.G)/255, float64(.background.B)/255))
	if .backgroundPaint != nil {
		 := nativePaintDocument(.backgroundPaint)
		,  := nativeSurfaceSVG(, &nativeVectorSurface{document: }, "background")
		if  != nil {
			return nil, 
		}
		.write(`<g transform="scale(%d %d)">%s</g>`, .width, .height, )
	}
	writeSVGGroundShadow(, )
	 := 0
	for ,  := range  {
		.write(`<g opacity="%s">`, svgNumber(.opacity))
		for ,  := range .batches {
			++
			writeSVGBatch(, , , )
			if .err != nil {
				return nil, .err
			}
		}
		.write(`</g>`)
	}
	writeSVGLinks(, )
	.write(`</svg>`)
	if .err != nil {
		return nil, .err
	}
	return .buf.Bytes(), nil
}

func svgIsDecal( Triangle) bool {
	return .NoDepthWrite || .Material != nil && (.Material.svgContour || .Material.Texture != nil || .Material.Multiply || .Material.Color.A < 255)
}

func svgTextureAffine( Triangle,  [3]svgPoint) ([6]float64, error) {
	,  := .V[1].U-.V[0].U, .V[1].V-.V[0].V
	,  := .V[2].U-.V[0].U, .V[2].V-.V[0].V
	 := float64(*) - float64(*)
	if math.Abs() < 1e-15 {
		return [6]float64{}, fmt.Errorf("native SVG texture has a degenerate UV mapping")
	}
	, , ,  := [1].x-[0].x, [1].y-[0].y, [2].x-[0].x, [2].y-[0].y
	, , ,  := (float64(*)-float64(*))/, (float64(*)-float64(*))/, (float64(*)-float64(*))/, (float64(*)-float64(*))/
	return [6]float64{, , , , [0].x - float64(*.V[0].U) - float64(*.V[0].V), [0].y - float64(*.V[0].U) - float64(*.V[0].V)}, nil
}

func writeSVGBatch( *nativeSVGWriter,  *svgPaintBatch,  int,  rasterCamera) {
	if .cover != nil {
		writeSVGPaintCover(, .cover, )
		return
	}
	 := svgPolygonPath(.polygons)
	if  == "" {
		return
	}
	 := .triangle.Material
	 := float64(.Color.A) / 255
	 := ""
	if .Multiply {
		 = ` style="mix-blend-mode:multiply"`
	}
	if .texture {
		 := .surfaceDefinition(.Vector)
		if .err != nil {
			return
		}
		.write(`<defs><clipPath id="face-%d" clipPathUnits="userSpaceOnUse"><path d="%s"/></clipPath></defs>`, , )
		 := ""
		if !.Unlit {
			,  := svgTextureLighting(, .triangle)
			.write(`<defs><filter id="light-%d" color-interpolation-filters="linearRGB"><feComponentTransfer><feFuncR type="linear" slope="%s" intercept="%s"/><feFuncG type="linear" slope="%s" intercept="%s"/><feFuncB type="linear" slope="%s" intercept="%s"/></feComponentTransfer></filter></defs>`, , svgNumber([0]), svgNumber([0]), svgNumber([1]), svgNumber([1]), svgNumber([2]), svgNumber([2]))
			 = fmt.Sprintf(` filter="url(#light-%d)"`, )
		}
		 := .affine
		.write(`<g clip-path="url(#face-%d)" opacity="%s"%s><g transform="matrix(%s %s %s %s %s %s)"%s><use xlink:href="#%s"/></g></g>`, , svgNumber(), , svgNumber([0]), svgNumber([1]), svgNumber([2]), svgNumber([3]), svgNumber([4]), svgNumber([5]), , )
		return
	}
	 := .color
	if  == "" {
		 = writeSVGFaceGradient(, , , )
	}
	.write(`<path d="%s" fill="%s" fill-opacity="%s"%s/>`, , , svgNumber(), )
}

func writeSVGLinks( *nativeSVGWriter,  *nativeScene) {
	type  struct {
		   nativeLink
		 []svgPoint
		   float64
	}
	 := make([], len(.links))
	for ,  := range .links {
		 := &[]
		. = 
		for ,  := range .points {
			 := .camera.project()
			. = append(., svgProjectedPoint(.x, .y, .z))
		}
		// Hit regions use the same canonical projection and rounded area
		// arithmetic as physical faces. Tiny FMA residuals must not reorder
		// equal-size links, changing which overlapping target wins a click.
		. = math.Abs(svgPolygonArea(.))
	}
	sort.SliceStable(, func(,  int) bool { return []. > []. })
	for ,  := range  {
		,  := ., .
		 := .region.URL
		if  == "" && .region.Target != "" {
			 = "#" + .region.Target
		}
		.write(`<a xlink:href="%s">`, html.EscapeString())
		if .region.Tooltip != "" {
			.write(`<title>%s</title>`, html.EscapeString(.region.Tooltip))
		}
		.write(`<path d="%s" fill="transparent" pointer-events="all"/></a>`, svgPolygonPath([][]svgPoint{}))
	}
}