// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package iconvg

import (
	"bytes"
	"errors"
	"image/color"
	"math"
)

// UpgradeToFileFormatVersion1Options are the options to the
// UpgradeToFileFormatVersion1 function.
type UpgradeToFileFormatVersion1Options struct {
	// ArcsExpandWithHighResolutionCoordinates is like the
	// Encoder.HighResolutionCoordinates field. It controls whether to favor
	// file size (false) or precision (true) when replacing File Format Version
	// 0's arcs with cubic Bézier curves.
	ArcsExpandWithHighResolutionCoordinates bool
}

// UpgradeToFileFormatVersion1 upgrades IconVG data from the 2016 experimental
// "File Format Version 0" to the 2021 "File Format Version 1".
//
// This package (golang.org/x/exp/shiny/iconvg) holds a decoder for FFV0,
// including this function to convert from FFV0 to FFV1. Different packages
// (github.com/google/iconvg/src/go/*) decode FFV1.
//
// Amongst some new features and other clean-ups, FFV1 sets up the capability
// for animated vector graphics, therefore removing some FFV0 features (such as
// arc segments) that can be hard to animate smoothly. The IconvG FFV1 format
// and its design decisions are discussed at
// https://github.com/google/iconvg/issues/4#issuecomment-874105547
func UpgradeToFileFormatVersion1(v0 []byte, opts *UpgradeToFileFormatVersion1Options) (v1 []byte, retErr error) {
	u := &upgrader{}
	if opts != nil {
		u.opts = *opts
	}
	for i := range u.creg {
		u.creg[i] = upgradeColor{
			typ:          ColorTypePaletteIndex,
			paletteIndex: uint8(i),
		}
	}

	if !bytes.HasPrefix(v0, magicBytes) {
		return nil, errInvalidMagicIdentifier
	}
	v1 = append(v1, "\x8AIVG"...)
	v0 = v0[4:]

	v1, v0, retErr = u.upgradeMetadata(v1, v0)
	if retErr != nil {
		return nil, retErr
	}

	v1, _, retErr = u.upgradeBytecode(v1, v0)
	if retErr != nil {
		return nil, retErr
	}

	return v1, nil
}

const (
	upgradeVerbMoveTo = 0
	upgradeVerbLineTo = 1
	upgradeVerbQuadTo = 2
	upgradeVerbCubeTo = 3
)

type upgrader struct {
	opts UpgradeToFileFormatVersion1Options

	// These fields hold the current path's geometry.
	verbs []uint8
	args  [][2]float32

	// These fields track most of the FFV0 virtual machine register state. The
	// FFV1 register model is different enough that we don't just translate
	// each FFV0 register-related opcode individually.
	creg [64]upgradeColor
	nreg [64]float32
	csel uint32
	nsel uint32
	fill uint32

	// These fields track the most recent color written to FFV1 register
	// REGS[SEL+7] (and SEL is kept at 56). As a file size optimization, we
	// don't have to emit the first half of "Set REGS[SEL+7] = etc; Use
	// REGS[SEL+7]" if the register already holds the "etc" value.
	regsSel7    color.RGBA
	hasRegsSel7 bool

	// calculatingJumpLOD is whether the upgrader.upgradeBytecode method is
	// being called recursively. FFV0 sets a Level-Of-Detail filter that
	// applies implicitly until the next SetLOD opcode (if any). FFV1 instead
	// explicitly gives the number of opcodes to skip if outside the LOD range.
	calculatingJumpLOD bool
}

func (u *upgrader) upgradeMetadata(v1 buffer, v0 buffer) (newV1 buffer, newV0 buffer, retErr error) {
	nMetadataChunks, n := v0.decodeNatural()
	if n == 0 {
		return nil, nil, errInvalidNumberOfMetadataChunks
	}
	v1.encodeNaturalFFV1(nMetadataChunks)
	v0 = v0[n:]

	for ; nMetadataChunks > 0; nMetadataChunks-- {
		length, n := v0.decodeNatural()
		if n == 0 {
			return nil, nil, errInvalidMetadataChunkLength
		}
		v0 = v0[n:]
		if uint64(length) > uint64(len(v0)) {
			return nil, nil, errInvalidMetadataChunkLength
		}
		upgrade, err := u.upgradeMetadataChunk(v0[:length])
		if err != nil {
			return nil, nil, err
		}
		v1.encodeNaturalFFV1(uint32(len(upgrade)))
		v1 = append(v1, upgrade...)
		v0 = v0[length:]
	}
	return v1, v0, nil
}

func (u *upgrader) upgradeMetadataChunk(v0 buffer) (v1 buffer, retErr error) {
	mid, n := v0.decodeNatural()
	if n == 0 {
		return nil, errInvalidMetadataIdentifier
	}
	switch mid {
	case midViewBox:
		mid = ffv1MIDViewBox
	case midSuggestedPalette:
		mid = ffv1MIDSuggestedPalette
	default:
		return nil, errInvalidMetadataIdentifier
	}
	v1.encodeNaturalFFV1(mid)
	v0 = v0[n:]

	switch mid {
	case ffv1MIDViewBox:
		for i := 0; i < 4; i++ {
			x, n := v0.decodeNatural()
			if n == 0 {
				return nil, errInvalidViewBox
			}
			v1.encodeNaturalFFV1(x)
			v0 = v0[n:]
		}
		if len(v0) != 0 {
			return nil, errInvalidViewBox
		}

	case ffv1MIDSuggestedPalette:
		if len(v0) == 0 {
			return nil, errInvalidSuggestedPalette
		}
		numColors := 1 + int(v0[0]&0x3f)
		colorLength := 1 + int(v0[0]>>6)
		v1 = append(v1, uint8(numColors-1))
		v0 = v0[1:]
		for i := 0; i < numColors; i++ {
			c, n := Color{}, 0
			switch colorLength {
			case 1:
				c, n = v0.decodeColor1()
			case 2:
				c, n = v0.decodeColor2()
			case 3:
				c, n = v0.decodeColor3Direct()
			case 4:
				c, n = v0.decodeColor4()
			}
			if n == 0 {
				return nil, errInvalidSuggestedPalette
			} else if (c.typ == ColorTypeRGBA) && validAlphaPremulColor(c.data) {
				v1 = append(v1, c.data.R, c.data.G, c.data.B, c.data.A)
			} else {
				v1 = append(v1, 0x00, 0x00, 0x00, 0xff)
			}
			v0 = v0[n:]
		}
		if len(v0) != 0 {
			return nil, errInvalidSuggestedPalette
		}
	}
	return v1, nil
}

func (u *upgrader) upgradeBytecode(v1 buffer, v0 buffer) (newV1 buffer, newV0 buffer, retErr error) {
	uf := upgradeFunc(upgradeStyling)
	for len(v0) > 0 {
		uf, v1, v0, retErr = uf(u, v1, v0)
		if retErr != nil {
			if retErr == errCalculatingJumpLOD {
				return v1, v0, nil
			}
			return nil, nil, retErr
		}
	}
	return v1, v0, nil
}

var errCalculatingJumpLOD = errors.New("iconvg: calculating JumpLOD")

type upgradeFunc func(*upgrader, buffer, buffer) (upgradeFunc, buffer, buffer, error)

func upgradeStyling(u *upgrader, v1 buffer, v0 buffer) (uf upgradeFunc, newV1 buffer, newV0 buffer, retErr error) {
	for len(v0) > 0 {
		switch opcode := v0[0]; {
		case opcode < 0x80: // "Set CSEL/NSEL"
			if opcode < 0x40 {
				u.csel = uint32(opcode & 63)
			} else {
				u.nsel = uint32(opcode & 63)
			}
			v0 = v0[1:]

		case opcode < 0xa8: // "Set CREG[etc] to an etc color"
			adj := uint32(opcode & 7)
			if adj == 7 {
				adj = 0
			}
			index := (u.csel - adj) & 63

			v0 = v0[1:]
			c, n := Color{}, 0
			switch (opcode - 0x80) >> 3 {
			case 0:
				c, n = v0.decodeColor1()
			case 1:
				c, n = v0.decodeColor2()
			case 2:
				c, n = v0.decodeColor3Direct()
			case 3:
				c, n = v0.decodeColor4()
			case 4:
				c, n = v0.decodeColor3Indirect()
			}
			if n == 0 {
				return nil, nil, nil, errInvalidColor
			}
			u.creg[index], retErr = u.resolve(c, false)
			if retErr != nil {
				return nil, nil, nil, retErr
			}
			v0 = v0[n:]

			if (opcode & 7) == 7 {
				u.csel = (u.csel + 1) & 63
			}

		case opcode < 0xc0: // "Set NREG[etc] to a real number"
			adj := uint32(opcode & 7)
			if adj == 7 {
				adj = 0
			}
			index := (u.nsel - adj) & 63

			v0 = v0[1:]
			f, n := float32(0), 0
			switch (opcode - 0x80) >> 3 {
			case 5:
				f, n = v0.decodeReal()
			case 6:
				f, n = v0.decodeCoordinate()
			case 7:
				f, n = v0.decodeZeroToOne()
			}
			if n == 0 {
				return nil, nil, nil, errInvalidNumber
			}
			u.nreg[index] = f
			v0 = v0[n:]

			if (opcode & 7) == 7 {
				u.nsel = (u.nsel + 1) & 63
			}

		case opcode < 0xc7: // Start path.
			adj := uint32(opcode & 7)
			u.fill = (u.csel - adj) & 63
			v1 = append(v1, 0x35) // FFV1 MoveTo.
			v0 = v0[1:]
			return upgradeDrawing, v1, v0, nil

		case opcode == 0xc7: // "Set LOD"
			if u.calculatingJumpLOD {
				u.calculatingJumpLOD = false
				return nil, v1, v0, errCalculatingJumpLOD
			}

			v0 = v0[1:]
			lod := [2]float32{}
			for i := range lod {
				f, n := v0.decodeReal()
				if n == 0 {
					return nil, nil, nil, errInvalidNumber
				}
				lod[i] = f
				v0 = v0[n:]
			}
			if (lod[0] == 0) && math.IsInf(float64(lod[1]), +1) {
				break
			}

			u.calculatingJumpLOD = true
			ifTrue := []byte(nil)
			if ifTrue, v0, retErr = u.upgradeBytecode(nil, v0); retErr != nil {
				return nil, nil, nil, retErr
			}
			nInstructions := countFFV1Instructions(ifTrue)
			if nInstructions >= (1 << 30) {
				return nil, nil, nil, errUnsupportedUpgrade
			}
			v1 = append(v1, 0x3a) // FFV1 JumpLOD.
			v1.encodeNaturalFFV1(uint32(nInstructions))
			v1.encodeCoordinateFFV1(lod[0])
			v1.encodeCoordinateFFV1(lod[1])
			v1 = append(v1, ifTrue...)

		default:
			return nil, nil, nil, errUnsupportedStylingOpcode
		}
	}
	return upgradeStyling, v1, v0, nil
}

func upgradeDrawing(u *upgrader, v1 buffer, v0 buffer) (uf upgradeFunc, newV1 buffer, newV0 buffer, retErr error) {
	u.verbs = u.verbs[:0]
	u.args = u.args[:0]

	coords := [3][2]float32{}
	pen := [2]float32{}
	prevSmoothType := smoothTypeNone
	prevSmoothPoint := [2]float32{}

	// Handle the implicit M after a "Start path" styling op.
	v0, retErr = decodeCoordinates(pen[:2], nil, v0)
	if retErr != nil {
		return nil, nil, nil, retErr
	}
	u.verbs = append(u.verbs, upgradeVerbMoveTo)
	u.args = append(u.args, pen)
	startingPoint := pen

	for len(v0) > 0 {
		switch opcode := v0[0]; {
		case opcode < 0xc0: // LineTo, QuadTo, CubeTo.
			nCoordPairs, nReps, relative, smoothType := 0, 1+int(opcode&0x0f), false, smoothTypeNone
			switch opcode >> 4 {
			case 0x00, 0x01: // "L (absolute lineTo)"
				nCoordPairs = 1
				nReps = 1 + int(opcode&0x1f)
			case 0x02, 0x03: // "l (relative lineTo)"
				nCoordPairs = 1
				nReps = 1 + int(opcode&0x1f)
				relative = true
			case 0x04: // "T (absolute smooth quadTo)"
				nCoordPairs = 1
				smoothType = smoothTypeQuad
			case 0x05: // "t (relative smooth quadTo)"
				nCoordPairs = 1
				relative = true
				smoothType = smoothTypeQuad
			case 0x06: // "Q (absolute quadTo)"
				nCoordPairs = 2
			case 0x07: // "q (relative quadTo)"
				nCoordPairs = 2
				relative = true
			case 0x08: // "S (absolute smooth cubeTo)"
				nCoordPairs = 2
				smoothType = smoothTypeCube
			case 0x09: // "s (relative smooth cubeTo)"
				nCoordPairs = 2
				relative = true
				smoothType = smoothTypeCube
			case 0x0a: // "C (absolute cubeTo)"
				nCoordPairs = 3
			case 0x0b: // "c (relative cubeTo)"
				nCoordPairs = 3
				relative = true
			}
			v0 = v0[1:]

			for i := 0; i < nReps; i++ {
				smoothIndex := 0
				if smoothType != smoothTypeNone {
					smoothIndex = 1
					if smoothType != prevSmoothType {
						coords[0][0] = pen[0]
						coords[0][1] = pen[1]
					} else {
						coords[0][0] = (2 * pen[0]) - prevSmoothPoint[0]
						coords[0][1] = (2 * pen[1]) - prevSmoothPoint[1]
					}
				}
				allCoords := coords[:smoothIndex+nCoordPairs]
				explicitCoords := allCoords[smoothIndex:]

				v0, retErr = decodeCoordinatePairs(explicitCoords, nil, v0)
				if retErr != nil {
					return nil, nil, nil, retErr
				}
				if relative {
					for c := range explicitCoords {
						explicitCoords[c][0] += pen[0]
						explicitCoords[c][1] += pen[1]
					}
				}

				u.verbs = append(u.verbs, uint8(len(allCoords)))
				u.args = append(u.args, allCoords...)

				pen = allCoords[len(allCoords)-1]
				if len(allCoords) == 2 {
					prevSmoothPoint = allCoords[0]
					prevSmoothType = smoothTypeQuad
				} else if len(allCoords) == 3 {
					prevSmoothPoint = allCoords[1]
					prevSmoothType = smoothTypeCube
				} else {
					prevSmoothType = smoothTypeNone
				}
			}

		case opcode < 0xe0: // ArcTo.
			v1, v0, retErr = u.upgradeArcs(&pen, v1, v0)
			if retErr != nil {
				return nil, nil, nil, retErr
			}
			prevSmoothType = smoothTypeNone

		default: // Other drawing opcodes.
			v0 = v0[1:]
			switch opcode {
			case 0xe1: // "z (closePath); end path"
				goto endPath

			case 0xe2, 0xe3: // "z (closePath); M (absolute/relative moveTo)"
				v0, retErr = decodeCoordinatePairs(coords[:1], nil, v0)
				if retErr != nil {
					return nil, nil, nil, retErr
				}
				if opcode == 0xe2 {
					pen[0] = coords[0][0]
					pen[1] = coords[0][1]
				} else {
					pen[0] += coords[0][0]
					pen[1] += coords[0][1]
				}
				u.verbs = append(u.verbs, upgradeVerbMoveTo)
				u.args = append(u.args, pen)

			default:
				tmp := [1]float32{}
				v0, retErr = decodeCoordinates(tmp[:1], nil, v0)
				if retErr != nil {
					return nil, nil, nil, retErr
				}
				switch opcode {
				case 0xe6: // "H (absolute horizontal lineTo)"
					pen[0] = tmp[0]
				case 0xe7: // "h (relative horizontal lineTo)"
					pen[0] += tmp[0]
				case 0xe8: // "V (absolute vertical lineTo)"
					pen[1] = tmp[0]
				case 0xe9: // "v (relative vertical lineTo)"
					pen[1] += tmp[0]
				default:
					return nil, nil, nil, errUnsupportedDrawingOpcode
				}
				u.verbs = append(u.verbs, upgradeVerbLineTo)
				u.args = append(u.args, pen)
			}
			prevSmoothType = smoothTypeNone
		}
	}

endPath:
	v1, retErr = u.finishDrawing(v1, startingPoint)
	return upgradeStyling, v1, v0, retErr
}

func (u *upgrader) finishDrawing(v1 buffer, startingPoint [2]float32) (newV1 buffer, retErr error) {
	v1.encodeCoordinatePairFFV1(u.args[0])

	for i, j := 1, 1; i < len(u.verbs); {
		curr := u.args[j-1]
		runLength := u.computeRunLength(u.verbs[i:])
		verb := u.verbs[i]

		if verb == upgradeVerbMoveTo {
			v1 = append(v1, 0x35) // FFV1 MoveTo.
			v1.encodeCoordinatePairFFV1(u.args[j])
			i += 1
			j += 1
			continue
		}

		switch verb {
		case upgradeVerbLineTo:
			if ((runLength == 3) && ((j + 3) == len(u.args)) && u.looksLikeParallelogram3(&curr, u.args[j:], &startingPoint)) ||
				((runLength == 4) && u.looksLikeParallelogram4(&curr, u.args[j:j+4])) {
				v1 = append(v1, 0x34) // FFV1 Parallelogram.
				v1.encodeCoordinatePairFFV1(u.args[j+0])
				v1.encodeCoordinatePairFFV1(u.args[j+1])
				i += 4
				j += 4 * 1
				continue
			}
		case upgradeVerbCubeTo:
			if (runLength == 4) && u.looksLikeEllipse(&curr, u.args[j:j+(4*3)]) {
				v1 = append(v1, 0x33) // FFV1 Ellipse (4 quarters).
				v1.encodeCoordinatePairFFV1(u.args[j+2])
				v1.encodeCoordinatePairFFV1(u.args[j+5])
				i += 4
				j += 4 * 3
				continue
			}
		}

		opcodeBase := 0x10 * (verb - 1) // FFV1 LineTo / QuadTo / CubeTo.
		if runLength < 16 {
			v1 = append(v1, opcodeBase|uint8(runLength))
		} else {
			v1 = append(v1, opcodeBase)
			v1.encodeNaturalFFV1(uint32(runLength) - 16)
		}
		args := u.args[j : j+(runLength*int(verb))]
		for _, arg := range args {
			v1.encodeCoordinatePairFFV1(arg)
		}
		i += runLength
		j += len(args)
	}

	return u.emitFill(v1)
}

func (u *upgrader) emitFill(v1 buffer) (newV1 buffer, retErr error) {
	switch c := u.creg[u.fill]; c.typ {
	case ColorTypeRGBA:
		if validAlphaPremulColor(c.rgba) {
			if !u.hasRegsSel7 || (u.regsSel7 != c.rgba) {
				u.hasRegsSel7, u.regsSel7 = true, c.rgba
				v1 = append(v1, 0x57, // FFV1 Set REGS[SEL+7].hi32.
					c.rgba.R, c.rgba.G, c.rgba.B, c.rgba.A)
			}
			v1 = append(v1, 0x87) // FFV1 Fill (flat color) with REGS[SEL+7].

		} else if (c.rgba.A == 0) && (c.rgba.B&0x80 != 0) {
			nStops := int(c.rgba.R & 63)
			cBase := int(c.rgba.G & 63)
			nBase := int(c.rgba.B & 63)
			if nStops < 2 {
				return nil, errInvalidColor
			} else if nStops > 17 {
				return nil, errUnsupportedUpgrade
			}

			v1 = append(v1, 0x70|uint8(nStops-2)) // FFV1 SEL -= N; Set REGS[SEL+1 .. SEL+1+N].
			for i := 0; i < nStops; i++ {
				if stopOffset := u.nreg[(nBase+i)&63]; stopOffset <= 0 {
					v1 = append(v1, 0x00, 0x00, 0x00, 0x00)
				} else if stopOffset < 1 {
					u := uint32(stopOffset * 0x10000)
					v1 = append(v1, uint8(u>>0), uint8(u>>8), uint8(u>>16), uint8(u>>24))
				} else {
					v1 = append(v1, 0x00, 0x00, 0x01, 0x00)
				}

				if stopColor := u.creg[(cBase+i)&63]; stopColor.typ != ColorTypeRGBA {
					return nil, errUnsupportedUpgrade
				} else {
					v1 = append(v1,
						stopColor.rgba.R,
						stopColor.rgba.G,
						stopColor.rgba.B,
						stopColor.rgba.A,
					)
				}
			}

			nMatrixElements := 0
			if c.rgba.B&0x40 == 0 {
				v1 = append(v1, 0x91, // FFV1 Fill (linear gradient) with REGS[SEL+1 .. SEL+1+N].
					(c.rgba.G&0xc0)|uint8(nStops-2))
				nMatrixElements = 3
			} else {
				v1 = append(v1, 0xa1, // FFV1 Fill (radial gradient) with REGS[SEL+1 .. SEL+1+N].
					(c.rgba.G&0xc0)|uint8(nStops-2))
				nMatrixElements = 6
			}
			for i := 0; i < nMatrixElements; i++ {
				u := math.Float32bits(u.nreg[(nBase+i-6)&63])
				v1 = append(v1, uint8(u>>0), uint8(u>>8), uint8(u>>16), uint8(u>>24))
			}

			v1 = append(v1, 0x36, // FFV1 SEL += N.
				uint8(nStops))
		} else {
			return nil, errInvalidColor
		}

	case ColorTypePaletteIndex:
		if c.paletteIndex < 7 {
			v1 = append(v1, 0x88+c.paletteIndex) // FFV1 Fill (flat color) with REGS[SEL+8+N].
		} else {
			v1 = append(v1, 0x56, // FFV1 Set REGS[SEL+6].hi32.
				0x80|c.paletteIndex, 0, 0, 0,
				0x86) // FFV1 Fill (flat color) with REGS[SEL+6].
		}

	case ColorTypeBlend:
		if c.color0.typ == ColorTypeRGBA {
			v1 = append(v1, 0x53, // FFV1 Set REGS[SEL+3].hi32.
				c.color0.rgba.R, c.color0.rgba.G, c.color0.rgba.B, c.color0.rgba.A)
		}
		if c.color1.typ == ColorTypeRGBA {
			v1 = append(v1, 0x54, // FFV1 Set REGS[SEL+4].hi32.
				c.color1.rgba.R, c.color1.rgba.G, c.color1.rgba.B, c.color1.rgba.A)
		}
		v1 = append(v1, 0x55, // FFV1 Set REGS[SEL+5].hi32.
			c.blend)
		if c.color0.typ == ColorTypeRGBA {
			v1 = append(v1, 0xfe)
		} else {
			v1 = append(v1, 0x80|c.color0.paletteIndex)
		}
		if c.color1.typ == ColorTypeRGBA {
			v1 = append(v1, 0xff)
		} else {
			v1 = append(v1, 0x80|c.color1.paletteIndex)
		}
		v1 = append(v1, 0, 0x85) // FFV1 Fill (flat color) with REGS[SEL+5].
	}

	return v1, nil
}

func (u *upgrader) computeRunLength(verbs []uint8) int {
	firstVerb := verbs[0]
	if firstVerb == 0 {
		return 1
	}
	n := 1
	for ; (n < len(verbs)) && (verbs[n] == firstVerb); n++ {
	}
	return n
}

// looksLikeParallelogram3 is like looksLikeParallelogram4 but the final point
// (implied by the ClosePath op) is separate from the middle 3 args.
func (u *upgrader) looksLikeParallelogram3(curr *[2]float32, args [][2]float32, final *[2]float32) bool {
	if len(args) != 3 {
		panic("unreachable")
	}
	return (*curr == *final) &&
		(curr[0] == (args[0][0] - args[1][0] + args[2][0])) &&
		(curr[1] == (args[0][1] - args[1][1] + args[2][1]))
}

// looksLikeParallelogram4 returns whether the 5 coordinate pairs (A, B, C, D,
// E) form a parallelogram:
//
// E=A           B
//
//	o---------o
//	 \         \
//	  \         \
//	   \         \
//	    o---------o
//	   D           C
//
// Specifically, it checks that (A == E) and ((A - B) == (D - C)). That last
// equation can be rearranged as (A == (B - C + D)).
//
// The motivation is that, if looksLikeParallelogram4 is true, then the 5 input
// coordinate pairs can then be compressed to 3: A, B and C. Or, if the current
// point A is implied by context then 4 input pairs can be compressed to 2.
func (u *upgrader) looksLikeParallelogram4(curr *[2]float32, args [][2]float32) bool {
	if len(args) != 4 {
		panic("unreachable")
	}
	return (*curr == args[3]) &&
		(curr[0] == (args[0][0] - args[1][0] + args[2][0])) &&
		(curr[1] == (args[0][1] - args[1][1] + args[2][1]))
}

// looksLikeEllipse returns whether the 13 coordinate pairs (A, A+, B-, B, B+,
// C- C, C+, D-, D, D+, A-, E) form a cubic Bézier approximation to an ellipse.
// Let A± denote the two tangent vectors (A+ - A) and (A - A-) and likewise for
// B±, C± and D±.
//
//	A+     B-
//
// E=A  o    o   B
// A- o---------o   B+
//
//	o  \         \ o
//	    \    X    \
//	   o \         \  o
//	  D+  o---------o  C-
//	     D   o    o  C
//	        D-     C+
//
// See https://nigeltao.github.io/blog/2021/three-points-define-ellipse.html
// for a better version of that ASCII art.
//
// Specifically, it checks that (A, B, C, D, E), also known as (*curr, args[2],
// args[5], args[8] and args[11]), forms a parallelogram. If so, let X be the
// parallelogram center and define two axis vectors: r = B-X and s = C-X.
//
// These axes define the parallelogram's or ellipse's shape but they are not
// necessarily orthogonal and hence not necessarily the ellipse's major
// (longest) and minor (shortest) axes. If s is a 90 degree rotation of r then
// the parallelogram is a square and the ellipse is a circle.
//
// This function further checks that the A±, B± C± and D± tangents are
// approximately equal to +λ×r, +λ×s, -λ×r and -λ×s, where λ = ((math.Sqrt2 -
// 1) × 4 / 3) comes from the cubic Bézier approximation to a quarter-circle.
//
// The motivation is that, if looksLikeEllipse is true, then the 13 input
// coordinate pairs can then be compressed to 3: A, B and C. Or, if the current
// point A is implied by context then 12 input pairs can be compressed to 2.
func (u *upgrader) looksLikeEllipse(curr *[2]float32, args [][2]float32) bool {
	if len(args) != 12 {
		panic("unreachable")
	}
	if (*curr != args[11]) ||
		(curr[0] != (args[2][0] - args[5][0] + args[8][0])) ||
		(curr[1] != (args[2][1] - args[5][1] + args[8][1])) {
		return false
	}
	center := [2]float32{
		(args[2][0] + args[8][0]) / 2,
		(args[2][1] + args[8][1]) / 2,
	}

	// 0.5522847498307933984022516322796 ≈ ((math.Sqrt2 - 1) × 4 / 3), the
	// tangent lengths (as a fraction of the radius) for a commonly used cubic
	// Bézier approximation to a circle. Multiplying that by 0.98 and 1.02
	// checks that we're within 2% of that fraction.
	//
	// This also covers the slightly different 0.551784777779014 constant,
	// recommended by https://pomax.github.io/bezierinfo/#circles_cubic
	const λMin = 0.98 * 0.5522847498307933984022516322796
	const λMax = 1.02 * 0.5522847498307933984022516322796

	// Check the first axis.
	r := [2]float32{
		args[2][0] - center[0],
		args[2][1] - center[1],
	}
	rMin := [2]float32{r[0] * λMin, r[1] * λMin}
	rMax := [2]float32{r[0] * λMax, r[1] * λMax}
	if rMin[0] > rMax[0] {
		rMin[0], rMax[0] = rMax[0], rMin[0]
	}
	if rMin[1] > rMax[1] {
		rMin[1], rMax[1] = rMax[1], rMin[1]
	}
	if !within(args[0][0]-curr[0], args[0][1]-curr[1], rMin, rMax) ||
		!within(args[4][0]-args[5][0], args[4][1]-args[5][1], rMin, rMax) ||
		!within(args[5][0]-args[6][0], args[5][1]-args[6][1], rMin, rMax) ||
		!within(args[11][0]-args[10][0], args[11][1]-args[10][1], rMin, rMax) {
		return false
	}

	// Check the second axis.
	s := [2]float32{
		args[5][0] - center[0],
		args[5][1] - center[1],
	}
	sMin := [2]float32{s[0] * λMin, s[1] * λMin}
	sMax := [2]float32{s[0] * λMax, s[1] * λMax}
	if sMin[0] > sMax[0] {
		sMin[0], sMax[0] = sMax[0], sMin[0]
	}
	if sMin[1] > sMax[1] {
		sMin[1], sMax[1] = sMax[1], sMin[1]
	}
	if !within(args[2][0]-args[1][0], args[2][1]-args[1][1], sMin, sMax) ||
		!within(args[3][0]-args[2][0], args[3][1]-args[2][1], sMin, sMax) ||
		!within(args[7][0]-args[8][0], args[7][1]-args[8][1], sMin, sMax) ||
		!within(args[8][0]-args[9][0], args[8][1]-args[9][1], sMin, sMax) {
		return false
	}

	return true
}

func within(v0 float32, v1 float32, min [2]float32, max [2]float32) bool {
	return (min[0] <= v0) && (v0 <= max[0]) && (min[1] <= v1) && (v1 <= max[1])
}

func (u *upgrader) upgradeArcs(pen *[2]float32, v1 buffer, v0 buffer) (newV1 buffer, newV0 buffer, retErr error) {
	coords := [6]float32{}
	largeArc, sweep := false, false
	opcode := v0[0]
	v0 = v0[1:]
	nReps := 1 + int(opcode&0x0f)
	for i := 0; i < nReps; i++ {
		v0, retErr = decodeCoordinates(coords[:2], nil, v0)
		if retErr != nil {
			return nil, nil, retErr
		}
		coords[2], v0, retErr = decodeAngle(nil, v0)
		if retErr != nil {
			return nil, nil, retErr
		}
		largeArc, sweep, v0, retErr = decodeArcToFlags(nil, v0)
		if retErr != nil {
			return nil, nil, retErr
		}
		v0, retErr = decodeCoordinates(coords[4:6], nil, v0)
		if retErr != nil {
			return nil, nil, retErr
		}
		if (opcode >> 4) == 0x0d {
			coords[4] += pen[0]
			coords[5] += pen[1]
		}
		u.upgradeArc(pen, coords[0], coords[1], coords[2], largeArc, sweep, coords[4], coords[5])
		pen[0] = coords[4]
		pen[1] = coords[5]
	}
	return v1, v0, nil
}

func (u *upgrader) upgradeArc(pen *[2]float32, rx, ry, xAxisRotation float32, largeArc, sweep bool, finalX, finalY float32) {
	// We follow the "Conversion from endpoint to center parameterization"
	// algorithm as per
	// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter

	// There seems to be a bug in the spec's "implementation notes".
	//
	// Actual implementations, such as
	//	- https://git.gnome.org/browse/librsvg/tree/rsvg-path.c
	//	- http://svn.apache.org/repos/asf/xmlgraphics/batik/branches/svg11/sources/org/apache/batik/ext/awt/geom/ExtendedGeneralPath.java
	//	- https://java.net/projects/svgsalamander/sources/svn/content/trunk/svg-core/src/main/java/com/kitfox/svg/pathcmd/Arc.java
	//	- https://github.com/millermedeiros/SVGParser/blob/master/com/millermedeiros/geom/SVGArc.as
	// do something slightly different (marked with a †).

	// (†) The Abs isn't part of the spec. Neither is checking that Rx and Ry
	// are non-zero (and non-NaN).
	Rx := math.Abs(float64(rx))
	Ry := math.Abs(float64(ry))
	if !(Rx > 0 && Ry > 0) {
		u.verbs = append(u.verbs, upgradeVerbLineTo)
		u.args = append(u.args, [2]float32{finalX, finalY})
		return
	}

	x1 := float64(pen[0])
	y1 := float64(pen[1])
	x2 := float64(finalX)
	y2 := float64(finalY)

	phi := 2 * math.Pi * float64(xAxisRotation)

	// Step 1: Compute (x1′, y1′)
	halfDx := (x1 - x2) / 2
	halfDy := (y1 - y2) / 2
	cosPhi := math.Cos(phi)
	sinPhi := math.Sin(phi)
	x1Prime := +cosPhi*halfDx + sinPhi*halfDy
	y1Prime := -sinPhi*halfDx + cosPhi*halfDy

	// Step 2: Compute (cx′, cy′)
	rxSq := Rx * Rx
	rySq := Ry * Ry
	x1PrimeSq := x1Prime * x1Prime
	y1PrimeSq := y1Prime * y1Prime

	// (†) Check that the radii are large enough.
	radiiCheck := x1PrimeSq/rxSq + y1PrimeSq/rySq
	if radiiCheck > 1 {
		c := math.Sqrt(radiiCheck)
		Rx *= c
		Ry *= c
		rxSq = Rx * Rx
		rySq = Ry * Ry
	}

	denom := rxSq*y1PrimeSq + rySq*x1PrimeSq
	step2 := 0.0
	if a := rxSq*rySq/denom - 1; a > 0 {
		step2 = math.Sqrt(a)
	}
	if largeArc == sweep {
		step2 = -step2
	}
	cxPrime := +step2 * Rx * y1Prime / Ry
	cyPrime := -step2 * Ry * x1Prime / Rx

	// Step 3: Compute (cx, cy) from (cx′, cy′)
	cx := +cosPhi*cxPrime - sinPhi*cyPrime + (x1+x2)/2
	cy := +sinPhi*cxPrime + cosPhi*cyPrime + (y1+y2)/2

	// Step 4: Compute θ1 and Δθ
	ax := (+x1Prime - cxPrime) / Rx
	ay := (+y1Prime - cyPrime) / Ry
	bx := (-x1Prime - cxPrime) / Rx
	by := (-y1Prime - cyPrime) / Ry
	theta1 := angle(1, 0, ax, ay)
	deltaTheta := angle(ax, ay, bx, by)
	if sweep {
		if deltaTheta < 0 {
			deltaTheta += 2 * math.Pi
		}
	} else {
		if deltaTheta > 0 {
			deltaTheta -= 2 * math.Pi
		}
	}

	// This ends the
	// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
	// algorithm. What follows below is specific to this implementation.

	// We approximate an arc by one or more cubic Bézier curves.
	n := int(math.Ceil(math.Abs(deltaTheta) / (math.Pi/2 + 0.001)))
	for i := 0; i < n; i++ {
		u.arcSegmentTo(cx, cy,
			theta1+deltaTheta*float64(i+0)/float64(n),
			theta1+deltaTheta*float64(i+1)/float64(n),
			Rx, Ry, cosPhi, sinPhi,
		)
	}
}

// arcSegmentTo approximates an arc by a cubic Bézier curve. The mathematical
// formulae for the control points are the same as that used by librsvg.
func (u *upgrader) arcSegmentTo(cx, cy, theta1, theta2, rx, ry, cosPhi, sinPhi float64) {
	halfDeltaTheta := (theta2 - theta1) * 0.5
	q := math.Sin(halfDeltaTheta * 0.5)
	t := (8 * q * q) / (3 * math.Sin(halfDeltaTheta))
	cos1 := math.Cos(theta1)
	sin1 := math.Sin(theta1)
	cos2 := math.Cos(theta2)
	sin2 := math.Sin(theta2)
	x1 := rx * (+cos1 - t*sin1)
	y1 := ry * (+sin1 + t*cos1)
	x2 := rx * (+cos2 + t*sin2)
	y2 := ry * (+sin2 - t*cos2)
	x3 := rx * (+cos2)
	y3 := ry * (+sin2)
	highResolutionCoordinates := u.opts.ArcsExpandWithHighResolutionCoordinates
	u.verbs = append(u.verbs, upgradeVerbCubeTo)
	u.args = append(u.args,
		[2]float32{
			quantize(float32(cx+cosPhi*x1-sinPhi*y1), highResolutionCoordinates),
			quantize(float32(cy+sinPhi*x1+cosPhi*y1), highResolutionCoordinates),
		},
		[2]float32{
			quantize(float32(cx+cosPhi*x2-sinPhi*y2), highResolutionCoordinates),
			quantize(float32(cy+sinPhi*x2+cosPhi*y2), highResolutionCoordinates),
		},
		[2]float32{
			quantize(float32(cx+cosPhi*x3-sinPhi*y3), highResolutionCoordinates),
			quantize(float32(cy+sinPhi*x3+cosPhi*y3), highResolutionCoordinates),
		},
	)
}

func countFFV1Instructions(src buffer) (ret uint64) {
	for len(src) > 0 {
		ret++
		opcode := src[0]
		src = src[1:]

		switch {
		case opcode < 0x40:
			switch {
			case opcode < 0x30:
				nReps := uint32(opcode & 15)
				if nReps == 0 {
					n := 0
					nReps, n = src.decodeNaturalFFV1()
					src = src[n:]
					nReps += 16
				}
				nCoords := 2 * (1 + int(opcode>>4))
				for ; nReps > 0; nReps-- {
					for i := 0; i < nCoords; i++ {
						_, n := src.decodeNaturalFFV1()
						src = src[n:]
					}
				}
			case opcode < 0x35:
				for i := 0; i < 4; i++ {
					_, n := src.decodeNaturalFFV1()
					src = src[n:]
				}
			case opcode == 0x35:
				for i := 0; i < 2; i++ {
					_, n := src.decodeNaturalFFV1()
					src = src[n:]
				}
			case opcode == 0x36:
				src = src[1:]
			case opcode == 0x37:
				// No-op.
			default:
				// upgradeBytecode (with calculatingJumpLOD set) will not emit
				// jump or call instructions.
				panic("unexpected FFV1 instruction")
			}

		case opcode < 0x80:
			switch (opcode >> 4) & 3 {
			case 0, 1:
				src = src[4:]
			case 2:
				src = src[8:]
			default:
				src = src[8*(2+int(opcode&15)):]
			}

		case opcode < 0xc0:
			switch (opcode >> 4) & 3 {
			case 0:
				// No-op.
			case 1:
				src = src[13:]
			case 2:
				src = src[25:]
			default:
				// upgradeBytecode (with calculatingJumpLOD set) will not emit
				// reserved instructions.
				panic("unexpected FFV1 instruction")
			}

		default:
			// upgradeBytecode (with calculatingJumpLOD set) will not emit
			// reserved instructions.
			panic("unexpected FFV1 instruction")
		}
	}
	return ret
}

type upgradeColor struct {
	typ          ColorType
	paletteIndex uint8
	blend        uint8
	rgba         color.RGBA
	color0       *upgradeColor
	color1       *upgradeColor
}

func (u *upgrader) resolve(c Color, denyBlend bool) (upgradeColor, error) {
	switch c.typ {
	case ColorTypeRGBA:
		return upgradeColor{
			typ:  ColorTypeRGBA,
			rgba: c.data,
		}, nil
	case ColorTypePaletteIndex:
		return upgradeColor{
			typ:          ColorTypePaletteIndex,
			paletteIndex: c.paletteIndex(),
		}, nil
	case ColorTypeCReg:
		upgrade := u.creg[c.cReg()]
		if denyBlend && (upgrade.typ == ColorTypeBlend) {
			return upgradeColor{}, errUnsupportedUpgrade
		}
		return upgrade, nil
	}

	if denyBlend {
		return upgradeColor{}, errUnsupportedUpgrade
	}
	t, c0, c1 := c.blend()
	color0, err := u.resolve(decodeColor1(c0), true)
	if err != nil {
		return upgradeColor{}, err
	}
	color1, err := u.resolve(decodeColor1(c1), true)
	if err != nil {
		return upgradeColor{}, err
	}
	return upgradeColor{
		typ:    ColorTypeBlend,
		blend:  t,
		color0: &color0,
		color1: &color1,
	}, nil
}
