package assets

import (
	_ "image/png"
	"log"
	"openquell/config"
	"openquell/util"
	"strings"

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

var Project = LoadLDTK("levels")
var Tiles = InitTiles()

// Tile:  contains  image, identifier  (as  used  in level  data)  and
// additional properties
type Tile struct {
	Id          byte
	Sprite      *ebiten.Image
	Class       string
	Solid       bool            // wall brick
	Player      bool            // player sphere
	IsPrimary   bool            // primary player sphere
	Renderable  bool            // visible, has sprite
	Velocity    bool            // movable
	Collectible bool            // collectible, vanishes once collected
	Transient   bool            // turns into brick wall when traversed
	Destroyable bool            // turns into empty floor when bumped into twice
	Particle    int             // -1=unused, 0-3 = show image of slice
	Tiles       []*ebiten.Image // has N sprites
	TileNames   []string        // same thing, only the names
	Obstacle    bool            // is an obstacle/enemy
	Direction   int             // obstacle business end shows into this direction
}

func (tile *Tile) Clone() *Tile {
	newtile := &Tile{
		Id:          tile.Id,
		Sprite:      tile.Sprite,
		Class:       tile.Class,
		Solid:       tile.Solid,
		Player:      tile.Player,
		IsPrimary:   tile.IsPrimary,
		Renderable:  tile.Renderable,
		Velocity:    tile.Velocity,
		Collectible: tile.Collectible,
		Transient:   tile.Transient,
		Destroyable: tile.Destroyable,
		Particle:    tile.Particle,
		Tiles:       tile.Tiles,
		TileNames:   tile.TileNames,
		Obstacle:    tile.Obstacle,
		Direction:   tile.Direction,
	}

	return newtile
}

const (
	Primary   bool = true
	Secondary bool = false
)

func NewTilePlayer(isprimary bool) *Tile {
	tile := &Tile{
		Id:         'S',
		Class:      "sphere",
		Renderable: true,
		Player:     true,
		Velocity:   true,
		IsPrimary:  isprimary,
	}

	switch isprimary {
	case Primary:
		tile.Sprite = Assets["sphere-blue"]
	case Secondary:
		tile.Sprite = Assets["sphere-blue-secondary"]
		tile.Id = 's'
	}

	// primary sprite is always the first one
	tile.Tiles = []*ebiten.Image{Assets["sphere-blue"], Assets["sphere-blue-secondary"]}
	return tile
}

func NewTileBlock(class string) *Tile {
	return &Tile{
		Id:         '#',
		Sprite:     Assets[class],
		Class:      class,
		Solid:      true,
		Renderable: true,
	}
}

func NewTileCollectible(class string) *Tile {
	return &Tile{
		Id:          'o',
		Sprite:      Assets[class],
		Class:       class,
		Solid:       false,
		Renderable:  true,
		Collectible: true,
	}
}

func NewTileObstacle(class string, direction int) *Tile {
	return &Tile{
		Id:         '+',
		Sprite:     Assets[class],
		Class:      class,
		Solid:      false,
		Renderable: true,
		Obstacle:   true,
		Direction:  direction,
		Velocity:   true,
	}
}

func NewTileParticle(class []string) *Tile {
	sprites := []*ebiten.Image{}

	for _, sprite := range class {
		sprites = append(sprites, Assets[sprite])
	}

	return &Tile{
		Id:         '*',
		Class:      "particle",
		Solid:      false,
		Renderable: false,
		Particle:   0,
		Tiles:      sprites,
	}
}

func NewTileTranswall(class []string) *Tile {
	sprites := []*ebiten.Image{}
	names := []string{}

	for _, sprite := range class {
		sprites = append(sprites, Assets[sprite])
		names = append(names, sprite)
	}

	return &Tile{
		Id:         't',
		Class:      "transwall",
		Solid:      false,
		Renderable: true,
		Transient:  true,
		Tiles:      sprites,
		Sprite:     sprites[0], // initially use the first
		TileNames:  names,
	}
}

func NewTileHiddenDoor(class []string) *Tile {
	sprites := []*ebiten.Image{}
	names := []string{}

	for _, sprite := range class {
		sprites = append(sprites, Assets[sprite])
		names = append(names, sprite)
	}

	return &Tile{
		Id:          'W',
		Class:       "hiddendoor",
		Solid:       false,
		Renderable:  true,
		Destroyable: true,
		Tiles:       sprites,
		Sprite:      sprites[0], // initially use the first
		TileNames:   names,
	}
}

// used to map level data bytes to actual tiles
type TileRegistry map[string]*Tile

func InitTiles() TileRegistry {
	return TileRegistry{
		"floor":           {Id: ' ', Class: "floor", Renderable: false},
		"default":         NewTileBlock("block-greycolored"),
		"solidorange":     NewTileBlock("block-orange-32"),
		"PlayerPrimary":   NewTilePlayer(Primary),
		"PlayerSecondary": NewTilePlayer(Secondary),
		"Collectible":     NewTileCollectible("collectible-orange"),
		"ObstacleStar":    NewTileObstacle("obstacle-star", config.All),
		"ObstacleNorth":   NewTileObstacle("obstacle-north", config.North),
		"ObstacleSouth":   NewTileObstacle("obstacle-south", config.South),
		"ObstacleWest":    NewTileObstacle("obstacle-west", config.West),
		"ObstacleEast":    NewTileObstacle("obstacle-east", config.East),
		"Particle": NewTileParticle([]string{
			//"particle-ring-1",
			"particle-ring-2",
			"particle-ring-3",
			"particle-ring-4",
			"particle-ring-5",
			"particle-ring-6",
		}),
		"Transient":  NewTileTranswall([]string{"transwall", "block-orange-32"}),
		"HiddenDoor": NewTileHiddenDoor([]string{"block-greycolored", "block-greycolored-damaged"}),
	}
}

// load LDTK project at compile time into ram
func LoadLDTK(dir string) *ldtkgo.Project {
	fd, err := assetfs.Open("levels/openquell.ldtk")
	if err != nil {
		log.Fatalf("failed to open LDTK file levels/openquell.ldtk: %s", err)
	}
	defer fd.Close()

	fileinfo, err := fd.Stat()
	if err != nil {
		log.Fatalf("failed to stat() LDTK file levels/openquell.ldtk: %s", err)
	}

	filesize := fileinfo.Size()
	buffer := make([]byte, filesize)

	_, err = fd.Read(buffer)
	if err != nil {
		log.Fatalf("failed to read bytes from LDTK file levels/openquell.ldtk: %s", err)
	}

	project, err := ldtkgo.Read(buffer)
	if err != nil {
		panic(err)
	}

	// do some sanity checks
	properties := []string{"minmoves", "background", "level", "description"}
	need := len(properties)

	for idx, level := range project.Levels {
		have := 0

		for _, property := range level.Properties {
			if util.Contains(properties, property.Identifier) {
				have++
			}
		}

		if have != need {
			log.Fatalf("level definition for level %d (%s) invalid: %d missing properties\n   required: %s",
				idx, level.Identifier, need-have, strings.Join(properties, ", "),
			)
		}
	}

	return project
}
