package assets

import (
	"bytes"
	"embed"
	"encoding/json"
	"image"
	_ "image/png"
	"io/fs"
	"log"
	"log/slog"
	"path"
	"strings"
	"time"

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

// Maps image name to image data
type AssetRegistry map[string]*ebiten.Image

// A helper to pass the registry easier around
type assetData struct {
	Registry AssetRegistry
}

// to parse asesprite animation json
type AnimationGeo struct {
	X      int `json:"x"`
	Y      int `json:"y"`
	Width  int `json:"w"`
	Height int `json:"h"`
}

type AnimationFrame struct {
	Position AnimationGeo `json:"frame"`
	Duration int          `json:"duration"`
}

type AnimationMeta struct {
	Name string       `json:"image"`
	Geo  AnimationGeo `json:"size"`
}

// Needed to thaw sprite set written by asesprite
type AnimationJSON struct {
	Meta   AnimationMeta    `json:"Meta"`
	Frames []AnimationFrame `json:"frames"`
}

// used to store Animation data
type AnimationSprite struct {
	Sprite   *ebiten.Image
	Duration time.Duration
}

type AnimationSet struct {
	Width, Height int
	Sprites       []AnimationSprite
	File          string
}

// names in the registry match the  sprite set png file name, the JSON
// file names are irrelevant as they  point to the matching file using
// the "image" field.
type AnimationRegistry map[string]AnimationSet

//go:embed sprites/*.png fonts/*.ttf levels/*.ldtk shaders/*.kg sprites/*.json
var assetfs embed.FS

// Called at build time, creates the global asset and animation registries
var Assets, Animations = LoadImages()

// load pngs and json files
func LoadImages() (AssetRegistry, AnimationRegistry) {
	dir := "sprites"
	imagedata := &assetData{}
	imagedata.Registry = AssetRegistry{}
	rawanimations := []AnimationJSON{}

	// we use embed.FS to iterate over all files in ./assets/
	entries, err := assetfs.ReadDir(dir)
	if err != nil {
		log.Fatalf("failed to read assets dir %s: %s", dir, err)
	}

	for _, imagefile := range entries {
		path := path.Join(dir, imagefile.Name())

		fd, err := assetfs.Open(path)
		if err != nil {
			log.Fatalf("failed to open file %s: %s", imagefile.Name(), err)
		}
		defer fd.Close()

		switch {
		case strings.HasSuffix(path, ".png"):
			name, image := ReadImage(imagefile, fd)
			imagedata.Registry[name] = image

		case strings.HasSuffix(path, ".json"):
			animationjson := ReadJson(imagefile, fd)
			rawanimations = append(rawanimations, animationjson)
		}

		slog.Debug("loaded asset", "path", path)
	}

	// preprocess animation sprites
	animations := ProcessAnimations(rawanimations, imagedata)

	return imagedata.Registry, animations
}

func ReadImage(imagefile fs.DirEntry, fd fs.File) (string, *ebiten.Image) {
	name := strings.TrimSuffix(imagefile.Name(), ".png")

	img, _, err := image.Decode(fd)
	if err != nil {
		log.Fatalf("failed to decode image %s: %s", imagefile.Name(), err)
	}

	image := ebiten.NewImageFromImage(img)

	return name, image
}

func ReadJson(imagefile fs.DirEntry, fd fs.File) AnimationJSON {
	buf := new(bytes.Buffer)
	buf.ReadFrom(fd)

	animationjson := AnimationJSON{}

	err := json.Unmarshal(buf.Bytes(), &animationjson)
	if err != nil {
		log.Fatalf("failed to parse JSON: %s", err)
	}

	return animationjson
}

// turn a  raw JSON from  Asesprite into  something we can  use better
// internally we  also load all the  sprites of an animation  by using
// image.SubImage() on the spriteset matching the animation name.
// so, if the animation JSON is called "player-idle.json", then we
// expect to receive the spriteset in a file "player-idle.png", which
// has of course already been loaded at this stage. These spritesets
// must contain a row of sprites. We get the measurements from the JSON.
func ProcessAnimations(rawanimations []AnimationJSON, imagedata *assetData) AnimationRegistry {
	animations := AnimationRegistry{}

	for _, animation := range rawanimations {
		animationset := AnimationSet{}

		animationset.File = strings.TrimSuffix(animation.Meta.Name, ".png")

		for _, frame := range animation.Frames {
			sprite := imagedata.Registry[animationset.File].SubImage(
				image.Rect(
					frame.Position.X,
					frame.Position.Y,
					frame.Position.X+frame.Position.Width,
					frame.Position.Y+frame.Position.Height,
				)).(*ebiten.Image)

			animationset.Sprites = append(animationset.Sprites,
				AnimationSprite{Sprite: sprite, Duration: time.Duration(frame.Duration)})
		}

		animationset.Width = animationset.Sprites[0].Sprite.Bounds().Dx()
		animationset.Height = animationset.Sprites[0].Sprite.Bounds().Dy()

		animations[animationset.File] = animationset
	}

	return animations
}
