// SPDX-License-Identifier: Unlicense OR MIT

package ops

import (
	"encoding/binary"
	"image"
	"math"

	"gioui.org/f32"
	"gioui.org/internal/byteslice"
	"gioui.org/internal/scene"
)

type Ops struct {
	// version is incremented at each Reset.
	version uint32
	// data contains the serialized operations.
	data []byte
	// refs hold external references for operations.
	refs []interface{}
	// stringRefs provides space for string references, pointers to which will
	// be stored in refs. Storing a string directly in refs would cause a heap
	// allocation, to store the string header in an interface value. The backing
	// array of stringRefs, on the other hand, gets reused between calls to
	// reset, making string references free on average.
	//
	// Appending to stringRefs might reallocate the backing array, which will
	// leave pointers to the old array in refs. This temporarily causes a slight
	// increase in memory usage, but this, too, amortizes away as the capacity
	// of stringRefs approaches its stable maximum.
	stringRefs []string
	// nextStateID is the id allocated for the next
	// StateOp.
	nextStateID uint32
	// multipOp indicates a multi-op such as clip.Path is being added.
	multipOp bool

	macroStack stack
	stacks     [_StackKind]stack
}

type OpType byte

type Shape byte

// Start at a high number for easier debugging.
const firstOpIndex = 200

const (
	TypeMacro OpType = iota + firstOpIndex
	TypeCall
	TypeDefer
	TypeTransform
	TypePopTransform
	TypePushOpacity
	TypePopOpacity
	TypeImage
	TypePaint
	TypeColor
	TypeLinearGradient
	TypePass
	TypePopPass
	TypeInput
	TypeKeyInputHint
	TypeSave
	TypeLoad
	TypeAux
	TypeClip
	TypePopClip
	TypeCursor
	TypePath
	TypeStroke
	TypeSemanticLabel
	TypeSemanticDesc
	TypeSemanticClass
	TypeSemanticSelected
	TypeSemanticEnabled
	TypeActionInput
)

type StackID struct {
	id   uint32
	prev uint32
}

// StateOp represents a saved operation snapshot to be restored
// later.
type StateOp struct {
	id      uint32
	macroID uint32
	ops     *Ops
}

// stack tracks the integer identities of stack operations to ensure correct
// pairing of their push and pop methods.
type stack struct {
	currentID uint32
	nextID    uint32
}

type StackKind uint8

// ClipOp is the shadow of clip.Op.
type ClipOp struct {
	Bounds  image.Rectangle
	Outline bool
	Shape   Shape
}

const (
	ClipStack StackKind = iota
	TransStack
	PassStack
	OpacityStack
	_StackKind
)

const (
	Path Shape = iota
	Ellipse
	Rect
)

const (
	TypeMacroLen            = 1 + 4 + 4
	TypeCallLen             = 1 + 4 + 4 + 4 + 4
	TypeDeferLen            = 1
	TypeTransformLen        = 1 + 1 + 4*6
	TypePopTransformLen     = 1
	TypePushOpacityLen      = 1 + 4
	TypePopOpacityLen       = 1
	TypeRedrawLen           = 1 + 8
	TypeImageLen            = 1 + 1
	TypePaintLen            = 1
	TypeColorLen            = 1 + 4
	TypeLinearGradientLen   = 1 + 8*2 + 4*2
	TypePassLen             = 1
	TypePopPassLen          = 1
	TypeInputLen            = 1
	TypeKeyInputHintLen     = 1 + 1
	TypeSaveLen             = 1 + 4
	TypeLoadLen             = 1 + 4
	TypeAuxLen              = 1
	TypeClipLen             = 1 + 4*4 + 1 + 1
	TypePopClipLen          = 1
	TypeCursorLen           = 2
	TypePathLen             = 8 + 1
	TypeStrokeLen           = 1 + 4
	TypeSemanticLabelLen    = 1
	TypeSemanticDescLen     = 1
	TypeSemanticClassLen    = 2
	TypeSemanticSelectedLen = 2
	TypeSemanticEnabledLen  = 2
	TypeActionInputLen      = 1 + 1
)

func (op *ClipOp) Decode(data []byte) {
	if len(data) < TypeClipLen || OpType(data[0]) != TypeClip {
		panic("invalid op")
	}
	data = data[:TypeClipLen]
	bo := binary.LittleEndian
	op.Bounds.Min.X = int(int32(bo.Uint32(data[1:])))
	op.Bounds.Min.Y = int(int32(bo.Uint32(data[5:])))
	op.Bounds.Max.X = int(int32(bo.Uint32(data[9:])))
	op.Bounds.Max.Y = int(int32(bo.Uint32(data[13:])))
	op.Outline = data[17] == 1
	op.Shape = Shape(data[18])
}

func Reset(o *Ops) {
	o.macroStack = stack{}
	o.stacks = [_StackKind]stack{}
	// Leave references to the GC.
	for i := range o.refs {
		o.refs[i] = nil
	}
	for i := range o.stringRefs {
		o.stringRefs[i] = ""
	}
	o.data = o.data[:0]
	o.refs = o.refs[:0]
	o.stringRefs = o.stringRefs[:0]
	o.nextStateID = 0
	o.version++
}

func Write(o *Ops, n int) []byte {
	if o.multipOp {
		panic("cannot mix multi ops with single ones")
	}
	o.data = append(o.data, make([]byte, n)...)
	return o.data[len(o.data)-n:]
}

func BeginMulti(o *Ops) {
	if o.multipOp {
		panic("cannot interleave multi ops")
	}
	o.multipOp = true
}

func EndMulti(o *Ops) {
	if !o.multipOp {
		panic("cannot end non multi ops")
	}
	o.multipOp = false
}

func WriteMulti(o *Ops, n int) []byte {
	if !o.multipOp {
		panic("cannot use multi ops in single ops")
	}
	o.data = append(o.data, make([]byte, n)...)
	return o.data[len(o.data)-n:]
}

func PushMacro(o *Ops) StackID {
	return o.macroStack.push()
}

func PopMacro(o *Ops, id StackID) {
	o.macroStack.pop(id)
}

func FillMacro(o *Ops, startPC PC) {
	pc := PCFor(o)
	// Fill out the macro definition reserved in Record.
	data := o.data[startPC.data:]
	data = data[:TypeMacroLen]
	data[0] = byte(TypeMacro)
	bo := binary.LittleEndian
	bo.PutUint32(data[1:], uint32(pc.data))
	bo.PutUint32(data[5:], uint32(pc.refs))
}

func AddCall(o *Ops, callOps *Ops, pc PC, end PC) {
	data := Write1(o, TypeCallLen, callOps)
	data[0] = byte(TypeCall)
	bo := binary.LittleEndian
	bo.PutUint32(data[1:], uint32(pc.data))
	bo.PutUint32(data[5:], uint32(pc.refs))
	bo.PutUint32(data[9:], uint32(end.data))
	bo.PutUint32(data[13:], uint32(end.refs))
}

func PushOp(o *Ops, kind StackKind) (StackID, uint32) {
	return o.stacks[kind].push(), o.macroStack.currentID
}

func PopOp(o *Ops, kind StackKind, sid StackID, macroID uint32) {
	if o.macroStack.currentID != macroID {
		panic("stack push and pop must not cross macro boundary")
	}
	o.stacks[kind].pop(sid)
}

func Write1(o *Ops, n int, ref1 interface{}) []byte {
	o.data = append(o.data, make([]byte, n)...)
	o.refs = append(o.refs, ref1)
	return o.data[len(o.data)-n:]
}

func Write1String(o *Ops, n int, ref1 string) []byte {
	o.data = append(o.data, make([]byte, n)...)
	o.stringRefs = append(o.stringRefs, ref1)
	o.refs = append(o.refs, &o.stringRefs[len(o.stringRefs)-1])
	return o.data[len(o.data)-n:]
}

func Write2(o *Ops, n int, ref1, ref2 interface{}) []byte {
	o.data = append(o.data, make([]byte, n)...)
	o.refs = append(o.refs, ref1, ref2)
	return o.data[len(o.data)-n:]
}

func Write2String(o *Ops, n int, ref1 interface{}, ref2 string) []byte {
	o.data = append(o.data, make([]byte, n)...)
	o.stringRefs = append(o.stringRefs, ref2)
	o.refs = append(o.refs, ref1, &o.stringRefs[len(o.stringRefs)-1])
	return o.data[len(o.data)-n:]
}

func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte {
	o.data = append(o.data, make([]byte, n)...)
	o.refs = append(o.refs, ref1, ref2, ref3)
	return o.data[len(o.data)-n:]
}

func PCFor(o *Ops) PC {
	return PC{data: uint32(len(o.data)), refs: uint32(len(o.refs))}
}

func (s *stack) push() StackID {
	s.nextID++
	sid := StackID{
		id:   s.nextID,
		prev: s.currentID,
	}
	s.currentID = s.nextID
	return sid
}

func (s *stack) check(sid StackID) {
	if s.currentID != sid.id {
		panic("unbalanced operation")
	}
}

func (s *stack) pop(sid StackID) {
	s.check(sid)
	s.currentID = sid.prev
}

// Save the effective transformation.
func Save(o *Ops) StateOp {
	o.nextStateID++
	s := StateOp{
		ops:     o,
		id:      o.nextStateID,
		macroID: o.macroStack.currentID,
	}
	bo := binary.LittleEndian
	data := Write(o, TypeSaveLen)
	data[0] = byte(TypeSave)
	bo.PutUint32(data[1:], uint32(s.id))
	return s
}

// Load a previously saved operations state given
// its ID.
func (s StateOp) Load() {
	bo := binary.LittleEndian
	data := Write(s.ops, TypeLoadLen)
	data[0] = byte(TypeLoad)
	bo.PutUint32(data[1:], uint32(s.id))
}

func DecodeCommand(d []byte) scene.Command {
	var cmd scene.Command
	copy(byteslice.Uint32(cmd[:]), d)
	return cmd
}

func EncodeCommand(out []byte, cmd scene.Command) {
	copy(out, byteslice.Uint32(cmd[:]))
}

func DecodeTransform(data []byte) (t f32.Affine2D, push bool) {
	if OpType(data[0]) != TypeTransform {
		panic("invalid op")
	}
	push = data[1] != 0
	data = data[2:]
	data = data[:4*6]

	bo := binary.LittleEndian
	a := math.Float32frombits(bo.Uint32(data))
	b := math.Float32frombits(bo.Uint32(data[4*1:]))
	c := math.Float32frombits(bo.Uint32(data[4*2:]))
	d := math.Float32frombits(bo.Uint32(data[4*3:]))
	e := math.Float32frombits(bo.Uint32(data[4*4:]))
	f := math.Float32frombits(bo.Uint32(data[4*5:]))
	return f32.NewAffine2D(a, b, c, d, e, f), push
}

func DecodeOpacity(data []byte) float32 {
	if OpType(data[0]) != TypePushOpacity {
		panic("invalid op")
	}
	bo := binary.LittleEndian
	return math.Float32frombits(bo.Uint32(data[1:]))
}

// DecodeSave decodes the state id of a save op.
func DecodeSave(data []byte) int {
	if OpType(data[0]) != TypeSave {
		panic("invalid op")
	}
	bo := binary.LittleEndian
	return int(bo.Uint32(data[1:]))
}

// DecodeLoad decodes the state id of a load op.
func DecodeLoad(data []byte) int {
	if OpType(data[0]) != TypeLoad {
		panic("invalid op")
	}
	bo := binary.LittleEndian
	return int(bo.Uint32(data[1:]))
}

type opProp struct {
	Size    byte
	NumRefs byte
}

var opProps = [0x100]opProp{
	TypeMacro:            {Size: TypeMacroLen, NumRefs: 0},
	TypeCall:             {Size: TypeCallLen, NumRefs: 1},
	TypeDefer:            {Size: TypeDeferLen, NumRefs: 0},
	TypeTransform:        {Size: TypeTransformLen, NumRefs: 0},
	TypePopTransform:     {Size: TypePopTransformLen, NumRefs: 0},
	TypePushOpacity:      {Size: TypePushOpacityLen, NumRefs: 0},
	TypePopOpacity:       {Size: TypePopOpacityLen, NumRefs: 0},
	TypeImage:            {Size: TypeImageLen, NumRefs: 2},
	TypePaint:            {Size: TypePaintLen, NumRefs: 0},
	TypeColor:            {Size: TypeColorLen, NumRefs: 0},
	TypeLinearGradient:   {Size: TypeLinearGradientLen, NumRefs: 0},
	TypePass:             {Size: TypePassLen, NumRefs: 0},
	TypePopPass:          {Size: TypePopPassLen, NumRefs: 0},
	TypeInput:            {Size: TypeInputLen, NumRefs: 1},
	TypeKeyInputHint:     {Size: TypeKeyInputHintLen, NumRefs: 1},
	TypeSave:             {Size: TypeSaveLen, NumRefs: 0},
	TypeLoad:             {Size: TypeLoadLen, NumRefs: 0},
	TypeAux:              {Size: TypeAuxLen, NumRefs: 0},
	TypeClip:             {Size: TypeClipLen, NumRefs: 0},
	TypePopClip:          {Size: TypePopClipLen, NumRefs: 0},
	TypeCursor:           {Size: TypeCursorLen, NumRefs: 0},
	TypePath:             {Size: TypePathLen, NumRefs: 0},
	TypeStroke:           {Size: TypeStrokeLen, NumRefs: 0},
	TypeSemanticLabel:    {Size: TypeSemanticLabelLen, NumRefs: 1},
	TypeSemanticDesc:     {Size: TypeSemanticDescLen, NumRefs: 1},
	TypeSemanticClass:    {Size: TypeSemanticClassLen, NumRefs: 0},
	TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0},
	TypeSemanticEnabled:  {Size: TypeSemanticEnabledLen, NumRefs: 0},
	TypeActionInput:      {Size: TypeActionInputLen, NumRefs: 0},
}

func (t OpType) props() (size, numRefs uint32) {
	v := opProps[t]
	return uint32(v.Size), uint32(v.NumRefs)
}

func (t OpType) Size() uint32 {
	return uint32(opProps[t].Size)
}

func (t OpType) NumRefs() uint32 {
	return uint32(opProps[t].NumRefs)
}

func (t OpType) String() string {
	switch t {
	case TypeMacro:
		return "Macro"
	case TypeCall:
		return "Call"
	case TypeDefer:
		return "Defer"
	case TypeTransform:
		return "Transform"
	case TypePopTransform:
		return "PopTransform"
	case TypePushOpacity:
		return "PushOpacity"
	case TypePopOpacity:
		return "PopOpacity"
	case TypeImage:
		return "Image"
	case TypePaint:
		return "Paint"
	case TypeColor:
		return "Color"
	case TypeLinearGradient:
		return "LinearGradient"
	case TypePass:
		return "Pass"
	case TypePopPass:
		return "PopPass"
	case TypeInput:
		return "Input"
	case TypeKeyInputHint:
		return "KeyInputHint"
	case TypeSave:
		return "Save"
	case TypeLoad:
		return "Load"
	case TypeAux:
		return "Aux"
	case TypeClip:
		return "Clip"
	case TypePopClip:
		return "PopClip"
	case TypeCursor:
		return "Cursor"
	case TypePath:
		return "Path"
	case TypeStroke:
		return "Stroke"
	case TypeSemanticLabel:
		return "SemanticDescription"
	default:
		panic("unknown OpType")
	}
}
