package grid

import (
	"image"
	"log"
	"openquell/assets"
	"openquell/components"
	"openquell/config"
	"openquell/observers"

	"github.com/mlange-42/arche/ecs"
	"github.com/mlange-42/arche/generic"
)

type Grid struct {
	World          *ecs.World
	Width          int
	Height         int
	Size           int
	Tilesize       int
	TilesX, TilesY int
	Map            map[image.Point]*assets.Tile
}

// FIXME: put component addition into extra callable function, to be called
// by game.go in a level-loop. Also remove components of previous level
func NewGrid(world *ecs.World,
	tilesize, width, height int, mapslice map[image.Point]*assets.Tile) *Grid {

	// we use this to turn  our tiles into iterable entities, used for
	// collision detection, transformation and other things
	playermapper := generic.NewMap4[
		components.Position,
		components.Velocity,
		components.Renderable,
		components.Player](world)

	solidmapper := generic.NewMap4[
		components.Position,
		components.Renderable,
		components.Tilish,
		components.Solid](world)

	emptymapper := generic.NewMap2[components.Position, components.Tilish](world)

	colmapper := generic.NewMap3[
		components.Position,
		components.Renderable,
		components.Collectible](world)

	obsmapper := generic.NewMap4[
		components.Position,
		components.Velocity,
		components.Renderable,
		components.Obstacle](world)

	transmapper := generic.NewMap3[
		components.Position,
		components.Renderable,
		components.Transient](world)

	destructiblemapper := generic.NewMap3[
		components.Position,
		components.Renderable,
		components.Destroyable](world)

	switchmapper := generic.NewMap4[
		components.Position,
		components.Renderable,
		components.Bond,
		components.Switch](world)

	doormapper := generic.NewMap4[
		components.Position,
		components.Renderable,
		components.Bond,
		components.Door](world)

	var pos *components.Position
	var vel *components.Velocity
	var render *components.Renderable
	var transient *components.Transient
	var player *components.Player
	var destroyable *components.Destroyable
	var switcher *components.Switch
	var door *components.Door
	var bond *components.Bond

	playerID := ecs.ComponentID[components.Player](world)
	obstacleID := ecs.ComponentID[components.Obstacle](world)

	observer := observers.GetGameObserver(world)

	switches := []ecs.Entity{}
	doors := []ecs.Entity{}

	for point, tile := range mapslice {
		switch tile.Renderable {
		case true:
			switch {
			case tile.Solid:
				entity := solidmapper.New()
				pos, render, _, _ = solidmapper.Get(entity)

			case tile.Player:
				entity := playermapper.New()
				pos, vel, render, player = playermapper.Get(entity)
				observer.AddEntity(entity, playerID)
				vel.Speed = config.PLAYERSPEED
				player.IsPrimary = tile.IsPrimary
				player.Sprites = tile.Tiles
				player.LoopPos = &components.Position{Cellsize: tilesize}

			case tile.Collectible:
				entity := colmapper.New()
				pos, render, _ = colmapper.Get(entity)

			case tile.Obstacle:
				entity := obsmapper.New()
				pos, vel, render, _ = obsmapper.Get(entity)
				vel.Direction = tile.Direction
				vel.PointingAt = tile.Direction
				vel.Speed = config.PLAYERSPEED
				observer.AddEntity(entity, obstacleID)

			case tile.Transient:
				entity := transmapper.New()
				pos, render, transient = transmapper.Get(entity)
				transient.Wall = tile.ToggleSprite

			case tile.Destroyable:
				entity := destructiblemapper.New()
				pos, render, destroyable = destructiblemapper.Get(entity)
				destroyable.Sprites = tile.Tiles
				render.DamageImage = tile.Alpha
				render.Shader = tile.Shader
				render.Damaged = 0

			case tile.Switch:
				entity := switchmapper.New()
				pos, render, _, switcher = switchmapper.Get(entity)
				switcher.CloseSprite = tile.Sprite
				switcher.OpenSprite = tile.ToggleSprite
				switcher.Ref = tile.Ref
				switches = append(switches, entity)

			case tile.Door:
				entity := doormapper.New()
				pos, render, _, door = doormapper.Get(entity)
				door.CloseSprite = tile.Sprite
				door.OpenSprite = tile.ToggleSprite
				door.Id = tile.Id
				doors = append(doors, entity)

			default:
				log.Fatalln("unsupported tile type encountered")
			}

			render.Image = tile.Sprite
			render.Pos = pos

			if tile.Animation.OnCollision {
				render.CollisionAnimate.Sprites = tile.Animation.CollisionSheet.Sprites
				render.CollisionAnimate.Width = tile.Animation.CollisionSheet.Width
				render.CollisionAnimate.Height = tile.Animation.CollisionSheet.Height
			}

			if tile.Animation.OnDestruction {
				render.DestructionAnimate.Sprites = tile.Animation.DestructionSheet.Sprites
				render.DestructionAnimate.Width = tile.Animation.DestructionSheet.Width
				render.DestructionAnimate.Height = tile.Animation.DestructionSheet.Height
			}

			if tile.Animation.OnIdle {
				render.IdleAnimate.Sprites = tile.Animation.IdleSheet.Sprites
				render.IdleAnimate.Width = tile.Animation.IdleSheet.Width
				render.IdleAnimate.Height = tile.Animation.IdleSheet.Height
				render.StartAnimation(components.Idle)
				render.Hidden = true // we do NOT render the sprite, but the idle animation instead
			}

		default:
			// empty  cell, this is  where the player[s]  move. No
			// sprite required since  every level has a background
			// image.
			entity := emptymapper.New()
			pos, _ = emptymapper.Get(entity)
		}

		// however, every tile has a position
		pos.Update(point.X*tilesize, point.Y*tilesize, tilesize)
	}

	// check for switch->door references
	for _, switchentity := range switches {
		_, _, bond, switcher = switchmapper.Get(switchentity)

		if switcher.Ref != "" {
			// this switch has a reference
			for _, doorentity := range doors {
				_, _, _, door = doormapper.Get(doorentity)
				if door.Id == switcher.Ref {
					// the switch reference matches the door id, relate the two
					bond.Ref = switcher.Ref
					relID := ecs.ComponentID[components.Bond](world)
					world.Relations().Set(switchentity, relID, doorentity)
				}
			}
		}
	}

	return &Grid{
		Size:     len(mapslice),
		Tilesize: tilesize,
		Width:    width,
		Height:   height,
		Map:      mapslice,
		World:    world,
		TilesX:   width / tilesize,
		TilesY:   height / tilesize,
	}
}

// return the tile the moving object would end up on if indeed moving
func (grid *Grid) GetTile(
	position *components.Position,
	velocity *components.Velocity) *assets.Tile {

	newpoint := image.Point{
		int(position.X+velocity.Data.X) / grid.Tilesize,
		int(position.Y+velocity.Data.Y) / grid.Tilesize,
	}

	tile := grid.Map[newpoint]
	return tile
}

func (grid *Grid) RemoveTile(point image.Point) {
	delete(grid.Map, point)
}

func (grid *Grid) SetFloorTile(point image.Point) {
	grid.Map[point] = assets.Tiles["floor"]
}

func (grid *Grid) SetSolidTile(tile *assets.Tile, point image.Point) {
	grid.Map[point] = tile
}
