package hero

import (
	"image/color"

	"github.com/hajimehoshi/ebiten/v2"

	"github.com/atridad/LilGuy/internal/config"
	"github.com/atridad/LilGuy/internal/projectile"
	"github.com/atridad/LilGuy/internal/world"
)

const (
	defaultRadius       = 24.0
	defaultSpeed        = 200.0
	defaultMaxStamina   = 100.0
	defaultStaminaDrain = 50.0
	defaultStaminaRegen = 30.0

	heroSpriteScale = 0.175
)

// Input and bounds

type Input struct {
	Left   bool
	Right  bool
	Jump   bool
	Sprint bool
}

type Bounds struct {
	Width  float64
	Height float64
	Ground float64
}

type VisualState int

const (
	StateIdle VisualState = iota
	StateSprinting
	StateExhausted
)

type Direction int

const (
	DirLeft Direction = iota
	DirRight
)

// Tags

type Tag int

const (
	TagGravity Tag = iota
)

// Hero state

type Hero struct {
	X      float64
	Y      float64
	Radius float64

	VelocityX float64
	VelocityY float64

	Speed float64

	Color color.NRGBA

	Tags map[Tag]bool

	Stamina      float64
	MaxStamina   float64
	StaminaDrain float64
	StaminaRegen float64

	canSprint     bool
	wasSprintHeld bool
	isSprinting   bool
	isMoving      bool
	isGrounded    bool
	direction     Direction
	animFrame     int
	animTimer     float64
	lastAnimKey   animationKey

	ProjectileConfig projectile.ProjectileConfig

	Brightness float64
}

type Config struct {
	StartX       float64
	StartY       float64
	Radius       float64
	Speed        float64
	Color        color.NRGBA
	MaxStamina   float64
	StaminaDrain float64
	StaminaRegen float64
}

func New(cfg Config) *Hero {
	if cfg.Radius <= 0 {
		cfg.Radius = defaultRadius
	}
	if cfg.Speed <= 0 {
		cfg.Speed = defaultSpeed
	}
	if cfg.Color.A == 0 {
		cfg.Color = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
	}
	if cfg.MaxStamina <= 0 {
		cfg.MaxStamina = defaultMaxStamina
	}
	if cfg.StaminaDrain <= 0 {
		cfg.StaminaDrain = defaultStaminaDrain
	}
	if cfg.StaminaRegen <= 0 {
		cfg.StaminaRegen = defaultStaminaRegen
	}

	return &Hero{
		X:             cfg.StartX,
		Y:             cfg.StartY,
		Radius:        cfg.Radius,
		Speed:         cfg.Speed,
		Color:         cfg.Color,
		Tags:          map[Tag]bool{TagGravity: true},
		Stamina:       cfg.MaxStamina,
		MaxStamina:    cfg.MaxStamina,
		StaminaDrain:  cfg.StaminaDrain,
		StaminaRegen:  cfg.StaminaRegen,
		canSprint:     true,
		wasSprintHeld: false,
		direction:     DirRight,
		animFrame:     0,
		animTimer:     0,
		lastAnimKey:   animationKey{direction: DirRight, state: animIdle},
		ProjectileConfig: projectile.ProjectileConfig{
			Speed:  500.0,
			Radius: 4.0,
			Color:  color.NRGBA{R: 255, G: 0, B: 0, A: 255},
		},
		Brightness: 1.0,
	}
}

func (h *Hero) Update(input Input, dt float64, bounds Bounds, w *world.World) {
	if dt > 0.1 {
		dt = 0.1
	}
	h.updateMovement(input, dt, bounds, w)
	h.updateStamina(input, dt)
	h.updateAnimation(dt)
}

// Movement and physics

func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds, w *world.World) {
	if h.HasTag(TagGravity) {
		h.VelocityY += config.Gravity * dt
	}
	if h.VelocityY > config.MaxFallSpeed {
		h.VelocityY = config.MaxFallSpeed
	}

	newY := h.Y + h.VelocityY*dt

	h.isGrounded = false

	if w != nil && h.VelocityY >= 0 {
		feetX := h.X
		var landedSurface *world.Surface

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

			if feetX >= s.X && feetX <= s.X+s.Width {
				if h.Y <= s.Y && newY >= s.Y {
					if landedSurface == nil || s.Y < landedSurface.Y {
						landedSurface = s
					}
				}
			}
		}

		if landedSurface != nil {
			newY = landedSurface.Y
			h.VelocityY = 0
			h.isGrounded = true
		}
	}

	if !h.isGrounded {
		if newY >= bounds.Ground {
			newY = bounds.Ground
			h.VelocityY = 0
			h.isGrounded = true
		}
	}

	h.Y = newY

	if input.Jump && h.isGrounded {
		h.VelocityY = config.JumpStrength
		h.isGrounded = false
	}

	targetVelocityX := 0.0
	if input.Left {
		targetVelocityX -= h.Speed
		h.direction = DirLeft
	}
	if input.Right {
		targetVelocityX += h.Speed
		h.direction = DirRight
	}

	h.isMoving = targetVelocityX != 0

	h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && h.isMoving
	if h.isSprinting {
		targetVelocityX *= config.SprintSpeedMultiplier
	}

	friction := config.GroundFriction
	if !h.isGrounded {
		friction = config.AirFriction
	}

	frictionFactor := 1.0 - (1.0-friction)*dt*60.0
	if frictionFactor < 0 {
		frictionFactor = 0
	}
	h.VelocityX = h.VelocityX*frictionFactor + targetVelocityX*(1.0-frictionFactor)

	newX := h.X + h.VelocityX*dt

	if newX < h.Radius {
		h.X = h.Radius
		h.VelocityX = 0
	} else if newX > bounds.Width-h.Radius {
		h.X = bounds.Width - h.Radius
		h.VelocityX = 0
	} else {
		h.X = newX
	}
}

func (h *Hero) updateStamina(input Input, dt float64) {
	if !input.Sprint {
		h.wasSprintHeld = false
		if h.Stamina >= h.MaxStamina*config.SprintRecoveryThreshold {
			h.canSprint = true
		}
	}

	if h.isSprinting {
		h.wasSprintHeld = true
		h.Stamina -= h.StaminaDrain * dt
		if h.Stamina <= 0 {
			h.Stamina = 0
			h.canSprint = false
		}
	} else {
		h.Stamina += h.StaminaRegen * dt
		if h.Stamina > h.MaxStamina {
			h.Stamina = h.MaxStamina
		}
	}
}

// Animation

func (h *Hero) updateAnimation(dt float64) {
	isMoving := h.isMoving
	key := animationKey{direction: h.direction, state: animIdle}
	if isMoving {
		key.state = animMove
	}

	// reset animation on state change
	if key != h.lastAnimKey {
		h.animFrame = 0
		h.animTimer = 0
		h.lastAnimKey = key
	}

	// advance animation only when moving
	if isMoving {
		animSpeed := config.NormalAnimSpeed * 0.5
		if h.isSprinting {
			animSpeed = config.SprintAnimSpeed * 0.5
		}
		if !h.isGrounded {
			animSpeed = config.JumpingAnimSpeed * 0.5
		}

		h.animTimer += dt

		// precise frame advancement
		if h.animTimer >= animSpeed {
			frameAdvance := int(h.animTimer / animSpeed)
			h.animTimer -= animSpeed * float64(frameAdvance)
			h.animFrame = (h.animFrame + frameAdvance) % config.AnimFrameWrap
		}
	} else {
		h.animTimer = 0
	}
}

func (h *Hero) getVisualState() VisualState {
	if h.Stamina < h.MaxStamina*config.ExhaustedThreshold {
		return StateExhausted
	}

	if h.isSprinting {
		return StateSprinting
	}

	return StateIdle
}

// Rendering

func (h *Hero) Draw(screen *ebiten.Image) {
	sprite := h.getCurrentSprite()

	if sprite != nil {
		bounds := sprite.Bounds()
		actualHeight := float64(bounds.Dy())
		actualWidth := float64(bounds.Dx())

		op := &ebiten.DrawImageOptions{}

		// center sprite horizontally, align bottom to feet
		op.GeoM.Translate(-actualWidth/2, -actualHeight)
		op.GeoM.Scale(config.HeroSpriteScale, config.HeroSpriteScale)

		// round position to nearest pixel for crisp rendering
		drawX := float64(int(h.X + 0.5))
		drawY := float64(int(h.Y + 0.5))
		op.GeoM.Translate(drawX, drawY)

		// apply visual state coloring
		state := h.getVisualState()
		switch state {
		case StateExhausted:
			op.ColorScale.ScaleWithColor(color.RGBA{R: 255, G: 100, B: 100, A: 255})
		case StateSprinting:
			// no color modification for sprinting
		case StateIdle:
			// no color modification for idle
		}

		if h.Brightness < 1.0 {
			op.ColorScale.Scale(float32(h.Brightness), float32(h.Brightness), float32(h.Brightness), 1.0)
		}

		op.Filter = ebiten.FilterNearest
		screen.DrawImage(sprite, op)
	}
}

func (h *Hero) getCurrentSprite() *ebiten.Image {
	return getKnightSprite(h.direction, h.isMoving, h.animFrame)
}

func (h *Hero) AddTag(tag Tag) {
	if h.Tags == nil {
		h.Tags = make(map[Tag]bool)
	}
	h.Tags[tag] = true
}

func (h *Hero) RemoveTag(tag Tag) {
	if h.Tags == nil {
		return
	}
	delete(h.Tags, tag)
}

func (h *Hero) HasTag(tag Tag) bool {
	if h.Tags == nil {
		return false
	}
	return h.Tags[tag]
}

func (h *Hero) GetDirection() Direction {
	return h.direction
}
