package hero

import (
	"image/color"

	"github.com/hajimehoshi/ebiten/v2"

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

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

	sprintSpeedMultiplier   = 1.8
	sprintRecoveryThreshold = 0.2

	normalAnimSpeed = 0.15
	idleAnimSpeed   = 0.3
	sprintAnimSpeed = 0.08

	exhaustedThreshold = 0.2

	animFrameWrap     = 4096
	heroSpriteScale   = 0.175
	fixedSpriteHeight = 329.0
	fixedSpriteWidth  = 315.0

	gravity        = 1200.0
	jumpStrength   = -450.0
	maxFallSpeed   = 800.0
	groundFriction = 0.85
	airFriction    = 0.95
)

// 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
)

// Hero state

type Hero struct {
	X      float64
	Y      float64
	Radius float64

	VelocityX float64
	VelocityY float64

	Speed float64

	Color color.NRGBA

	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
}

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,
		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},
		},
	}
}

func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
	h.updateMovement(input, dt, bounds)
	h.updateStamina(input, dt)
	h.updateAnimation(dt)
}

// Movement and physics

func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
	h.VelocityY += gravity * dt
	if h.VelocityY > maxFallSpeed {
		h.VelocityY = maxFallSpeed
	}

	h.Y += h.VelocityY * dt

	footPosition := h.Y
	if footPosition >= bounds.Ground {
		h.Y = bounds.Ground
		h.VelocityY = 0
		h.isGrounded = true
	} else {
		h.isGrounded = false
	}

	if input.Jump && h.isGrounded {
		h.VelocityY = 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 *= sprintSpeedMultiplier
	}

	friction := groundFriction
	if !h.isGrounded {
		friction = airFriction
	}

	h.VelocityX = h.VelocityX*friction + targetVelocityX*(1-friction)

	h.X += h.VelocityX * dt

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

func (h *Hero) updateStamina(input Input, dt float64) {
	if !input.Sprint {
		h.wasSprintHeld = false
		if h.Stamina >= h.MaxStamina*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
	}

	if key != h.lastAnimKey {
		h.animFrame = 0
		h.animTimer = 0
		h.lastAnimKey = key
	}

	if isMoving {
		animSpeed := normalAnimSpeed * 0.5
		if h.isSprinting {
			animSpeed = sprintAnimSpeed * 0.5
		}
		h.animTimer += dt
		frameAdvance := int(h.animTimer / animSpeed)
		if frameAdvance > 0 {
			h.animTimer -= animSpeed * float64(frameAdvance)
			h.animFrame = (h.animFrame + frameAdvance) % animFrameWrap
		}
	}
}

func (h *Hero) getVisualState() VisualState {
	if h.Stamina < h.MaxStamina*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{}
		op.GeoM.Translate(-actualWidth/2, -actualHeight)
		op.GeoM.Scale(heroSpriteScale, heroSpriteScale)
		op.GeoM.Translate(h.X, h.Y)

		state := h.getVisualState()
		switch state {
		case StateExhausted:
			op.ColorScale.ScaleWithColor(color.RGBA{R: 255, G: 100, B: 100, A: 255})
		case StateSprinting:
		case StateIdle:
		}

		screen.DrawImage(sprite, op)
	}
}

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

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