package chunk

import (
	"slices"

	"github.com/df-mc/dragonfly/server/block/cube"
)

// Chunk is a segment in the world with a size of 16x16x256 blocks. A chunk contains multiple sub chunks
// and stores other information such as biomes.
// It is not safe to call methods on Chunk simultaneously from multiple goroutines.
type Chunk struct {
	// r holds the (vertical) range of the Chunk. It includes both the minimum and maximum coordinates.
	r cube.Range
	// br is the block registry used for this chunk.
	br BlockRegistry
	// air is the runtime ID of air.
	air uint32
	// recalculateHeightMap is true if the chunk's height map should be recalculated on the next call to the HeightMap
	// function.
	recalculateHeightMap bool
	// heightMap is the height map of the chunk.
	heightMap HeightMap
	// sub holds all sub chunks part of the chunk. The pointers held by the array are nil if no sub chunk is
	// allocated at the indices.
	sub []*SubChunk
	// biomes is an array of biome IDs. There is one biome ID for every column in the chunk.
	biomes []*PalettedStorage
}

// New initialises a new chunk and returns it, so that it may be used.
// The BlockRegistry passed must be finalized and must correspond to the runtime IDs used in the chunk's storages.
func New(br BlockRegistry, r cube.Range) *Chunk {
	n := (r.Height() >> 4) + 1
	sub, biomes := make([]*SubChunk, n), make([]*PalettedStorage, n)
	air := br.AirRuntimeID()
	for i := 0; i < n; i++ {
		sub[i] = NewSubChunk(air)
		biomes[i] = emptyStorage(0)
	}
	return &Chunk{
		r:                    r,
		br:                   br,
		air:                  air,
		sub:                  sub,
		biomes:               biomes,
		recalculateHeightMap: true,
		heightMap:            make(HeightMap, 256),
	}
}

// Clone returns an independent copy of the Chunk.
func (chunk *Chunk) Clone() *Chunk {
	clone := &Chunk{
		r:                    chunk.r,
		br:                   chunk.br,
		air:                  chunk.air,
		recalculateHeightMap: chunk.recalculateHeightMap,
		heightMap:            slices.Clone(chunk.heightMap),
		sub:                  make([]*SubChunk, len(chunk.sub)),
		biomes:               make([]*PalettedStorage, len(chunk.biomes)),
	}
	for i, sub := range chunk.sub {
		clone.sub[i] = sub.Clone()
	}
	for i, biomes := range chunk.biomes {
		clone.biomes[i] = biomes.Clone()
	}
	return clone
}

// Equals returns if the chunk passed is equal to the current one
func (chunk *Chunk) Equals(c *Chunk) bool {
	if !chunk.recalculateHeightMap && !c.recalculateHeightMap && !slices.Equal(c.heightMap, chunk.heightMap) {
		return false
	}

	if c.r != chunk.r || c.air != chunk.air || len(c.sub) != len(chunk.sub) {
		return false
	}

	for i, s := range c.sub {
		if !s.Equals(chunk.sub[i]) {
			return false
		}
	}

	return true
}

// Range returns the cube.Range of the Chunk as passed to New.
func (chunk *Chunk) Range() cube.Range {
	return chunk.r
}

// Sub returns a list of all sub chunks present in the chunk.
func (chunk *Chunk) Sub() []*SubChunk {
	return chunk.sub
}

// Block returns the runtime ID of the block at a given x, y and z in a chunk at the given layer. If no
// sub chunk exists at the given y, the block is assumed to be air.
func (chunk *Chunk) Block(x uint8, y int16, z uint8, layer uint8) uint32 {
	sub := chunk.SubChunk(y)
	if sub.Empty() || uint8(len(sub.storages)) <= layer {
		return chunk.air
	}
	return sub.storages[layer].At(x, uint8(y), z)
}

// SetBlock sets the runtime ID of a block at a given x, y and z in a chunk at the given layer. If no
// SubChunk exists at the given y, a new SubChunk is created and the block is set.
func (chunk *Chunk) SetBlock(x uint8, y int16, z uint8, layer uint8, block uint32) {
	sub := chunk.sub[chunk.SubIndex(y)]
	if uint8(len(sub.storages)) <= layer && block == chunk.air {
		// Air was set at n layer, but there were less than n layers, so there already was air there.
		// Don't do anything with this, just return.
		return
	}
	sub.Layer(layer).Set(x, uint8(y), z, block)
	chunk.recalculateHeightMap = true
}

// Biome returns the biome ID at a specific column in the chunk.
func (chunk *Chunk) Biome(x uint8, y int16, z uint8) uint32 {
	return chunk.biomes[chunk.SubIndex(y)].At(x, uint8(y), z)
}

// SetBiome sets the biome ID at a specific column in the chunk.
func (chunk *Chunk) SetBiome(x uint8, y int16, z uint8, biome uint32) {
	chunk.biomes[chunk.SubIndex(y)].Set(x, uint8(y), z, biome)
}

// Light returns the light level at a specific position in the chunk.
func (chunk *Chunk) Light(x uint8, y int16, z uint8) uint8 {
	ux, uy, uz, sub := x&0xf, uint8(y&0xf), z&0xf, chunk.SubChunk(y)
	sky := sub.SkyLight(ux, uy, uz)
	if sky == 15 {
		// The skylight was already on the maximum value, so return it without checking block light.
		return sky
	}
	if block := sub.BlockLight(ux, uy, uz); block > sky {
		return block
	}
	return sky
}

// SkyLight returns the skylight level at a specific position in the chunk.
func (chunk *Chunk) SkyLight(x uint8, y int16, z uint8) uint8 {
	return chunk.SubChunk(y).SkyLight(x&15, uint8(y&15), z&15)
}

// HighestLightBlocker iterates from the highest non-empty sub chunk downwards to find the Y value of the
// highest block that completely blocks any light from going through. If none is found, the value returned is
// the minimum height.
func (chunk *Chunk) HighestLightBlocker(x, z uint8) int16 {
	return chunk.highestLightBlocker(x, z, false)
}

// highestLightBlocker iterates from the highest non-empty sub chunk downwards
// to find the Y value of the highest block that completely blocks any light
// from going through. If none is found, the value returned is the minimum
// height. If addOne is true, one is added to the Y returned if a block was
// found.
func (chunk *Chunk) highestLightBlocker(x, z uint8, addOne bool) int16 {
	var plus int16
	if addOne {
		plus++
	}
	for index := int16(len(chunk.sub) - 1); index >= 0; index-- {
		if sub := chunk.sub[index]; !sub.Empty() {
			for y := 15; y >= 0; y-- {
				if chunk.br.FilteringBlock(sub.storages[0].At(x, uint8(y), z)) == 15 {
					return int16(y) | chunk.SubY(index) + plus
				}
			}
		}
	}
	return int16(chunk.r[0])
}

// HighestBlock iterates from the highest non-empty sub chunk downwards to find the Y value of the highest
// non-air block at an x and z. If no blocks are present in the column, the minimum height is returned.
func (chunk *Chunk) HighestBlock(x, z uint8) int16 {
	for index := int16(len(chunk.sub) - 1); index >= 0; index-- {
		if sub := chunk.sub[index]; !sub.Empty() {
			for y := 15; y >= 0; y-- {
				if rid := sub.storages[0].At(x, uint8(y), z); rid != chunk.air {
					return int16(y) | chunk.SubY(index)
				}
			}
		}
	}
	return int16(chunk.r[0])
}

// HeightMap returns the height map of the chunk. If the chunk is edited, the height map will be recalculated on the
// next call to this function.
func (chunk *Chunk) HeightMap() HeightMap {
	if chunk.recalculateHeightMap {
		for x := uint8(0); x < 16; x++ {
			for z := uint8(0); z < 16; z++ {
				chunk.heightMap.Set(x, z, chunk.highestLightBlocker(x, z, true))
			}
		}
		chunk.recalculateHeightMap = false
	}
	return chunk.heightMap
}

// Compact compacts the chunk as much as possible, getting rid of any sub chunks that are empty, and compacts
// all storages in the sub chunks to occupy as little space as possible.
// Compact should be called right before the chunk is saved in order to optimise the storage space.
func (chunk *Chunk) Compact() {
	for i := range chunk.sub {
		chunk.sub[i].compact()
	}
}

// SubChunk finds the correct SubChunk in the Chunk by a Y value.
func (chunk *Chunk) SubChunk(y int16) *SubChunk {
	return chunk.sub[chunk.SubIndex(y)]
}

// SubIndex returns the sub chunk Y index matching the y value passed.
func (chunk *Chunk) SubIndex(y int16) int16 {
	return (y - int16(chunk.r[0])) >> 4
}

// SubY returns the sub chunk Y value matching the index passed.
func (chunk *Chunk) SubY(index int16) int16 {
	return (index << 4) + int16(chunk.r[0])
}

// HighestFilledSubChunk returns the number of sub chunks up to and including the
// highest sub chunk in the chunk that has any blocks in it. 0 is returned if no
// subchunks have any blocks.
func (chunk *Chunk) HighestFilledSubChunk() uint16 {
	for i, sub := range slices.Backward(chunk.sub) {
		if !sub.Empty() {
			return uint16(i + 1)
		}
	}
	return 0
}
