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, 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
	Animation            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
	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
	AnimateOnDestruct    bool         // wether to animate destruction
	AnimationTrigger     string       // dynamically configured via LDTP
	AnimationSpriteSheet AnimationSet // which sprites to use (refers to an entry in assets.Animations[name])
}

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,
		Animation:            tile.Animation,
		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,
		AnimateOnDestruct:    tile.AnimateOnDestruct,
		AnimationSpriteSheet: tile.AnimationSpriteSheet,
		AnimationTrigger:     tile.AnimationTrigger,
	}

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

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

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

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

func NewTileCollectible() *Tile {
	return &Tile{
		Solid:       false,
		Renderable:  true,
		Collectible: true,
	}
}

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

func NewTileAnimation(class []string) *Tile {
	sprites := GetSprites(class)

	return &Tile{
		Solid:      false,
		Renderable: false,
		Animation:  0,
		Tiles:      sprites,
	}
}

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),
		"Animation": NewTileAnimation([]string{
			"collectible-detonating1",
			"collectible-detonating2",
			"collectible-detonating3",
			"collectible-detonating4",
			"collectible-detonating5",
			"collectible-detonating6",
			"collectible-detonating7",
			"collectible-detonating8",
			"collectible-detonating9",
			"collectible-detonating10",
			"collectible-detonating11",
			"collectible-detonating12",
			"collectible-detonating13",
			"collectible-detonating14",
			"collectible-detonating15",
		}),
		"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", "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
}
