package d2dc6

import (
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
)

const (
	endOfScanLine = 0x80
	maxRunLength  = 0x7f

	terminationSize = 4
	terminatorSize  = 3
)

type scanlineState int

const (
	endOfLine scanlineState = iota
	runOfTransparentPixels
	runOfOpaquePixels
)

// DC6 represents a DC6 file.
type DC6 struct {
	Version            int32
	Flags              uint32
	Encoding           uint32
	Termination        []byte // 4 bytes
	Directions         uint32
	FramesPerDirection uint32
	FramePointers      []uint32    // size is Directions*FramesPerDirection
	Frames             []*DC6Frame // size is Directions*FramesPerDirection
}

// New creates a new, empty DC6
func New() *DC6 {
	result := &DC6{
		Version:            0,
		Flags:              0,
		Encoding:           0,
		Termination:        make([]byte, 4),
		Directions:         0,
		FramesPerDirection: 0,
		FramePointers:      make([]uint32, 0),
		Frames:             make([]*DC6Frame, 0),
	}

	return result
}

// Load loads a dc6 animation
func Load(data []byte) (*DC6, error) {
	d := New()

	err := d.Unmarshal(data)
	if err != nil {
		return nil, err
	}

	return d, nil
}

// Unmarshal converts bite slice into DC6 structure
func (d *DC6) Unmarshal(data []byte) error {
	var err error

	r := d2datautils.CreateStreamReader(data)

	err = d.loadHeader(r)
	if err != nil {
		return err
	}

	frameCount := int(d.Directions * d.FramesPerDirection)

	d.FramePointers = make([]uint32, frameCount)
	for i := 0; i < frameCount; i++ {
		d.FramePointers[i], err = r.ReadUInt32()
		if err != nil {
			return err
		}
	}

	d.Frames = make([]*DC6Frame, frameCount)

	if err := d.loadFrames(r); err != nil {
		return err
	}

	return nil
}

func (d *DC6) loadHeader(r *d2datautils.StreamReader) error {
	var err error

	if d.Version, err = r.ReadInt32(); err != nil {
		return err
	}

	if d.Flags, err = r.ReadUInt32(); err != nil {
		return err
	}

	if d.Encoding, err = r.ReadUInt32(); err != nil {
		return err
	}

	if d.Termination, err = r.ReadBytes(terminationSize); err != nil {
		return err
	}

	if d.Directions, err = r.ReadUInt32(); err != nil {
		return err
	}

	if d.FramesPerDirection, err = r.ReadUInt32(); err != nil {
		return err
	}

	return nil
}

func (d *DC6) loadFrames(r *d2datautils.StreamReader) error {
	var err error

	for i := 0; i < len(d.FramePointers); i++ {
		frame := &DC6Frame{}

		if frame.Flipped, err = r.ReadUInt32(); err != nil {
			return err
		}

		if frame.Width, err = r.ReadUInt32(); err != nil {
			return err
		}

		if frame.Height, err = r.ReadUInt32(); err != nil {
			return err
		}

		if frame.OffsetX, err = r.ReadInt32(); err != nil {
			return err
		}

		if frame.OffsetY, err = r.ReadInt32(); err != nil {
			return err
		}

		if frame.Unknown, err = r.ReadUInt32(); err != nil {
			return err
		}

		if frame.NextBlock, err = r.ReadUInt32(); err != nil {
			return err
		}

		if frame.Length, err = r.ReadUInt32(); err != nil {
			return err
		}

		if frame.FrameData, err = r.ReadBytes(int(frame.Length)); err != nil {
			return err
		}

		if frame.Terminator, err = r.ReadBytes(terminatorSize); err != nil {
			return err
		}

		d.Frames[i] = frame
	}

	return nil
}

// Marshal encodes dc6 animation back into byte slice
func (d *DC6) Marshal() []byte {
	sw := d2datautils.CreateStreamWriter()

	// Encode header
	sw.PushInt32(d.Version)
	sw.PushUint32(d.Flags)
	sw.PushUint32(d.Encoding)

	sw.PushBytes(d.Termination...)

	sw.PushUint32(d.Directions)
	sw.PushUint32(d.FramesPerDirection)

	// load frames
	for _, i := range d.FramePointers {
		sw.PushUint32(i)
	}

	for i := range d.Frames {
		sw.PushUint32(d.Frames[i].Flipped)
		sw.PushUint32(d.Frames[i].Width)
		sw.PushUint32(d.Frames[i].Height)
		sw.PushInt32(d.Frames[i].OffsetX)
		sw.PushInt32(d.Frames[i].OffsetY)
		sw.PushUint32(d.Frames[i].Unknown)
		sw.PushUint32(d.Frames[i].NextBlock)
		sw.PushUint32(d.Frames[i].Length)
		sw.PushBytes(d.Frames[i].FrameData...)
		sw.PushBytes(d.Frames[i].Terminator...)
	}

	return sw.GetBytes()
}

// DecodeFrame decodes the given frame to an indexed color texture
func (d *DC6) DecodeFrame(frameIndex int) []byte {
	frame := d.Frames[frameIndex]

	indexData := make([]byte, frame.Width*frame.Height)
	x := 0
	y := int(frame.Height) - 1
	offset := 0

loop: // this is a label for the loop, so the switch can break the loop (and not the switch)
	for {
		b := int(frame.FrameData[offset])
		offset++

		switch scanlineType(b) {
		case endOfLine:
			if y == 0 {
				break loop
			}

			y--

			x = 0
		case runOfTransparentPixels:
			transparentPixels := b & maxRunLength
			x += transparentPixels
		case runOfOpaquePixels:
			for i := 0; i < b; i++ {
				indexData[x+y*int(frame.Width)+i] = frame.FrameData[offset]
				offset++
			}

			x += b
		}
	}

	return indexData
}

func scanlineType(b int) scanlineState {
	if b == endOfScanLine {
		return endOfLine
	}

	if (b & endOfScanLine) > 0 {
		return runOfTransparentPixels
	}

	return runOfOpaquePixels
}

// Clone creates a copy of the DC6
func (d *DC6) Clone() *DC6 {
	clone := *d
	copy(clone.Termination, d.Termination)
	copy(clone.FramePointers, d.FramePointers)
	clone.Frames = make([]*DC6Frame, len(d.Frames))

	for i := range d.Frames {
		cloneFrame := *d.Frames[i]
		clone.Frames[i] = &cloneFrame
	}

	return &clone
}
