package raycast

import (
	"image/color"
	"math"
	"sort"

	"github.com/atridad/LilGuy/internal/world"
	"github.com/hajimehoshi/ebiten/v2"
)

// Line represents a line segment
type Line struct {
	X1, Y1, X2, Y2 float64
}

// angle returns the angle of the line
func (l *Line) angle() float64 {
	return math.Atan2(l.Y2-l.Y1, l.X2-l.X1)
}

// LightSource represents a light that casts rays
type LightSource struct {
	X, Y      float64
	Radius    float64
	Color     color.RGBA
	Intensity float64
}

type System struct {
	lights          []*LightSource
	shadowImage     *ebiten.Image
	triangleImage   *ebiten.Image
	screenWidth     int
	screenHeight    int
	enabled         bool
	shadowIntensity float64
}

func NewSystem(screenWidth, screenHeight int) *System {
	shadowImage := ebiten.NewImage(screenWidth, screenHeight)
	triangleImage := ebiten.NewImage(screenWidth, screenHeight)
	triangleImage.Fill(color.White)

	return &System{
		lights:          make([]*LightSource, 0),
		shadowImage:     shadowImage,
		triangleImage:   triangleImage,
		screenWidth:     screenWidth,
		screenHeight:    screenHeight,
		enabled:         true,
		shadowIntensity: 0.7,
	}
}

func (s *System) AddLight(light *LightSource) {
	s.lights = append(s.lights, light)
}

func (s *System) ClearLights() {
	s.lights = s.lights[:0]
}

func (s *System) SetEnabled(enabled bool) {
	s.enabled = enabled
}

func (s *System) IsEnabled() bool {
	return s.enabled
}

func (s *System) SetShadowIntensity(intensity float64) {
	s.shadowIntensity = intensity
}

func worldToLines(w *world.World) []Line {
	var lines []Line

	for _, surface := range w.Surfaces {
		if !surface.IsSolid() {
			continue
		}

		x, y, width, height := surface.Bounds()

		lines = append(lines,
			Line{x, y, x + width, y},
			Line{x + width, y, x + width, y + height},
			Line{x + width, y + height, x, y + height},
			Line{x, y + height, x, y},
		)
	}

	return lines
}

// intersection calculates the intersection of two lines
func intersection(l1, l2 Line) (float64, float64, bool) {
	denom := (l1.X1-l1.X2)*(l2.Y1-l2.Y2) - (l1.Y1-l1.Y2)*(l2.X1-l2.X2)
	tNum := (l1.X1-l2.X1)*(l2.Y1-l2.Y2) - (l1.Y1-l2.Y1)*(l2.X1-l2.X2)
	uNum := -((l1.X1-l1.X2)*(l1.Y1-l2.Y1) - (l1.Y1-l1.Y2)*(l1.X1-l2.X1))

	if denom == 0 {
		return 0, 0, false
	}

	t := tNum / denom
	if t > 1 || t < 0 {
		return 0, 0, false
	}

	u := uNum / denom
	if u > 1 || u < 0 {
		return 0, 0, false
	}

	x := l1.X1 + t*(l1.X2-l1.X1)
	y := l1.Y1 + t*(l1.Y2-l1.Y1)
	return x, y, true
}

// newRay creates a ray from a point at a given angle
func newRay(x, y, length, angle float64) Line {
	return Line{
		X1: x,
		Y1: y,
		X2: x + length*math.Cos(angle),
		Y2: y + length*math.Sin(angle),
	}
}

// getLinePoints extracts unique points from lines
func getLinePoints(lines []Line) [][2]float64 {
	pointMap := make(map[[2]float64]bool)

	for _, line := range lines {
		pointMap[[2]float64{line.X1, line.Y1}] = true
		pointMap[[2]float64{line.X2, line.Y2}] = true
	}

	points := make([][2]float64, 0, len(pointMap))
	for p := range pointMap {
		points = append(points, p)
	}

	return points
}

func castRays(cx, cy float64, lines []Line) []Line {
	const rayLength = 2000.0

	var rays []Line
	points := getLinePoints(lines)

	for _, p := range points {
		l := Line{cx, cy, p[0], p[1]}
		angle := l.angle()

		for _, offset := range []float64{-0.001, 0.001} {
			var intersections [][2]float64
			ray := newRay(cx, cy, rayLength, angle+offset)

			for _, wall := range lines {
				if px, py, ok := intersection(ray, wall); ok {
					intersections = append(intersections, [2]float64{px, py})
				}
			}

			if len(intersections) > 0 {
				minDist := math.Inf(1)
				minIdx := -1

				for i, point := range intersections {
					dist := (cx-point[0])*(cx-point[0]) + (cy-point[1])*(cy-point[1])
					if dist < minDist {
						minDist = dist
						minIdx = i
					}
				}

				if minIdx >= 0 {
					rays = append(rays, Line{cx, cy, intersections[minIdx][0], intersections[minIdx][1]})
				}
			}
		}
	}

	sort.Slice(rays, func(i, j int) bool {
		return rays[i].angle() < rays[j].angle()
	})

	return rays
}

// rayVertices creates vertices for a triangle between rays
func rayVertices(x1, y1, x2, y2, x3, y3 float64) []ebiten.Vertex {
	return []ebiten.Vertex{
		{DstX: float32(x1), DstY: float32(y1), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
		{DstX: float32(x2), DstY: float32(y2), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
		{DstX: float32(x3), DstY: float32(y3), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
	}
}

func (s *System) Draw(screen *ebiten.Image, w *world.World) {
	if !s.enabled || len(s.lights) == 0 {
		return
	}

	lines := worldToLines(w)

	sw := float64(s.screenWidth)
	sh := float64(s.screenHeight)
	lines = append(lines,
		Line{0, 0, sw, 0},
		Line{sw, 0, sw, sh},
		Line{sw, sh, 0, sh},
		Line{0, sh, 0, 0},
	)

	s.shadowImage.Fill(color.RGBA{0, 0, 0, 180})

	for _, light := range s.lights {
		rays := castRays(light.X, light.Y, lines)

		opt := &ebiten.DrawTrianglesOptions{}
		opt.Blend = ebiten.BlendSourceOut

		for i, ray := range rays {
			nextRay := rays[(i+1)%len(rays)]

			v := rayVertices(light.X, light.Y, nextRay.X2, nextRay.Y2, ray.X2, ray.Y2)
			s.shadowImage.DrawTriangles(v, []uint16{0, 1, 2}, s.triangleImage, opt)
		}
	}

	shadowOpt := &ebiten.DrawImageOptions{}
	shadowOpt.ColorScale.ScaleAlpha(float32(s.shadowIntensity))
	screen.DrawImage(s.shadowImage, shadowOpt)
}

func (s *System) DrawDebug(screen *ebiten.Image, w *world.World) {
	if !s.enabled || len(s.lights) == 0 {
		return
	}

	lines := worldToLines(w)

	sw := float64(s.screenWidth)
	sh := float64(s.screenHeight)
	lines = append(lines,
		Line{0, 0, sw, 0},
		Line{sw, 0, sw, sh},
		Line{sw, sh, 0, sh},
		Line{0, sh, 0, 0},
	)

	for _, light := range s.lights {
		rays := castRays(light.X, light.Y, lines)

		for _, ray := range rays {
			drawLine(screen, ray, color.RGBA{255, 255, 0, 150})
		}

		drawCircle(screen, light.X, light.Y, 5, color.RGBA{255, 200, 100, 255})
	}
}

// Helper function to draw a line
func drawLine(screen *ebiten.Image, l Line, c color.RGBA) {
	x1, y1 := int(l.X1), int(l.Y1)
	x2, y2 := int(l.X2), int(l.Y2)

	dx := abs(x2 - x1)
	dy := abs(y2 - y1)
	sx := -1
	if x1 < x2 {
		sx = 1
	}
	sy := -1
	if y1 < y2 {
		sy = 1
	}
	err := dx - dy

	for {
		if x1 >= 0 && x1 < screen.Bounds().Dx() && y1 >= 0 && y1 < screen.Bounds().Dy() {
			screen.Set(x1, y1, c)
		}

		if x1 == x2 && y1 == y2 {
			break
		}

		e2 := 2 * err
		if e2 > -dy {
			err -= dy
			x1 += sx
		}
		if e2 < dx {
			err += dx
			y1 += sy
		}
	}
}

// Helper function to draw a circle
func drawCircle(screen *ebiten.Image, cx, cy, radius float64, c color.RGBA) {
	for angle := 0.0; angle < 2*math.Pi; angle += 0.1 {
		x := int(cx + radius*math.Cos(angle))
		y := int(cy + radius*math.Sin(angle))
		if x >= 0 && x < screen.Bounds().Dx() && y >= 0 && y < screen.Bounds().Dy() {
			screen.Set(x, y, c)
		}
	}
}

func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}

func (s *System) IsPointInShadow(x, y float64, w *world.World) bool {
	if !s.enabled || len(s.lights) == 0 {
		return false
	}

	lines := worldToLines(w)

	for _, light := range s.lights {
		rayToPoint := Line{light.X, light.Y, x, y}
		distanceToPoint := math.Sqrt((x-light.X)*(x-light.X) + (y-light.Y)*(y-light.Y))

		blocked := false
		for _, wall := range lines {
			if ix, iy, ok := intersection(rayToPoint, wall); ok {
				distanceToIntersection := math.Sqrt((ix-light.X)*(ix-light.X) + (iy-light.Y)*(iy-light.Y))

				if distanceToIntersection < distanceToPoint-10.0 {
					blocked = true
					break
				}
			}
		}

		if !blocked {
			return false
		}
	}

	return true
}
