package game

import (
	"fmt"
	"image"
	"image/draw"
	"log"
	"openquell/assets"
	"openquell/components"
	. "openquell/config"
	"strings"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/mlange-42/arche/ecs"
	"github.com/mlange-42/arche/filter"
)

type Level struct {
	Grid                    *Grid
	Cellsize, Width, Height int
	World                   *ecs.World
	Name                    string
	Description             string
	Background              *ebiten.Image
	Mapslice                map[image.Point]*assets.Tile
	UseCache                bool
	Cache                   *ebiten.Image
	Selector                map[string]ecs.Mask
	Component               map[string]ecs.ID
}

func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level {
	cache := ebiten.NewImage(game.ScreenWidth, game.ScreenHeight)

	positionid := ecs.ComponentID[components.Position](game.World)
	velocityid := ecs.ComponentID[components.Velocity](game.World)
	playerid := ecs.ComponentID[components.Player](game.World)
	colid := ecs.ComponentID[components.Collectible](game.World)
	ptid := ecs.ComponentID[components.Particle](game.World)
	sid := ecs.ComponentID[components.Solid](game.World)
	renderid := ecs.ComponentID[components.Renderable](game.World)

	selectors := map[string]ecs.Mask{}
	selectors["player"] = filter.All(positionid, velocityid, playerid)
	selectors["tile"] = filter.All(renderid, positionid, sid)
	selectors["movable"] = filter.All(renderid, positionid)
	selectors["collectible"] = filter.All(positionid, colid)
	selectors["particle"] = filter.All(positionid, ptid)

	components := map[string]ecs.ID{}
	components["position"] = positionid
	components["velocity"] = velocityid
	components["player"] = playerid
	components["collectible"] = colid
	components["particle"] = ptid
	components["solid"] = sid
	components["renderable"] = renderid

	return &Level{
		Mapslice:    LevelToSlice(game, plan, cellsize),
		Cellsize:    cellsize,
		World:       game.World,
		Width:       game.ScreenWidth,
		Height:      game.ScreenHeight,
		Description: plan.Description,
		Background:  plan.Background,
		UseCache:    false,
		Cache:       cache,
		Selector:    selectors,
		Component:   components,
	}
}

func (level *Level) Update() {
	query := level.World.Query(level.Selector["player"])

	toRemove := []ecs.Entity{}
	particle_pos := image.Point{}

	for query.Next() {
		playerposition := (*components.Position)(query.Get(level.Component["position"]))
		velocity := (*components.Velocity)(query.Get(level.Component["velocity"]))

		if !velocity.Moving() {
			switch {
			case ebiten.IsKeyPressed(ebiten.KeyRight):
				velocity.Change(East)
			case ebiten.IsKeyPressed(ebiten.KeyLeft):
				velocity.Change(West)
			case ebiten.IsKeyPressed(ebiten.KeyDown):
				velocity.Change(South)
			case ebiten.IsKeyPressed(ebiten.KeyUp):
				velocity.Change(North)
				// other keys: <tab>: switch player, etc
			}
		}

		if velocity.Moving() {
			ok, newpos := level.Grid.BumpEdge(playerposition, velocity)
			if ok {
				fmt.Printf("falling off the edge, new pos: %v\n", newpos)
				playerposition.Set(newpos)
			} else {
				ok, tilepos := level.Grid.GetSolidNeighborPosition(playerposition, velocity, true)
				if ok {
					intersects, newpos := tilepos.Intersects(playerposition, velocity)
					if intersects {
						fmt.Printf("collision detected. tile: %s\n", tilepos)
						fmt.Printf("                  player: %s\n", playerposition)
						fmt.Printf("                     new: %s\n", newpos)

						playerposition.Set(newpos)
						fmt.Printf("              player new: %s\n", playerposition)
						velocity.Change(Stop)
					}
				}
			}

			colquery := level.World.Query(level.Selector["collectible"])
			for colquery.Next() {
				collectible := (*components.Collectible)(colquery.Get(level.Component["collectible"]))
				colposition := (*components.Position)(colquery.Get(level.Component["position"]))
				ok, _ := playerposition.Intersects(colposition, velocity)
				if ok {
					fmt.Printf("bumped into collectible %v\n", collectible)
					toRemove = append(toRemove, colquery.Entity())
					particle_pos.X = colposition.X
					particle_pos.Y = colposition.Y
				}
			}
		}

		playerposition.Move(velocity)
	}

	// remove collectible if collected
	for _, entity := range toRemove {
		// FIXME: or keep them and prepare an animated death
		level.World.RemoveEntity(entity)
	}

	// display debris after collecting
	ptquery := level.World.Query(level.Selector["particle"])
	for ptquery.Next() {
		// we loop, but it's only one anyway
		particle := (*components.Particle)(ptquery.Get(level.Component["particle"]))
		colposition := (*components.Position)(ptquery.Get(level.Component["position"]))

		if len(toRemove) > 0 {
			// particle appears
			colposition.Update(
				particle_pos.X-(level.Cellsize/2),
				particle_pos.Y-(level.Cellsize/2),
				64,
			)

			particle.Index = 0 // start displaying the particle
		} else {
			switch {
			case particle.Index > -1 && particle.Index < len(particle.Particles)-1:
				particle.Index++
			default:
				// last sprite reached, remove it
				particle.Index = -1
			}
		}
	}

}

func (level *Level) Position2Point(position *components.Position) image.Point {
	return image.Point{
		int(position.X) / level.Cellsize,
		int(position.Y) / level.Cellsize,
	}
}

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

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

	tile := level.Mapslice[newpoint]
	return tile
}

func (level *Level) Draw(screen *ebiten.Image) {
	level.DrawTiles(screen)
	level.DrawMovables(screen)
	level.DrawParticles(screen)
}

func (level *Level) DrawTiles(screen *ebiten.Image) {
	op := &ebiten.DrawImageOptions{}

	if !level.UseCache {
		// map not cached or cacheable, write it to the cache
		draw.Draw(level.Cache, level.Background.Bounds(), level.Background, image.ZP, draw.Src)

		query := level.World.Query(level.Selector["tile"])
		for query.Next() {
			pos := (*components.Position)(query.Get(level.Component["position"]))
			sprite := (*components.Renderable)(query.Get(level.Component["renderable"]))

			draw.Draw(
				level.Cache,
				image.Rect(pos.X, pos.Y, pos.X+pos.Cellsize, pos.Y+pos.Cellsize),
				sprite.Image, image.ZP, draw.Over)
		}

		op.GeoM.Reset()
		screen.DrawImage(level.Cache, op)

		level.UseCache = true
	} else {
		// use the cached map
		op.GeoM.Reset()
		screen.DrawImage(level.Cache, op)
	}
}

func (level *Level) DrawMovables(screen *ebiten.Image) {
	// write the movable tiles
	op := &ebiten.DrawImageOptions{}
	selector := level.Selector["movable"].Without(level.Component["solid"])
	query := level.World.Query(&selector)

	for query.Next() {
		pos := (*components.Position)(query.Get(level.Component["position"]))
		sprite := (*components.Renderable)(query.Get(level.Component["renderable"]))

		op.GeoM.Reset()
		op.GeoM.Translate(float64(pos.X), float64(pos.Y))

		screen.DrawImage(sprite.Image, op)
	}
}

func (level *Level) DrawParticles(screen *ebiten.Image) {
	// write particles (these are no tiles!)
	op := &ebiten.DrawImageOptions{}
	query := level.World.Query(level.Selector["particle"])

	for query.Next() {
		pos := (*components.Position)(query.Get(level.Component["position"]))
		particle := (*components.Particle)(query.Get(level.Component["particle"]))

		if particle.Index > -1 {
			op.GeoM.Reset()
			op.GeoM.Translate(float64(pos.X), float64(pos.Y))
			screen.DrawImage(particle.Particles[particle.Index], op)
		}
	}

}

func (level *Level) SetupGrid(game *Game) {
	// generic variant does not work here:
	// selector := generic.NewFilter1[components.Position]()
	// level.World.Batch().RemoveEntities(selector)
	// missing argument in conversion to generic.Filter1[components.Position]

	// erase all entities of previous level, if any
	posID := ecs.ComponentID[components.Position](level.World)
	selector := ecs.All(posID)
	level.World.Batch().RemoveEntities(selector)

	// setup world
	level.Grid = NewGrid(game, level.Cellsize, level.Mapslice)
}

func LevelToSlice(game *Game, level *assets.RawLevel, tilesize int) map[image.Point]*assets.Tile {
	size := game.ScreenWidth * game.ScreenHeight
	mapslice := make(map[image.Point]*assets.Tile, size)

	for y, line := range strings.Split(string(level.Data), "\n") {
		if len(line) != game.ScreenWidth/tilesize && y < game.ScreenHeight/tilesize {
			log.Fatalf("line %d doesn't contain %d tiles, but %d",
				y, game.ScreenWidth/tilesize, len(line))
		}

		for x, char := range line {
			mapslice[image.Point{x, y}] = assets.Tiles[byte(char)]
		}
	}

	return mapslice
}
