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()

type TileAnimation struct {
	OnCollision   bool // wether to animate a collision
	OnDestruction bool // wether to animate destruction
	OnIdle        bool // wether to animate during idling

	CollisionSheet   AnimationSet // an entry in assets.Animations[name]
	DestructionSheet AnimationSet
	IdleSheet        AnimationSet
}

// Tile:  contains  image, identifier  (as  used  in level  data)  and
// additional properties
type Tile struct {
	Id, Ref      string
	Sprite       *ebiten.Image
	ToggleSprite *ebiten.Image
	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
	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
	Shader       *ebiten.Shader
	Alpha        *ebiten.Image
	Bond         bool // denotes an entity which can have a relation to another
	Door         bool // a door, can be manipulated by a switch
	Switch       bool // opens|closes a door
	Animation    TileAnimation
}

func (tile *Tile) Clone() *Tile {
	newtile := &Tile{
		Id:           tile.Id,
		Ref:          tile.Ref,
		Sprite:       tile.Sprite,
		ToggleSprite: tile.ToggleSprite,
		Solid:        tile.Solid,
		Player:       tile.Player,
		IsPrimary:    tile.IsPrimary,
		Renderable:   tile.Renderable,
		Velocity:     tile.Velocity,
		Collectible:  tile.Collectible,
		Transient:    tile.Transient,
		Destroyable:  tile.Destroyable,
		Tiles:        tile.Tiles,
		TileNames:    tile.TileNames,
		Obstacle:     tile.Obstacle,
		Direction:    tile.Direction,
		Alpha:        tile.Alpha,
		Shader:       tile.Shader,
		Bond:         tile.Bond,
		Door:         tile.Door,
		Switch:       tile.Switch,
		Animation:    tile.Animation,
	}

	return newtile
}

const (
	Primary   bool = true
	Secondary bool = false
)

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

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

	return sprites
}

func NewTilePlayer(isprimary bool) *Tile {
	tile := &Tile{
		Renderable: true,
		Player:     true,
		Velocity:   true,
		IsPrimary:  isprimary,
	}

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

func NewTileBlock() *Tile {
	return &Tile{
		Solid:      true,
		Renderable: true,
	}
}

func NewTileCollectible() *Tile {
	return &Tile{
		Solid:       false,
		Renderable:  true,
		Collectible: true,
		Animation: TileAnimation{
			OnDestruction:    true,
			DestructionSheet: Animations["collectible-detonating"],
			OnIdle:           true,
			IdleSheet:        Animations["collectible-idle"],
		},
	}
}

func NewTileObstacle(direction int) *Tile {
	return &Tile{
		Solid:      false,
		Renderable: true,
		Obstacle:   true,
		Direction:  direction,
		Velocity:   true,
	}
}

func NewTileTranswall() *Tile {
	return &Tile{
		Solid:      false,
		Renderable: true,
		Transient:  true,
	}
}

func NewTileHiddenDoor(alpha string) *Tile {
	return &Tile{
		Solid:       false,
		Renderable:  true,
		Destroyable: true,
		Alpha:       Assets[alpha],
		Shader:      Shaders["destruct"],
	}
}

func NewTileSwitch() *Tile {
	return &Tile{
		Solid:      false,
		Renderable: true,
		Bond:       true,
		Switch:     true,
	}
}

func NewTileDoor() *Tile {
	return &Tile{
		Solid:      false,
		Renderable: true,
		Bond:       true,
		Door:       true,
	}
}

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

func InitTiles() TileRegistry {
	return TileRegistry{
		"floor":           {Renderable: false},
		"default":         NewTileBlock(),
		"solidorange":     NewTileBlock(),
		"PlayerPrimary":   NewTilePlayer(Primary),
		"PlayerSecondary": NewTilePlayer(Secondary),
		"Collectible":     NewTileCollectible(),
		"ObstacleStar":    NewTileObstacle(config.All),
		"ObstacleNorth":   NewTileObstacle(config.North),
		"ObstacleSouth":   NewTileObstacle(config.South),
		"ObstacleWest":    NewTileObstacle(config.West),
		"ObstacleEast":    NewTileObstacle(config.East),
		"Transient":       NewTileTranswall(),
		"HiddenDoor":      NewTileHiddenDoor("damage"),
		"HiddenDoor2":     NewTileHiddenDoor("damage"),
		"HiddenDoor3":     NewTileHiddenDoor("damage"),
		"HiddenDoor4":     NewTileHiddenDoor("damage"),
		"HiddenDoor5":     NewTileHiddenDoor("damage"),
		"HiddenDoor6":     NewTileHiddenDoor("damage"),
		"HiddenDoor7":     NewTileHiddenDoor("damage"),
		"HiddenDoor8":     NewTileHiddenDoor("damage"),
		"HiddenDoor9":     NewTileHiddenDoor("damage"),
		"HiddenDoor10":    NewTileHiddenDoor("damage"),
		"HiddenDoor11":    NewTileHiddenDoor("damage"),
		"HiddenDoor12":    NewTileHiddenDoor("damage"),
		"HiddenDoor13":    NewTileHiddenDoor("damage"),
		"Switch":          NewTileSwitch(),
		"Door":            NewTileDoor(),
	}
}

// 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", "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
}
