package d2isometricimg

import (
	
	

	
	
	
)

type nativeSceneOptions struct {
	fitContent    bool
	deferRaster   bool
	vector        bool          // retain solid cap coverage beneath vector face paint
	camera        *rasterCamera // final logical pixels, before supersampling
	outputDensity float64
	links         *d2scenebuild.LinkBudget
}

const nativeTrafficRadius = .045

// Projected extents include every final mesh vertex, including outside labels,
// routed captions and document decorations. All boards use the same axes.
type projectedExtent struct {
	minX, minY, minZ, maxX, maxY, maxZ float64
	valid                              bool
}

func ( *projectedExtent) ( Vec) {
	 := nativeCameraAxes()
	, ,  := rasterDot(, .right), rasterDot(, .up), rasterDot(, .direction)
	if !.valid {
		* = projectedExtent{, , , , , , true}
		return
	}
	.minX, .minY, .minZ = min(.minX, ), min(.minY, ), min(.minZ, )
	.maxX, .maxY, .maxZ = max(.maxX, ), max(.maxY, ), max(.maxZ, )
}

func ( *projectedExtent) ( []Triangle) {
	for ,  := range  {
		for ,  := range .V {
			.add(.Position)
		}
	}
}

func ( *projectedExtent) ( []Triangle,  []nativeAnimatedNode,  float64) {
	.mesh()
	for ,  := range  {
		for  := .first;  < .last; ++ {
			for ,  := range [].V {
				.Position.Z -= 4 * 
				.add(.Position)
			}
		}
	}
	.groundShadows(, , )
}

// The ground receiver is implicit raster content, so its directional shadows
// have no mesh vertices for ordinary framing to see. Project caster vertices
// onto that same receiver, then reserve the shadow map's 3x3 sampling footprint.
// This changes framing only; the source geometry and output density stay intact.
func ( *projectedExtent) ( []Triangle,  []nativeAnimatedNode,  float64,  ...rasterCamera) {
	if len() == 0 {
		return
	}
	 := rasterShadowGround()
	 = rasterGroundTriangles(, , nativeViewDirection())
	.preparedGroundShadows(, , , , ...)
}

func ( *projectedExtent) ( []Triangle,  []nativeAnimatedNode, ,  float64,  ...rasterCamera) {
	 :=  + rasterShadowNormalOffset
	var  rasterCamera
	if len() > 0 {
		 = [0]
	} else {
		 = rasterFit(, rasterShadowDirection(), 2048, 2048, 1.08)
	}
	var  projectedExtent
	 := func( Vec) {
		 := (.Y - ) / .direction.Y
		 = nsub(, nmul(.direction, ))
		.Y = 
		.add()
	}
	 := func( Triangle,  float64,  bool) {
		if .Material != nil && .Material.Color.A == 0 {
			return
		}
		 := .CastShadow && (.ShadowOpacity == nil || *.ShadowOpacity > 0)
		if  && !.CastShadow && (.Material == nil || .Material.Texture == nil || !.NoDepthWrite) {
			 = true // frameRaster enables an authored animation's shadow
		}
		if ! {
			return
		}
		// Clip to the receiver's sampling plane. Geometry below it cannot cast
		// onto the ground; an intersecting wall contributes its intersection.
		for ,  := range .V {
			,  := .Position, .V[(+1)%3].Position
			.Z, .Z = .Z-, .Z-
			if .Y >=  {
				()
			}
			if (.Y < ) != (.Y < ) {
				(nlerp(, , (-.Y)/(.Y-.Y)))
			}
		}
	}
	for ,  := range  {
		(, 0, false)
	}
	for ,  := range  {
		for ,  := range [.first:.last] {
			(, 0, true)
			(, 4*, true)
		}
	}
	if !.valid {
		return
	}
	// A caster sample at a pixel center can influence receiver samples up to
	// 1.5 shadow texels away: floor-to-integer sampling plus the ±1 filter.
	// Move both light-camera basis vectors onto the ground to derive the
	// corresponding footprint in the final camera's coordinates.
	 := nsub(.right, nmul(.direction, .right.Y/.direction.Y))
	 := nsub(.up, nmul(.direction, .up.Y/.direction.Y))
	,  := 1.5/.scale, nativeCameraAxes()
	 := func( Vec) float64 { return  * (math.Abs(ndot(, )) + math.Abs(ndot(, ))) }
	, ,  := (.right), (.up), (.direction)
	.minX, .maxX = .minX-, .maxX+
	.minY, .maxY = .minY-, .maxY+
	.minZ, .maxZ = .minZ-, .maxZ+
	if !.valid {
		* = 
		return
	}
	.minX, .minY, .minZ = min(.minX, .minX), min(.minY, .minY), min(.minZ, .minZ)
	.maxX, .maxY, .maxZ = max(.maxX, .maxX), max(.maxY, .maxY), max(.maxZ, .maxZ)
}

// A packet interpolates linearly between routed vertices. Expanding their
// projected union by the physical sphere radius therefore contains every
// traffic phase, including packets at endpoints and on crossing bridges.
func ( *projectedExtent) ( []packetRoute) {
	var  projectedExtent
	for ,  := range  {
		for ,  := range .points {
			.add()
		}
	}
	if !.valid {
		return
	}
	.minX -= nativeTrafficRadius
	.minY -= nativeTrafficRadius
	.minZ -= nativeTrafficRadius
	.maxX += nativeTrafficRadius
	.maxY += nativeTrafficRadius
	.maxZ += nativeTrafficRadius
	if !.valid {
		* = 
		return
	}
	.minX, .minY, .minZ = min(.minX, .minX), min(.minY, .minY), min(.minZ, .minZ)
	.maxX, .maxY, .maxZ = max(.maxX, .maxX), max(.maxY, .maxY), max(.maxZ, .maxZ)
}

func nativeCameraAxes() rasterCamera {
	 := rasterUnit(nativeViewDirection())
	 := rasterUnit(rasterCross(Vec{Y: 1}, ))
	return rasterCamera{direction: , right: , up: rasterUnit(rasterCross(, ))}
}

func ( projectedExtent) (,  int,  bool) rasterCamera {
	if !.valid {
		 = projectedExtent{-1, -1, -1, 1, 1, 1, true}
	}
	,  := max(.02, .maxX-.minX), max(.02, .maxY-.minY)
	if  {
		// Preserve the maximum image budget and source geometry. The smaller
		// side contracts; very thin diagrams retain the 64-pixel admission floor.
		 := min(float64()/, float64()/)
		 = max(64, min(, int(math.Ceil(*))))
		 = max(64, min(, int(math.Ceil(*))))
	}
	 := nativeCameraAxes()
	.width, .height = , 
	.centerX, .centerY, .centerDepth = (.minX+.maxX)/2, (.minY+.maxY)/2, (.minZ+.maxZ)/2
	.scale = min(float64()/, float64()/) / 1.08
	return 
}

func cameraAtResolution( rasterCamera, ,  int) rasterCamera {
	,  := float64()/float64(.width), float64()/float64(.height)
	.scale *= min(, )
	.width, .height = , 
	return 
}

// Before textures exist, source geometry estimates their output sampling rate.
// Extra labels/decorations can only enlarge final bounds and lower that rate;
// texture budgets cap the conservative estimate. No layout dimensions change.
func sceneOutputDensity( *d2isometric.Scene, ,  int,  *rasterCamera) float64 {
	if  != nil {
		return .scale
	}
	var  projectedExtent
	for ,  := range .Boards {
		if .Kind == "ungrouped" {
			continue
		}
		for ,  := range []float64{-.5, .5} {
			for ,  := range []float64{-.5, .5} {
				.add(nadd(.Position, nv(*.Size.X, 0, *.Size.Z)))
			}
		}
	}
	for ,  := range .Nodes {
		if .Opacity <= 0 {
			continue
		}
		 := .Position.Y - .Size.Y/2
		 := max(.3, .Size.Y)*1.3 + .2
		if nativeSolidNode() {
			 = nativeSolidHeight() + .002
		} else if nativeReliefSymbol() || nativeStructuredNode() {
			 = nativeCanonicalHeight(, .PixelScale) + .002
		}
		 *= hierarchyNodeRelief()
		for ,  := range []float64{-.5, .5} {
			for ,  := range []float64{-.5, .5} {
				for ,  := range []float64{,  + } {
					.add(nv(.Position.X+*.Size.X, , .Position.Z+*.Size.Z))
				}
			}
		}
	}
	for ,  := range .Edges {
		for ,  := range .Points {
			.add()
		}
	}
	return .camera(, , false).scale
}

// Measure one board at a time so animation framing uses the union of the exact
// final geometry without retaining every board's textures or raster buffers.
func timelineCamera( context.Context,  []*d2target.Diagram,  Options) (rasterCamera, error) {
	var  projectedExtent
	for ,  := range  {
		if  := .Err();  != nil {
			return rasterCamera{}, 
		}
		,  := d2isometric.BuildScene(, &.Render)
		if  != nil {
			return rasterCamera{}, 
		}
		,  := newNativeSceneWithOptions(, , .Width, .Height, .Assets, .Fonts, nativeSceneOptions{deferRaster: true, outputDensity: sceneOutputDensity(, .Width, .Height, nil)})
		if  != nil {
			return rasterCamera{}, 
		}
		.animatedMesh(.triangles, .animatedNodes, .pixelScale)
		.trafficRoutes(.packets)
	}
	return .camera(.Width, .Height, .FitContent), nil
}