package chunk

import (
	"math"
	"slices"
)

// paletteSize is the size of a palette. It indicates the amount of bits occupied per value stored.
type paletteSize byte

// Palette is a palette of values that every PalettedStorage has. Storages hold 'pointers' to indices
// in this palette.
type Palette struct {
	last      uint32
	lastIndex int16
	size      paletteSize

	// values is a map of values. A PalettedStorage points to the index to this value.
	values []uint32
}

// newPalette returns a new Palette with size and a slice of added values.
func newPalette(size paletteSize, values []uint32) *Palette {
	return &Palette{size: size, values: values, last: math.MaxUint32}
}

// Clone returns an independent copy of the Palette.
func (palette *Palette) Clone() *Palette {
	return &Palette{
		last:      palette.last,
		lastIndex: palette.lastIndex,
		size:      palette.size,
		values:    slices.Clone(palette.values),
	}
}

// Len returns the amount of unique values in the Palette.
func (palette *Palette) Len() int {
	return len(palette.values)
}

// Add adds a values to the Palette. It does not first check if the value was already set in the Palette.
// The index at which the value was added is returned. Another bool is returned indicating if the Palette
// was resized as a result of adding the value.
func (palette *Palette) Add(v uint32) (index int16, resize bool) {
	i := int16(len(palette.values))
	palette.values = append(palette.values, v)

	if palette.needsResize() {
		palette.increaseSize()
		return i, true
	}
	return i, false
}

// Replace calls the function passed for each value present in the Palette. The value returned by the
// function replaces the value present at the index of the value passed.
func (palette *Palette) Replace(f func(v uint32) uint32) {
	// Reset last runtime ID as it now has a different offset.
	palette.last = math.MaxUint32
	for index, v := range palette.values {
		palette.values[index] = f(v)
	}
}

// Index loops through the values of the Palette and looks for the index of the given value. If the value could
// not be found, -1 is returned.
func (palette *Palette) Index(runtimeID uint32) int16 {
	if runtimeID == palette.last {
		// Fast path out.
		return palette.lastIndex
	}
	// Slow path in a separate function allows for inlining the fast path.
	return palette.indexSlow(runtimeID)
}

// indexSlow searches the index of a value in the Palette's values by iterating through the Palette's values.
func (palette *Palette) indexSlow(runtimeID uint32) int16 {
	l := len(palette.values)
	for i := 0; i < l; i++ {
		if palette.values[i] == runtimeID {
			palette.last = runtimeID
			v := int16(i)
			palette.lastIndex = v
			return v
		}
	}
	return -1
}

// Value returns the value in the Palette at a specific index.
func (palette *Palette) Value(i uint16) uint32 {
	return palette.values[i]
}

// needsResize checks if the Palette, and with it the holding PalettedStorage, needs to be resized to a bigger
// size.
func (palette *Palette) needsResize() bool {
	return len(palette.values) > (1 << palette.size)
}

var sizes = [...]paletteSize{0, 1, 2, 3, 4, 5, 6, 8, 16}
var offsets = [...]int{0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 8: 7, 16: 8}

// increaseSize increases the size of the Palette to the next palette size.
func (palette *Palette) increaseSize() {
	palette.size = sizes[offsets[palette.size]+1]
}

// padded returns true if the Palette size is 3, 5 or 6.
func (p paletteSize) padded() bool {
	return p == 3 || p == 5 || p == 6
}

// paletteSizeFor finds a suitable paletteSize for the amount of values passed n.
func paletteSizeFor(n int) paletteSize {
	for _, size := range sizes {
		if n <= (1 << size) {
			return size
		}
	}
	// Should never happen.
	return 0
}

// uint32s returns the amount of uint32s needed to represent a storage with this palette size.
func (p paletteSize) uint32s() (n int) {
	uint32Count := 0
	if p != 0 {
		// indicesPerUint32 is the amount of indices that may be stored in a single uint32.
		indicesPerUint32 := 32 / int(p)
		// uint32Count is the amount of uint32s required to store all indices: 4096 indices need to be stored in
		// total.
		uint32Count = 4096 / indicesPerUint32
	}
	if p.padded() {
		// We've got one of the padded sizes, so the storage has another uint32 to be able to store
		// every index.
		uint32Count++
	}
	return uint32Count
}
