package main

import (
	"log"
	"math"
	"math/rand"
	"sync"
	"time"

	"github.com/hajimehoshi/ebiten/v2"
)

const (
	TypeVampire = iota
	TypeBat
	TypeGhost
	TypeSoul
	TypeTorch
)

type gameCreep struct {
	x, y float64

	sprites []*ebiten.Image

	frame     int
	frames    int
	lastFrame time.Time

	creepType int

	moveX, moveY float64

	tick       int
	nextAction int

	level  *Level
	player *gamePlayer

	health int

	angle float64

	sync.Mutex
}

func newCreep(creepType int, l *Level, p *gamePlayer) *gameCreep {
	sprites := []*ebiten.Image{
		imageAtlas[ImageVampire1],
		imageAtlas[ImageVampire2],
		imageAtlas[ImageVampire3],
		imageAtlas[ImageVampire2],
	}
	startingHealth := 1
	if creepType == TypeBat {
		sprites = []*ebiten.Image{
			batSS.Frame1,
			batSS.Frame2,
			batSS.Frame3,
			batSS.Frame4,
			batSS.Frame5,
			batSS.Frame6,
			batSS.Frame7,
		}
		startingHealth = 2
	} else if creepType == TypeGhost {
		sprites = []*ebiten.Image{
			imageAtlas[ImageGhost1],
		}
		startingHealth = 1
	} else if creepType == TypeSoul {
		sprites = []*ebiten.Image{
			ojasDungeonSS.Soul1,
		}
	} else if creepType == TypeTorch {
		sprites = []*ebiten.Image{
			sandstoneSS.TorchTop1,
			sandstoneSS.TorchTop2,
			sandstoneSS.TorchTop3,
			sandstoneSS.TorchTop4,
			sandstoneSS.TorchTop5,
			sandstoneSS.TorchTop6,
			sandstoneSS.TorchTop7,
			sandstoneSS.TorchTop8,
		}
	}

	startingFrame := 0
	if len(sprites) > 1 {
		startingFrame = rand.Intn(len(sprites))
	}

	var x, y float64
	if creepType != TypeTorch {
		x, y = l.newSpawnLocation()
	}

	return &gameCreep{
		creepType: creepType,
		x:         x,
		y:         y,
		sprites:   sprites,
		frames:    len(sprites),
		frame:     startingFrame,
		level:     l,
		player:    p,
		health:    startingHealth,
	}
}

func (c *gameCreep) queueNextAction() {
	c.tick = 0
	if c.creepType == TypeBat {
		c.nextAction = 288 + rand.Intn(288)
		return
	}
	c.nextAction = 288 + rand.Intn(432)
}

func (c *gameCreep) runAway() {
	c.queueNextAction()

	randMovementA := ((rand.Float64() - 0.5) * c.moveSpeed()) / 8
	randMovementB := ((rand.Float64() - 0.5) * c.moveSpeed()) / 8

	c.moveX = c.x - c.player.x
	if c.moveX < 0 {
		c.moveX = math.Abs(randMovementA) * -1
	} else {
		c.moveX = math.Abs(randMovementA)
	}
	c.moveY = c.y - c.player.y
	if c.moveY < 0 {
		c.moveY = math.Abs(randMovementB) * -1
	} else {
		c.moveY = math.Abs(randMovementB)
	}
}

func (c *gameCreep) moveSpeed() float64 {
	if c.creepType == TypeSoul {
		return 0.5 / 4
	}
	return 0.29 + (float64(c.level.num) * 0.01)
}

func (c *gameCreep) seekPlayer() {
	maxSpeed := c.moveSpeed() / 9
	minSpeed := c.moveSpeed() / 5 / 9

	if c.creepType == TypeSoul {
		maxSpeed *= 5
	}

	a := angle(c.x, c.y, c.player.x, c.player.y)
	c.moveX = -math.Cos(a)
	c.moveY = -math.Sin(a)
	for {
		if (c.moveX < -minSpeed || c.moveX > minSpeed) || (c.moveY < -minSpeed || c.moveY > minSpeed) {
			break
		}

		c.moveX *= 1.1
		c.moveY *= 1.1
	}
	for {
		if c.moveX >= -maxSpeed && c.moveX <= maxSpeed && c.moveY >= -maxSpeed && c.moveY <= maxSpeed {
			break
		}

		c.moveX *= 0.9
		c.moveY *= 0.9
	}

	c.tick = 0
	c.nextAction = 1440
}

func (c *gameCreep) doNextAction() {
	c.queueNextAction()

	randMovementA := ((rand.Float64() - 0.5) * c.moveSpeed()) / 12
	randMovementB := ((rand.Float64() - 0.5) * c.moveSpeed()) / 12

	if c.creepType == TypeGhost {
		c.angle = angle(c.x, c.y, c.player.x, c.player.y)

		// TODO optimize
		if c.angle > math.Pi/2 || c.angle < -1*math.Pi/2 {
			c.sprites = []*ebiten.Image{
				imageAtlas[ImageGhost1R],
			}
			c.angle = c.angle - math.Pi
		} else {
			c.sprites = []*ebiten.Image{
				imageAtlas[ImageGhost1],
			}
		}
	}

	repelled := c.repelled()
	if !repelled && rand.Intn(13) == 0 && c.creepType != TypeSoul {
		c.seekPlayer()
	} else {
		c.moveX = randMovementA
		c.moveY = randMovementB
	}

	if c.x <= 2 && c.moveX < 0 {
		c.moveX *= 1
	} else if c.x >= float64(c.level.w-3) && c.moveX > 0 {
		c.moveX *= 1
	}
	if c.y <= 2 && c.moveY > 0 {
		c.moveY *= 1
	} else if c.y >= float64(c.level.h-3) && c.moveY < 0 {
		c.moveY *= 1
	}
}

func (c *gameCreep) repelled() bool {
	if c.creepType == TypeSoul {
		return false
	}
	repelled := !c.player.garlicUntil.IsZero() || !c.player.holyWaterUntil.IsZero()
	return repelled
}

// TODO return true when creep is facing player and player is facing creep
func (c *gameCreep) facingPlayer() bool {
	mod := func(v float64) float64 {
		for v > math.Pi {
			v -= math.Pi
		}
		for v < math.Pi*-1 {
			v += math.Pi
		}
		return v
	}
	_ = mod

	ca := math.Remainder(c.angle, math.Pi)
	ca = math.Remainder(ca, -math.Pi)
	if ca < 0 {
		ca = math.Pi + ca
	}
	ca = c.angle
	if ca < 0 {
		ca = math.Pi*2 + ca
	}
	pa := math.Remainder(c.player.angle, math.Pi)
	pa = math.Remainder(pa, -math.Pi)
	if pa < 0 {
		pa = math.Pi + pa
	}
	pa = c.player.angle
	if pa < 0 {
		pa = math.Pi*2 + pa
	}

	a := ca - pa
	if pa > ca {
		a = pa - ca
	}

	if rand.Intn(70) == 0 {
		// TODO
		log.Println(ca, pa, a)
	}

	return a < 2
	//a2 := c.player.angle - c.angle
	// TODO
	//return a > math.Pi/2*-1 && a2 > math.Pi/2*-1 && a < math.Pi/2 && a2 < math.Pi/2
}

func (c *gameCreep) Update() {
	c.Lock()
	defer c.Unlock()

	if c.health == 0 {
		return
	}

	if c.creepType == TypeTorch {
		return
	}

	c.tick++

	repelled := c.repelled()

	if c.creepType == TypeGhost && c.facingPlayer() {
		return
	}

	dx, dy := deltaXY(c.x, c.y, c.player.x, c.player.y)
	seekDistance := 3.5
	if !repelled && dx < seekDistance && dy < seekDistance {
		c.queueNextAction()
		c.seekPlayer()
	} else if c.tick >= c.nextAction {
		c.doNextAction()
		c.tick = 0
	}

	x, y := c.x+c.moveX, c.y+c.moveY
	if c.level.isFloor(x, y) {
		c.x, c.y = x, y
	} else if c.level.isFloor(x, c.y) {
		c.x = x
		c.moveY *= -1
	} else if c.level.isFloor(c.x, y) {
		c.y = y
		c.moveX *= -1
	} else {
		c.nextAction = 0
		return
	}

	if repelled {
		dx, dy := deltaXY(c.x, c.y, c.player.x, c.player.y)
		if dx <= 3 && dy <= 3 {
			c.runAway()
		}
	}

	// Avoid garlic.
	for _, item := range c.level.items {
		if item.health == 0 || item.itemType != itemTypeGarlic {
			continue
		}

		dx, dy := deltaXY(c.x, c.y, item.x, item.y)
		if dx <= 2 && dy <= 2 {
			c.runAway()
		}
	}
}

func (c *gameCreep) Position() (float64, float64) {
	c.Lock()
	defer c.Unlock()
	return c.x, c.y
}

func (c *gameCreep) killScore() int {
	switch c.creepType {
	case TypeVampire:
		return 50
	case TypeBat:
		return 125
	case TypeGhost:
		return 150
	default:
		return 0
	}
}
