package data

import (
	"encoding/binary"
	"fmt"
	"image"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"

	"code.ur.gs/lupine/ordoor/internal/data/rle"
	"code.ur.gs/lupine/ordoor/internal/palettes"
)

type SpriteHeader struct {
	XOffset   uint16
	YOffset   uint16
	Width     uint16
	Height    uint16
	Padding1  uint32 // I don't think this is used. Could be wrong.
	PixelSize uint32
	Unknown1  [4]byte // ??? Only observed in `WarHammer.ani` so far
	Padding2  uint32  // I don't think this is used either. Could be wrong.
}

func (s SpriteHeader) Check(expectedSize uint32) error {
	if s.Padding1 != 0 || s.Padding2 != 0 {
		return fmt.Errorf("Sprite header padding contains unknown values: %d %d", s.Padding1, s.Padding2)
	}

	// TODO: WarHammer.ani sets Unknown1 to this for all 188,286 sprites. I am
	// very interested in seeing if there are any others
	if s.Unknown1[0]|s.Unknown1[1]|s.Unknown1[2]|s.Unknown1[3] > 0 {
		if s.Unknown1[0] != 212 || s.Unknown1[1] != 113 || s.Unknown1[2] != 59 || s.Unknown1[3] != 1 {
			log.Printf("Value of Unknown1 field: %v", s.Unknown1)
		}
	}

	// Remove 24 bytes from passed-in size to account for the header
	if s.PixelSize != expectedSize-24 {
		return fmt.Errorf("Advertised pixel size: %d differs from expected: %v", s.PixelSize, expectedSize-24)
	}

	return nil
}

type Sprite struct {
	SpriteHeader

	Data []byte
}

func (s *Sprite) ToImage() *image.Paletted {
	return &image.Paletted{
		Pix:     s.Data,
		Stride:  int(s.Width),
		Rect:    image.Rect(0, 0, int(s.Width), int(s.Height)),
		Palette: palettes.DefaultPalette(),
	}
}

// dirEntry totals 8 bytes on disk
type dirEntry struct {
	Offset uint32 // Offset of the sprite relative to the data block
	Size   uint32 // Size of the sprite in bytes, including any header
}

func (d dirEntry) Check() error {
	if d.Size < 24 {
		return fmt.Errorf("Unexpected sprite size: %d (expected >= 24)", d.Size)
	}

	return nil
}

// ObjectHeader totals 24 bytes on disk
type ObjectHeader struct {
	NumSprites uint32 // How many sprites does this object have?
	DirOffset  uint32 // Offset of the directory block
	DirSize    uint32 // Size of the directory block. 8 * NumSprites
	DataOffset uint32 // Offset of the sprite data block
	DataSize   uint32 // Size of the sprite data block
}

func (h ObjectHeader) ExpectedDirSize() uint32 {
	return h.NumSprites * 8
}

func (h ObjectHeader) Check() error {
	// TODO: check for overlaps

	if h.ExpectedDirSize() != h.DirSize {
		return fmt.Errorf("Unexpected sprite directory size: %d (expected %d)", h.DirSize, h.ExpectedDirSize())
	}

	return nil
}

type Object struct {
	ObjectHeader

	Filename string
	Name     string // left blank for use by you
	Sprites  []*Sprite
}

func LoadObjectLazily(filename string) (*Object, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}

	defer f.Close()

	out := &Object{Filename: filename}
	if err := binary.Read(f, binary.LittleEndian, &out.ObjectHeader); err != nil {
		return nil, fmt.Errorf("Reading object header: %v", err)
	}

	if err := out.ObjectHeader.Check(); err != nil {
		return nil, err
	}

	out.Sprites = make([]*Sprite, int(out.NumSprites))

	return out, nil
}

func LoadObject(filename string) (*Object, error) {
	obj, err := LoadObjectLazily(filename)
	if err != nil {
		return nil, err
	}

	if err := obj.LoadAllSprites(); err != nil {
		return nil, err
	}

	return obj, nil
}

func (o *Object) LoadAllSprites() error {
	for i := 0; i < int(o.NumSprites); i++ {
		if err := o.LoadSprite(i); err != nil {
			return err
		}
	}

	return nil
}

func (o *Object) LoadSprite(idx int) error {
	if idx < 0 || idx >= int(o.NumSprites) {
		return fmt.Errorf("Asked for idx %v of %v", idx, o.NumSprites)
	}

	f, err := os.Open(o.Filename)
	if err != nil {
		return err
	}
	defer f.Close()

	var entry dirEntry

	if _, err := f.Seek(int64(o.DirOffset)+int64(idx*8), io.SeekStart); err != nil {
		return fmt.Errorf("Seeking to sprite directory entry %v: %v", idx, err)
	}

	if err := binary.Read(f, binary.LittleEndian, &entry); err != nil {
		return fmt.Errorf("Reading sprite directory entry %v: %v", idx, err)
	}

	if err := entry.Check(); err != nil {
		return err
	}

	if _, err := f.Seek(int64(o.DataOffset+entry.Offset), io.SeekStart); err != nil {
		return fmt.Errorf("Seeking to sprite %v: %v", idx, err)
	}

	sprite := &Sprite{}

	if err := binary.Read(f, binary.LittleEndian, &sprite.SpriteHeader); err != nil {
		return fmt.Errorf("Reading sprite %v header: %v", idx, err)
	}

	if err := sprite.Check(entry.Size); err != nil {
		return err
	}

	buf := io.LimitReader(f, int64(sprite.PixelSize))
	sprite.Data = make([]byte, int(sprite.Height)*int(sprite.Width))

	// The pixel data is RLE-compressed. Uncompress it here.
	if err := rle.Expand(buf, sprite.Data); err != nil {
		return err
	}

	o.Sprites[idx] = sprite
	return nil
}

func LoadObjects(dir string) (map[string]*Object, error) {
	fis, err := ioutil.ReadDir(dir)
	if err != nil {
		return nil, err
	}

	out := make(map[string]*Object, len(fis))

	for _, fi := range fis {
		filename := filepath.Join(dir, fi.Name())
		basename := filepath.Base(filename)
		extname := filepath.Ext(filename)

		// Don't try to load non-.obj files
		if !strings.EqualFold(extname, ".obj") {
			continue
		}

		obj, err := LoadObject(filename)
		if err != nil {
			return nil, fmt.Errorf("%s: %v", filename, err)
		}

		out[basename] = obj
	}

	return out, nil
}
