package renderer

import (
	"bytes"
	"fmt"
	"image"
	"image/color"
	"image/png"
	"math"
	"sync"

	"gitea.dockr.co/kev/go-jdenticon/internal/engine"
)

// PNG rendering constants
const (
	defaultSupersamplingFactor = 8 // Default antialiasing supersampling factor
)

// Memory pools for reducing allocations during rendering
var (
	// Pool for point slices used during polygon processing
	// Uses pointer to slice to avoid allocation during type assertion (SA6002)
	pointSlicePool = sync.Pool{
		New: func() interface{} {
			s := make([]engine.Point, 0, 16) // Pre-allocate reasonable capacity
			return &s
		},
	}

	// Pool for color row buffers
	// Uses pointer to slice to avoid allocation during type assertion (SA6002)
	colorRowBufferPool = sync.Pool{
		New: func() interface{} {
			s := make([]color.RGBA, 0, 1024) // Row buffer capacity
			return &s
		},
	}
)

// ShapeCommand represents a rendering command for deferred execution
type ShapeCommand struct {
	Type   string         // "polygon", "circle", "background"
	Points []engine.Point // For polygons
	Center engine.Point   // For circles
	Size   float64        // For circles
	Invert bool           // For circles
	Color  color.RGBA
	BBox   image.Rectangle // Pre-calculated bounding box for culling
}

// PNGRenderer implements memory-efficient PNG generation using streaming row processing
// This eliminates the dual buffer allocation problem, reducing memory usage by ~80%
type PNGRenderer struct {
	*BaseRenderer
	finalImg  *image.RGBA    // Single buffer at target resolution
	finalSize int            // Target output size
	bgColor   color.RGBA     // Background color
	shapes    []ShapeCommand // Queued rendering commands
}

// NewPNGRenderer creates a new memory-optimized PNG renderer
func NewPNGRenderer(iconSize int) *PNGRenderer {
	// Only allocate the final image buffer - no supersampled buffer
	finalBounds := image.Rect(0, 0, iconSize, iconSize)
	finalImg := image.NewRGBA(finalBounds)

	return &PNGRenderer{
		BaseRenderer: NewBaseRenderer(iconSize),
		finalImg:     finalImg,
		finalSize:    iconSize,
		shapes:       make([]ShapeCommand, 0, 16), // Pre-allocate for typical use
	}
}

// SetBackground sets the background color - queues background command
func (r *PNGRenderer) SetBackground(fillColor string, opacity float64) {
	r.BaseRenderer.SetBackground(fillColor, opacity)

	r.bgColor = r.parseColor(fillColor, opacity)

	// Queue background command for proper rendering order
	r.shapes = append(r.shapes, ShapeCommand{
		Type:  "background",
		Color: r.bgColor,
		BBox:  image.Rect(0, 0, r.finalSize*2, r.finalSize*2), // Full supersampled bounds
	})
}

// BeginShape marks the beginning of a new shape with the specified color
func (r *PNGRenderer) BeginShape(fillColor string) {
	r.BaseRenderer.BeginShape(fillColor)
}

// EndShape marks the end of the currently drawn shape (no-op for queuing renderer)
func (r *PNGRenderer) EndShape() {
	// No-op for command queuing approach
}

// AddPolygon queues a polygon command with pre-calculated bounding box
func (r *PNGRenderer) AddPolygon(points []engine.Point) {
	if len(points) < 3 {
		return // Can't render polygon with < 3 points
	}

	// Determine winding order for hole detection
	var area float64
	for i := 0; i < len(points); i++ {
		p1 := points[i]
		p2 := points[(i+1)%len(points)]
		area += (p1.X * p2.Y) - (p2.X * p1.Y)
	}

	var renderColor color.RGBA
	if area < 0 {
		// Counter-clockwise winding (hole) - use background color
		renderColor = r.bgColor
	} else {
		// Clockwise winding (normal shape)
		renderColor = r.parseColor(r.GetCurrentColor(), 1.0)
	}

	// Get pooled point slice and scale points to supersampled coordinates
	scaledPointsPtr := pointSlicePool.Get().(*[]engine.Point)
	scaledPointsSlice := *scaledPointsPtr
	defer func() {
		*scaledPointsPtr = scaledPointsSlice // Update with potentially resized slice
		pointSlicePool.Put(scaledPointsPtr)
	}()

	// Reset slice and ensure capacity
	scaledPointsSlice = scaledPointsSlice[:0]
	if cap(scaledPointsSlice) < len(points) {
		scaledPointsSlice = make([]engine.Point, 0, len(points)*2)
	}

	minX, minY := math.MaxFloat64, math.MaxFloat64
	maxX, maxY := -math.MaxFloat64, -math.MaxFloat64

	for _, p := range points {
		scaledP := engine.Point{
			X: p.X * defaultSupersamplingFactor,
			Y: p.Y * defaultSupersamplingFactor,
		}
		scaledPointsSlice = append(scaledPointsSlice, scaledP)

		if scaledP.X < minX {
			minX = scaledP.X
		}
		if scaledP.X > maxX {
			maxX = scaledP.X
		}
		if scaledP.Y < minY {
			minY = scaledP.Y
		}
		if scaledP.Y > maxY {
			maxY = scaledP.Y
		}
	}

	// Copy scaled points for storage in command (must copy since we're returning slice to pool)
	scaledPoints := make([]engine.Point, len(scaledPointsSlice))
	copy(scaledPoints, scaledPointsSlice)

	// Create bounding box for culling (with safety margins)
	bbox := image.Rect(
		int(math.Floor(minX))-1,
		int(math.Floor(minY))-1,
		int(math.Ceil(maxX))+1,
		int(math.Ceil(maxY))+1,
	)

	// Queue the polygon command
	r.shapes = append(r.shapes, ShapeCommand{
		Type:   "polygon",
		Points: scaledPoints,
		Color:  renderColor,
		BBox:   bbox,
	})
}

// AddCircle queues a circle command with pre-calculated bounding box
func (r *PNGRenderer) AddCircle(topLeft engine.Point, size float64, invert bool) {
	// Scale to supersampled coordinates
	scaledTopLeft := engine.Point{
		X: topLeft.X * defaultSupersamplingFactor,
		Y: topLeft.Y * defaultSupersamplingFactor,
	}
	scaledSize := size * defaultSupersamplingFactor

	centerX := scaledTopLeft.X + scaledSize/2.0
	centerY := scaledTopLeft.Y + scaledSize/2.0
	radius := scaledSize / 2.0

	var renderColor color.RGBA
	if invert {
		renderColor = r.bgColor
	} else {
		renderColor = r.parseColor(r.GetCurrentColor(), 1.0)
	}

	// Calculate bounding box for the circle
	bbox := image.Rect(
		int(math.Floor(centerX-radius))-1,
		int(math.Floor(centerY-radius))-1,
		int(math.Ceil(centerX+radius))+1,
		int(math.Ceil(centerY+radius))+1,
	)

	// Queue the circle command
	r.shapes = append(r.shapes, ShapeCommand{
		Type:   "circle",
		Center: engine.Point{X: centerX, Y: centerY},
		Size:   radius,
		Color:  renderColor,
		BBox:   bbox,
	})
}

// ToPNG generates the final PNG image data using streaming row processing
func (r *PNGRenderer) ToPNG() ([]byte, error) {
	return r.ToPNGWithSize(r.GetSize())
}

// ToPNGWithSize generates PNG image data with streaming row processing
func (r *PNGRenderer) ToPNGWithSize(outputSize int) ([]byte, error) {
	// Execute streaming rendering pipeline
	r.renderWithStreaming()

	var resultImg image.Image = r.finalImg

	// Scale if output size differs from internal size
	if outputSize != r.finalSize {
		resultImg = r.scaleImage(r.finalImg, outputSize)
	}

	// Encode to PNG with maximum compression
	var buf bytes.Buffer
	encoder := &png.Encoder{
		CompressionLevel: png.BestCompression,
	}

	err := encoder.Encode(&buf, resultImg)
	if err != nil {
		return nil, fmt.Errorf("jdenticon: optimized renderer: PNG encoding failed: %w", err)
	}

	return buf.Bytes(), nil
}

// renderWithStreaming executes the main streaming rendering pipeline
func (r *PNGRenderer) renderWithStreaming() {
	supersampledWidth := r.finalSize * defaultSupersamplingFactor

	// Get pooled row buffer for 2 supersampled rows - MASSIVE memory savings
	rowBufferPtr := colorRowBufferPool.Get().(*[]color.RGBA)
	rowBufferSlice := *rowBufferPtr
	defer func() {
		*rowBufferPtr = rowBufferSlice // Update with potentially resized slice
		colorRowBufferPool.Put(rowBufferPtr)
	}()

	// Ensure buffer has correct size
	requiredSize := supersampledWidth * 2
	if cap(rowBufferSlice) < requiredSize {
		rowBufferSlice = make([]color.RGBA, requiredSize)
	} else {
		rowBufferSlice = rowBufferSlice[:requiredSize]
	}

	// Process each final image row
	for y := 0; y < r.finalSize; y++ {
		// Clear row buffer to background color
		for i := range rowBufferSlice {
			rowBufferSlice[i] = r.bgColor
		}

		// Render all shapes for this row pair
		r.renderShapesForRowPair(y, rowBufferSlice, supersampledWidth)

		// Downsample directly into final image
		r.downsampleRowPairToFinal(y, rowBufferSlice, supersampledWidth)
	}
}

// renderShapesForRowPair renders all shapes that intersect the given row pair
func (r *PNGRenderer) renderShapesForRowPair(finalY int, rowBuffer []color.RGBA, supersampledWidth int) {
	// Calculate supersampled Y range for this row pair
	ssYStart := finalY * defaultSupersamplingFactor
	ssYEnd := ssYStart + defaultSupersamplingFactor

	// Render each shape that intersects this row pair
	for _, shape := range r.shapes {
		// Fast bounding box culling
		if shape.BBox.Max.Y <= ssYStart || shape.BBox.Min.Y >= ssYEnd {
			continue // Shape doesn't intersect this row pair
		}

		switch shape.Type {
		case "polygon":
			r.renderPolygonForRowPair(shape, ssYStart, ssYEnd, rowBuffer, supersampledWidth)
		case "circle":
			r.renderCircleForRowPair(shape, ssYStart, ssYEnd, rowBuffer, supersampledWidth)
		}
	}
}

// renderPolygonForRowPair renders a polygon for the specified row range
func (r *PNGRenderer) renderPolygonForRowPair(shape ShapeCommand, ssYStart, ssYEnd int, rowBuffer []color.RGBA, supersampledWidth int) {
	points := shape.Points
	color := shape.Color

	// Use triangle fan decomposition for simplicity
	if len(points) == 3 {
		// Direct triangle rendering
		r.fillTriangleForRowRange(points[0], points[1], points[2], color, ssYStart, ssYEnd, rowBuffer, supersampledWidth)
	} else if len(points) == 4 && r.isRectangle(points) {
		// Optimized rectangle rendering
		minX, minY, maxX, maxY := r.getBoundsFloat(points)
		r.fillRectForRowRange(minX, minY, maxX, maxY, color, ssYStart, ssYEnd, rowBuffer, supersampledWidth)
	} else {
		// General polygon - triangle fan from first vertex
		for i := 1; i < len(points)-1; i++ {
			r.fillTriangleForRowRange(points[0], points[i], points[i+1], color, ssYStart, ssYEnd, rowBuffer, supersampledWidth)
		}
	}
}

// renderCircleForRowPair renders a circle for the specified row range
func (r *PNGRenderer) renderCircleForRowPair(shape ShapeCommand, ssYStart, ssYEnd int, rowBuffer []color.RGBA, supersampledWidth int) {
	centerX := shape.Center.X
	centerY := shape.Center.Y
	radius := shape.Size
	color := shape.Color
	radiusSq := radius * radius

	// Process each supersampled row in the range
	for y := ssYStart; y < ssYEnd; y++ {
		yFloat := float64(y)
		dy := yFloat - centerY
		dySq := dy * dy

		if dySq > radiusSq {
			continue // Row doesn't intersect circle
		}

		// Calculate horizontal span for this row
		dx := math.Sqrt(radiusSq - dySq)
		xStart := int(math.Floor(centerX - dx))
		xEnd := int(math.Ceil(centerX + dx))

		// Clip to buffer bounds
		if xStart < 0 {
			xStart = 0
		}
		if xEnd >= supersampledWidth {
			xEnd = supersampledWidth - 1
		}

		// Fill the horizontal span
		rowIndex := (y - ssYStart) * supersampledWidth
		for x := xStart; x <= xEnd; x++ {
			// Verify pixel is actually inside circle
			dxPixel := float64(x) - centerX
			if dxPixel*dxPixel+dySq <= radiusSq {
				if rowIndex+x < len(rowBuffer) {
					rowBuffer[rowIndex+x] = color
				}
			}
		}
	}
}

// fillTriangleForRowRange fills a triangle within the specified row range
func (r *PNGRenderer) fillTriangleForRowRange(p1, p2, p3 engine.Point, color color.RGBA, ssYStart, ssYEnd int, rowBuffer []color.RGBA, supersampledWidth int) {
	// Get triangle bounds
	minY := math.Min(math.Min(p1.Y, p2.Y), p3.Y)
	maxY := math.Max(math.Max(p1.Y, p2.Y), p3.Y)

	// Clip to row range
	iterYStart := int(math.Max(math.Ceil(minY), float64(ssYStart)))
	iterYEnd := int(math.Min(math.Floor(maxY), float64(ssYEnd-1)))

	if iterYStart > iterYEnd {
		return // Triangle doesn't intersect row range
	}

	// Sort points by Y coordinate
	x1, y1 := p1.X, p1.Y
	x2, y2 := p2.X, p2.Y
	x3, y3 := p3.X, p3.Y

	if y1 > y2 {
		x1, y1, x2, y2 = x2, y2, x1, y1
	}
	if y1 > y3 {
		x1, y1, x3, y3 = x3, y3, x1, y1
	}
	if y2 > y3 {
		x2, y2, x3, y3 = x3, y3, x2, y2
	}

	// Fill triangle using scan-line algorithm
	for y := iterYStart; y <= iterYEnd; y++ {
		yFloat := float64(y)
		var xLeft, xRight float64

		if yFloat < y2 {
			// Upper part of triangle
			if y2 != y1 {
				slope12 := (x2 - x1) / (y2 - y1)
				xLeft = x1 + slope12*(yFloat-y1)
			} else {
				xLeft = x1
			}
			if y3 != y1 {
				slope13 := (x3 - x1) / (y3 - y1)
				xRight = x1 + slope13*(yFloat-y1)
			} else {
				xRight = x1
			}
		} else {
			// Lower part of triangle
			if y3 != y2 {
				slope23 := (x3 - x2) / (y3 - y2)
				xLeft = x2 + slope23*(yFloat-y2)
			} else {
				xLeft = x2
			}
			if y3 != y1 {
				slope13 := (x3 - x1) / (y3 - y1)
				xRight = x1 + slope13*(yFloat-y1)
			} else {
				xRight = x1
			}
		}

		if xLeft > xRight {
			xLeft, xRight = xRight, xLeft
		}

		// Convert to pixel coordinates and fill
		xLeftInt := int(math.Floor(xLeft))
		xRightInt := int(math.Floor(xRight))

		// Clip to buffer bounds
		if xLeftInt < 0 {
			xLeftInt = 0
		}
		if xRightInt >= supersampledWidth {
			xRightInt = supersampledWidth - 1
		}

		// Fill horizontal span in row buffer
		rowIndex := (y - ssYStart) * supersampledWidth
		for x := xLeftInt; x <= xRightInt; x++ {
			if rowIndex+x < len(rowBuffer) {
				rowBuffer[rowIndex+x] = color
			}
		}
	}
}

// fillRectForRowRange fills a rectangle within the specified row range
func (r *PNGRenderer) fillRectForRowRange(x1, y1, x2, y2 float64, color color.RGBA, ssYStart, ssYEnd int, rowBuffer []color.RGBA, supersampledWidth int) {
	// Convert to integer bounds
	xStart := int(math.Floor(x1))
	yStart := int(math.Floor(y1))
	xEnd := int(math.Ceil(x2))
	yEnd := int(math.Ceil(y2))

	// Clip to row range
	if yStart < ssYStart {
		yStart = ssYStart
	}
	if yEnd > ssYEnd {
		yEnd = ssYEnd
	}
	if xStart < 0 {
		xStart = 0
	}
	if xEnd > supersampledWidth {
		xEnd = supersampledWidth
	}

	// Fill rectangle in row buffer
	for y := yStart; y < yEnd; y++ {
		rowIndex := (y - ssYStart) * supersampledWidth
		for x := xStart; x < xEnd; x++ {
			if rowIndex+x < len(rowBuffer) {
				rowBuffer[rowIndex+x] = color
			}
		}
	}
}

// downsampleRowPairToFinal downsamples 2 supersampled rows into 1 final row using box filter
func (r *PNGRenderer) downsampleRowPairToFinal(finalY int, rowBuffer []color.RGBA, supersampledWidth int) {
	for x := 0; x < r.finalSize; x++ {
		// Sample 2x2 block from row buffer
		x0 := x * defaultSupersamplingFactor
		x1 := x0 + 1

		// Row 0 (first supersampled row)
		idx00 := x0
		idx01 := x1

		// Row 1 (second supersampled row)
		idx10 := supersampledWidth + x0
		idx11 := supersampledWidth + x1

		// Sum RGBA values from 2x2 block
		var rSum, gSum, bSum, aSum uint32

		if idx00 < len(rowBuffer) {
			c := rowBuffer[idx00]
			rSum += uint32(c.R)
			gSum += uint32(c.G)
			bSum += uint32(c.B)
			aSum += uint32(c.A)
		}
		if idx01 < len(rowBuffer) {
			c := rowBuffer[idx01]
			rSum += uint32(c.R)
			gSum += uint32(c.G)
			bSum += uint32(c.B)
			aSum += uint32(c.A)
		}
		if idx10 < len(rowBuffer) {
			c := rowBuffer[idx10]
			rSum += uint32(c.R)
			gSum += uint32(c.G)
			bSum += uint32(c.B)
			aSum += uint32(c.A)
		}
		if idx11 < len(rowBuffer) {
			c := rowBuffer[idx11]
			rSum += uint32(c.R)
			gSum += uint32(c.G)
			bSum += uint32(c.B)
			aSum += uint32(c.A)
		}

		// Average by dividing by 4
		// #nosec G115 -- Safe: sum of 4 uint8 values (max 255*4=1020) divided by 4 always fits in uint8
		avgColor := color.RGBA{
			R: uint8(rSum / 4),
			G: uint8(gSum / 4),
			B: uint8(bSum / 4),
			A: uint8(aSum / 4),
		}

		// Set pixel in final image
		r.finalImg.Set(x, finalY, avgColor)
	}
}

// Helper functions (reused from original implementation)

func (r *PNGRenderer) parseColor(colorStr string, opacity float64) color.RGBA {
	if colorStr != "" && colorStr[0] != '#' {
		colorStr = "#" + colorStr
	}

	rgba, err := engine.ParseHexColorForRenderer(colorStr, opacity)
	if err != nil {
		return color.RGBA{0, 0, 0, uint8(opacity * 255)}
	}

	return rgba
}

func (r *PNGRenderer) isRectangle(points []engine.Point) bool {
	if len(points) != 4 {
		return false
	}

	uniqueX := make(map[float64]struct{})
	uniqueY := make(map[float64]struct{})

	for _, p := range points {
		uniqueX[p.X] = struct{}{}
		uniqueY[p.Y] = struct{}{}
	}

	return len(uniqueX) == 2 && len(uniqueY) == 2
}

func (r *PNGRenderer) getBoundsFloat(points []engine.Point) (float64, float64, float64, float64) {
	if len(points) == 0 {
		return 0, 0, 0, 0
	}

	minX, maxX := points[0].X, points[0].X
	minY, maxY := points[0].Y, points[0].Y

	for _, p := range points[1:] {
		if p.X < minX {
			minX = p.X
		}
		if p.X > maxX {
			maxX = p.X
		}
		if p.Y < minY {
			minY = p.Y
		}
		if p.Y > maxY {
			maxY = p.Y
		}
	}

	return minX, minY, maxX, maxY
}

func (r *PNGRenderer) scaleImage(src *image.RGBA, newSize int) image.Image {
	oldSize := r.finalSize
	if oldSize == newSize {
		return src
	}

	scaled := image.NewRGBA(image.Rect(0, 0, newSize, newSize))
	ratio := float64(oldSize) / float64(newSize)

	for y := 0; y < newSize; y++ {
		for x := 0; x < newSize; x++ {
			srcX := int(float64(x) * ratio)
			srcY := int(float64(y) * ratio)
			scaled.Set(x, y, src.At(srcX, srcY))
		}
	}

	return scaled
}
