package main

import (
	"log"
	"time"

	"opslag.de/schobers/allg5"
	"opslag.de/schobers/geom"
	"opslag.de/schobers/krampus19/gut"
)

type playLevelState struct {
	ctx *Context

	pack   levelPack
	level  level
	player *entity
	egg    *entity
	bricks entityList
	sunken entityList
	splash map[geom.Point]*splashAnimation

	steps      int
	complete   bool
	onComplete func()

	tick     time.Duration
	ani      gut.Animations
	keysDown keyPressedState
}

func (s *playLevelState) Entities() entityList {
	var entities entityList
	return entities.Add(s.player).Add(s.egg).AddList(s.bricks)
}

func (s *playLevelState) Particles(at geom.Point) []particle {
	var particles []particle
	for pos, ani := range s.splash {
		if pos != at {
			continue
		}
		log.Println("Found particles at", at)
		for _, p := range ani.particles {
			particles = append(particles, p.part)
		}
	}
	return particles
}

func (s *playLevelState) FindSunkenBrick(pos geom.Point) *entity {
	return s.sunken.FindEntity(pos)
}

func (s *playLevelState) IsNextToMagma(pos geom.Point) bool {
	return s.checkTile(pos.Add2D(1, 0), s.isMagma) ||
		s.checkTile(pos.Add2D(-1, 0), s.isMagma) ||
		s.checkTile(pos.Add2D(0, -1), s.isMagma) ||
		s.checkTile(pos.Add2D(0, 1), s.isMagma)
}

func (s *playLevelState) Init(ctx *Context, pack, level string, onComplete func()) {
	s.ctx = ctx
	s.pack = ctx.Levels.ByID(pack)
	s.level = s.pack.levels[level]
	s.bricks = nil
	s.sunken = nil
	s.splash = map[geom.Point]*splashAnimation{}
	for i, e := range s.level.entities {
		switch e {
		case entityTypeBrick:
			s.bricks = append(s.bricks, newEntity(e, s.level.idxToPos(i)))
		case entityTypeCharacter:
			s.player = newEntity(e, s.level.idxToPos(i))
		case entityTypeEgg:
			s.egg = newEntity(e, s.level.idxToPos(i))
		}
	}
	s.keysDown = keyPressedState{}
	s.onComplete = onComplete
}

func (s *playLevelState) Level() level { return s.level }

func (s *playLevelState) PressKey(key allg5.Key) {
	s.keysDown[key] = true
}

func (s *playLevelState) ReleaseKey(key allg5.Key) {
	s.keysDown[key] = false
}

func (s *playLevelState) Steps() int { return s.steps }

func (s *playLevelState) Tick(now time.Duration) {
	s.ani.Animate(now)
}

func (s *playLevelState) TryPlayerMove(key allg5.Key) {
	var dir geom.Point
	switch key {
	case s.ctx.Settings.Controls.MoveUp:
		dir = geom.Pt(0, -1)
	case s.ctx.Settings.Controls.MoveRight:
		dir = geom.Pt(1, 0)
	case s.ctx.Settings.Controls.MoveDown:
		dir = geom.Pt(0, 1)
	case s.ctx.Settings.Controls.MoveLeft:
		dir = geom.Pt(-1, 0)
	default:
		return
	}
	s.tryPlayerMove(dir, key)
}

func (s *playLevelState) tryPlayerMove(dir geom.Point, key allg5.Key) {
	if s.player.scr.pos != s.player.pos.ToF32() {
		return
	}

	to := s.player.pos.Add(dir)
	if !s.canMove(s.player.pos, dir) {
		log.Printf("Move is not allowed (tried out move to %s after key '%s' was pressed)", to, gut.KeyToString(key))
		return
	}

	s.steps++
	log.Printf("Moving player to %s", to)
	s.ani.StartFn(s.ctx.Tick, newMoveAnimation(s.player, to), func() {
		log.Printf("Player movement finished")
		if s.player.pos == s.egg.pos {
			s.complete = true
			if onComplete := s.onComplete; onComplete != nil {
				onComplete()
			}
		} else {
			pressed := s.keysDown.ArePressed(s.ctx.Settings.Controls.MovementKeys()...)
			if len(pressed) == 1 {
				log.Printf("Movement key %s is down, moving further", gut.KeyToString(key))
				s.TryPlayerMove(pressed[0])
			}
		}
	})

	if brick := s.bricks.FindEntity(to); brick != nil {
		log.Printf("Pushing brick at %s", to)
		brickTo := to.Add(dir)
		s.ani.StartFn(s.ctx.Tick, newMoveAnimation(brick, brickTo), func() {
			log.Printf("Brick movement finished")
			if s.checkTile(brickTo, s.wouldBrickSink) {
				log.Printf("Sinking brick at %s", brickTo)
				s.bricks = s.bricks.Remove(brickTo)
				s.sunken = s.sunken.Add(brick)
				s.ani.Start(s.ctx.Tick, newSinkAnimation(brick))

				splash := newSplashAnimation(brickTo)
				s.splash[brickTo] = splash
				s.ani.StartFn(s.ctx.Tick, splash, func() {
					delete(s.splash, brickTo)
				})
			}
		})
	}
}

func (s *playLevelState) canMove(from, dir geom.Point) bool {
	to := from.Add(dir)
	if !s.checkTile(to, s.isSolidTile) {
		return false
	}
	brick := s.bricks.FindEntity(to)
	if brick != nil {
		brickTo := to.Add(dir)
		return !s.checkTileNotFound(brickTo, s.isObstructed, true)
	}
	return true
}

func (s *playLevelState) checkTile(pos geom.Point, check func(pos geom.Point, idx int, t tile) bool) bool {
	return s.checkTileNotFound(pos, check, false)
}

func (s *playLevelState) checkTileNotFound(pos geom.Point, check func(pos geom.Point, idx int, t tile) bool, notFound bool) bool {
	idx := s.level.posToIdx(pos)
	if idx == -1 {
		return notFound
	}
	return check(pos, idx, s.level.tiles[idx])
}

func (s *playLevelState) findEntityAt(pos geom.Point) *entity {
	if s.player.pos == pos {
		return s.player
	}
	brick := s.bricks.FindEntity(pos)
	if brick != nil {
		return brick
	}
	return s.sunken.FindEntity(pos)
}

func (s *playLevelState) isObstructed(pos geom.Point, idx int, t tile) bool {
	if s.bricks.FindEntity(pos) != nil {
		return true // brick
	}
	switch s.level.tiles[idx] {
	case tileMagma:
		return false
	case tileBasic:
		return false
	}
	return true
}

func (s *playLevelState) isMagma(pos geom.Point, idx int, t tile) bool { return t == tileMagma }

func (s *playLevelState) isSolidTile(pos geom.Point, idx int, t tile) bool {
	switch t {
	case tileBasic:
		return true
	case tileMagma:
		return s.sunken.FindEntity(pos) != nil
	}
	return false
}

func (s *playLevelState) wouldBrickSink(pos geom.Point, idx int, t tile) bool {
	return t == tileMagma && s.sunken.FindEntity(pos) == nil
}
