package world

import "regionio/internal/protocol"

// Light is stored as vanilla nibble arrays: one 2048-byte array per 16^3
// section. The two protocol-only sections below and above the world are added
// while encoding.
const lightSections = SectionCount + 2

type lightVolume struct {
	minX, minZ int
	width      int
	depth      int
	blocks     []uint16
	sky        []byte
	block      []byte
}

type lightNode struct {
	x, y, z int
}

var lightDirections = [...]lightNode{
	{0, -1, 0},
	{0, 1, 0},
	{0, 0, -1},
	{0, 0, 1},
	{-1, 0, 0},
	{1, 0, 0},
}

func newLightVolume(minCX, minCZ, chunksWide, chunksDeep int, chunks map[[2]int32]*Chunk) *lightVolume {
	v := &lightVolume{
		minX:  minCX * 16,
		minZ:  minCZ * 16,
		width: chunksWide * 16,
		depth: chunksDeep * 16,
	}
	count := v.width * WorldHeight * v.depth
	v.blocks = make([]uint16, count)
	v.sky = make([]byte, count)
	v.block = make([]byte, count)
	for key, chunk := range chunks {
		baseX := int(key[0])*16 - v.minX
		baseZ := int(key[1])*16 - v.minZ
		for y := MinY; y < MinY+WorldHeight; y++ {
			for z := 0; z < 16; z++ {
				for x := 0; x < 16; x++ {
					idx := v.indexLocal(baseX+x, y, baseZ+z)
					v.blocks[idx] = chunk.getBlock(x, y, z)
					if chunk.lightReady {
						v.sky[idx] = chunk.getLight(false, x, y, z)
						v.block[idx] = chunk.getLight(true, x, y, z)
					}
				}
			}
		}
	}
	return v
}

func (v *lightVolume) indexLocal(x, y, z int) int {
	return ((y-MinY)*v.depth+z)*v.width + x
}

func (v *lightVolume) inside(x, y, z int) bool {
	return x >= 0 && x < v.width && z >= 0 && z < v.depth && y >= MinY && y < MinY+WorldHeight
}

func (v *lightVolume) calculate() {
	v.calculateSky()
	v.calculateBlock()
}

func (v *lightVolume) calculateSky() {
	queue := make([]int, 0, len(v.sky)/2)
	for z := 0; z < v.depth; z++ {
		for x := 0; x < v.width; x++ {
			from := StateAir
			for y := MinY + WorldHeight - 1; y >= MinY; y-- {
				idx := v.indexLocal(x, y, z)
				state := v.blocks[idx]
				if lightOpacity(state) != 0 || lightShapeOccludes(from, state, 0) {
					break
				}
				v.sky[idx] = 15
				queue = append(queue, idx)
				from = state
			}
		}
	}
	v.propagateIncreases(v.sky, queue)
}

func (v *lightVolume) calculateBlock() {
	queue := make([]int, 0, 256)
	for idx, state := range v.blocks {
		if emission := lightEmission(state); emission > 0 {
			v.block[idx] = emission
			queue = append(queue, idx)
		}
	}
	v.propagateIncreases(v.block, queue)
}

func (v *lightVolume) propagateIncreases(levels []byte, queue []int) {
	for head := 0; head < len(queue); head++ {
		idx := queue[head]
		level := levels[idx]
		if level <= 1 {
			continue
		}
		x, y, z := v.coordinates(idx)
		from := v.blocks[idx]
		for direction, delta := range lightDirections {
			nx, ny, nz := x+delta.x, y+delta.y, z+delta.z
			if !v.inside(nx, ny, nz) {
				continue
			}
			nidx := v.indexLocal(nx, ny, nz)
			into := v.blocks[nidx]
			attenuation := lightOpacity(into)
			if attenuation < 1 {
				attenuation = 1
			}
			if attenuation >= level || lightShapeOccludes(from, into, direction) {
				continue
			}
			candidate := level - attenuation
			if candidate > levels[nidx] {
				levels[nidx] = candidate
				queue = append(queue, nidx)
			}
		}
	}
}

func (v *lightVolume) coordinates(idx int) (x, y, z int) {
	x = idx % v.width
	row := idx / v.width
	z = row % v.depth
	y = row/v.depth + MinY
	return
}

func (v *lightVolume) relaxBlockChange(x, y, z int) {
	skySources := v.skySources()
	blockSeeds := make([]int, 0, 7)
	if v.inside(x, y, z) {
		idx := v.indexLocal(x, y, z)
		blockSeeds = append(blockSeeds, idx)
		for _, delta := range lightDirections {
			if v.inside(x+delta.x, y+delta.y, z+delta.z) {
				blockSeeds = append(blockSeeds, v.indexLocal(x+delta.x, y+delta.y, z+delta.z))
			}
		}
	}
	v.relax(v.block, nil, blockSeeds)

	skySeeds := make([]int, 0, WorldHeight*2)
	for sy := MinY; sy < MinY+WorldHeight; sy++ {
		idx := v.indexLocal(x, sy, z)
		skySeeds = append(skySeeds, idx)
		for _, direction := range []int{4, 5, 2, 3} {
			delta := lightDirections[direction]
			if v.inside(x+delta.x, sy, z+delta.z) {
				skySeeds = append(skySeeds, v.indexLocal(x+delta.x, sy, z+delta.z))
			}
		}
	}
	v.relax(v.sky, skySources, skySeeds)
}

func (v *lightVolume) skySources() []bool {
	sources := make([]bool, len(v.sky))
	for z := 0; z < v.depth; z++ {
		for x := 0; x < v.width; x++ {
			from := StateAir
			for y := MinY + WorldHeight - 1; y >= MinY; y-- {
				idx := v.indexLocal(x, y, z)
				state := v.blocks[idx]
				if lightOpacity(state) != 0 || lightShapeOccludes(from, state, 0) {
					break
				}
				sources[idx] = true
				from = state
			}
		}
	}
	return sources
}

func (v *lightVolume) relax(levels []byte, sources []bool, seeds []int) {
	queued := make([]bool, len(levels))
	queue := make([]int, 0, len(seeds)*2)
	for _, idx := range seeds {
		if idx >= 0 && idx < len(levels) && !queued[idx] {
			queued[idx] = true
			queue = append(queue, idx)
		}
	}
	for head := 0; head < len(queue); head++ {
		idx := queue[head]
		queued[idx] = false
		desired := v.desiredLight(levels, sources, idx)
		if desired == levels[idx] {
			continue
		}
		levels[idx] = desired
		x, y, z := v.coordinates(idx)
		for _, delta := range lightDirections {
			nx, ny, nz := x+delta.x, y+delta.y, z+delta.z
			if !v.inside(nx, ny, nz) {
				continue
			}
			nidx := v.indexLocal(nx, ny, nz)
			if !queued[nidx] {
				queued[nidx] = true
				queue = append(queue, nidx)
			}
		}
	}
}

func (v *lightVolume) desiredLight(levels []byte, sources []bool, idx int) byte {
	state := v.blocks[idx]
	desired := lightEmission(state)
	if sources != nil {
		desired = 0
		if sources[idx] {
			desired = 15
		}
	}
	attenuation := lightOpacity(state)
	if attenuation < 1 {
		attenuation = 1
	}
	x, y, z := v.coordinates(idx)
	opposite := [...]int{1, 0, 3, 2, 5, 4}
	for direction, delta := range lightDirections {
		nx, ny, nz := x+delta.x, y+delta.y, z+delta.z
		if !v.inside(nx, ny, nz) {
			continue
		}
		nidx := v.indexLocal(nx, ny, nz)
		neighbor := levels[nidx]
		if neighbor <= attenuation || lightShapeOccludes(v.blocks[nidx], state, opposite[direction]) {
			continue
		}
		candidate := neighbor - attenuation
		if candidate > desired {
			desired = candidate
		}
	}
	return desired
}

func (c *Chunk) getLight(block bool, x, y, z int) byte {
	si := (y - MinY) >> 4
	if si < 0 || si >= SectionCount {
		return 0
	}
	layers := &c.skyLight
	if block {
		layers = &c.blockLight
	}
	section := layers[si]
	if section == nil {
		return 0
	}
	idx := blockIndex(x, y, z)
	b := section[idx>>1]
	if idx&1 == 0 {
		return b & 0x0f
	}
	return b >> 4
}

func (c *Chunk) setLight(block bool, x, y, z int, value byte) bool {
	si := (y - MinY) >> 4
	if si < 0 || si >= SectionCount {
		return false
	}
	layers := &c.skyLight
	if block {
		layers = &c.blockLight
	}
	section := layers[si]
	if section == nil {
		if value == 0 {
			return false
		}
		section = new([2048]byte)
		layers[si] = section
	}
	idx := blockIndex(x, y, z)
	old := section[idx>>1]
	if idx&1 == 0 {
		section[idx>>1] = old&0xf0 | value&0x0f
	} else {
		section[idx>>1] = old&0x0f | value<<4
	}
	return old != section[idx>>1]
}

// LightAt returns the stored sky and block light at a local block coordinate.
func (c *Chunk) LightAt(x, y, z int) (sky, block byte, ready bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.getLight(false, x, y, z), c.getLight(true, x, y, z), c.lightReady
}

func (c *Chunk) installLight(v *lightVolume) bool {
	changed := !c.lightReady
	var sky [SectionCount]*[2048]byte
	var block [SectionCount]*[2048]byte
	baseX := int(c.X)*16 - v.minX
	baseZ := int(c.Z)*16 - v.minZ
	for y := MinY; y < MinY+WorldHeight; y++ {
		for z := 0; z < 16; z++ {
			for x := 0; x < 16; x++ {
				idx := v.indexLocal(baseX+x, y, baseZ+z)
				installNibble(&sky, x, y, z, v.sky[idx])
				installNibble(&block, x, y, z, v.block[idx])
			}
		}
	}
	if !lightLayersEqual(c.skyLight, sky) || !lightLayersEqual(c.blockLight, block) {
		changed = true
	}
	c.skyLight = sky
	c.blockLight = block
	c.lightReady = true
	c.lightValidated = true
	return changed
}

func installNibble(layers *[SectionCount]*[2048]byte, x, y, z int, value byte) {
	if value == 0 {
		return
	}
	si := (y - MinY) >> 4
	if layers[si] == nil {
		layers[si] = new([2048]byte)
	}
	idx := blockIndex(x, y, z)
	if idx&1 == 0 {
		layers[si][idx>>1] |= value
	} else {
		layers[si][idx>>1] |= value << 4
	}
}

func lightLayersEqual(a, b [SectionCount]*[2048]byte) bool {
	for i := 0; i < SectionCount; i++ {
		if a[i] == nil || b[i] == nil {
			if a[i] != nil || b[i] != nil {
				return false
			}
			continue
		}
		if *a[i] != *b[i] {
			return false
		}
	}
	return true
}

func (c *Chunk) writeLight(w *protocol.Writer) {
	sky, block, highest := c.skyLight, c.blockLight, c.highestFilledSection()
	if !c.lightReady {
		chunks := map[[2]int32]*Chunk{{c.X, c.Z}: c}
		v := newLightVolume(int(c.X), int(c.Z), 1, 1, chunks)
		v.calculate()
		standalone := &Chunk{X: c.X, Z: c.Z}
		standalone.installLight(v)
		sky, block = standalone.skyLight, standalone.blockLight
	}
	maxLightSection := highest + 2
	if maxLightSection < 0 {
		maxLightSection = 0
	}
	writeLightLayers(w, sky, block, maxLightSection)
}

func (c *Chunk) highestFilledSection() int {
	for si := SectionCount - 1; si >= 0; si-- {
		if c.sections[si] == nil {
			continue
		}
		for _, state := range c.sections[si] {
			if state != StateAir {
				return si
			}
		}
	}
	return -1
}

func writeLightLayers(w *protocol.Writer, sky, block [SectionCount]*[2048]byte, maxLightSection int) {
	if maxLightSection >= lightSections {
		maxLightSection = lightSections - 1
	}
	var skySections [lightSections]*[2048]byte
	var blockSections [lightSections]*[2048]byte
	for i := 0; i < SectionCount && i+1 <= maxLightSection; i++ {
		skySections[i+1] = sky[i]
		blockSections[i+1] = block[i]
	}
	if maxLightSection == lightSections-1 {
		skySections[maxLightSection] = new([2048]byte)
		for i := range skySections[maxLightSection] {
			skySections[maxLightSection][i] = 0xff
		}
	}

	var skyMask, blockMask, emptySkyMask, emptyBlockMask uint64
	for i := 0; i <= maxLightSection; i++ {
		if skySections[i] == nil {
			emptySkyMask |= 1 << i
		} else {
			skyMask |= 1 << i
		}
		if blockSections[i] == nil {
			emptyBlockMask |= 1 << i
		} else {
			blockMask |= 1 << i
		}
	}
	writeBitSet(w, []uint64{skyMask})
	writeBitSet(w, []uint64{blockMask})
	writeBitSet(w, []uint64{emptySkyMask})
	writeBitSet(w, []uint64{emptyBlockMask})
	writeLightArrays(w, skySections[:])
	writeLightArrays(w, blockSections[:])
}

func writeLightArrays(w *protocol.Writer, sections []*[2048]byte) {
	count := 0
	for _, section := range sections {
		if section != nil {
			count++
		}
	}
	w.VarInt(int32(count))
	for _, section := range sections {
		if section != nil {
			w.VarInt(2048)
			w.Raw(section[:])
		}
	}
}

// EncodeLightUpdate serializes a standalone light_update packet body from a
// consistent chunk snapshot.
func (c *Chunk) EncodeLightUpdate() []byte {
	snapshot, _ := c.snapshot()
	w := protocol.NewWriter(8192)
	w.VarInt(snapshot.X)
	w.VarInt(snapshot.Z)
	sky, block := snapshot.skyLight, snapshot.blockLight
	if !snapshot.lightReady {
		chunks := map[[2]int32]*Chunk{{snapshot.X, snapshot.Z}: snapshot}
		volume := newLightVolume(int(snapshot.X), int(snapshot.Z), 1, 1, chunks)
		volume.calculate()
		standalone := &Chunk{X: snapshot.X, Z: snapshot.Z}
		standalone.installLight(volume)
		sky, block = standalone.skyLight, standalone.blockLight
	}
	writeLightLayers(w, sky, block, lightSections-1)
	return w.Bytes()
}

func allSectionsMask() []uint64 {
	return []uint64{(uint64(1) << lightSections) - 1}
}

func writeBitSet(w *protocol.Writer, longs []uint64) {
	for len(longs) > 0 && longs[len(longs)-1] == 0 {
		longs = longs[:len(longs)-1]
	}
	w.VarInt(int32(len(longs)))
	for _, value := range longs {
		w.Int64(int64(value))
	}
}
