package world

import (
	_ "embed"
	"encoding/binary"
	"fmt"
)

// blockPropertiesBinary is generated by tools/VanillaBlockStateDump.java
// directly from the 26.1.2 runtime block-state registry. None of what it holds
// appears in generated/reports/blocks.json or in any tag file: light dampening,
// emission, voxel face-occlusion masks, whether a state blocks motion, whether
// it holds a fluid, and whether its block is a LeavesBlock all exist only once
// vanilla has built the registry.
//
//go:embed block_properties.bin
var blockPropertiesBinary []byte

var (
	blockOpacity    [totalBlockStates]byte
	blockEmission   [totalBlockStates]byte
	blockStateFlags [totalBlockStates]byte
	blockLightShape [totalBlockStates]uint16
	lightFaceShapes [][lightShapeBytes]byte
)

const (
	blockPropertiesMagic   = 0x52494f4c // RIOL
	blockPropertiesVersion = 2
	lightShapeBytes        = 6 * 32 // six 16x16 face masks
)

// Per-state flag bits, as written by tools/VanillaBlockStateDump.java. The
// first three drive lighting; the rest drive the heightmaps, and none of them
// can be derived from blocks.json or from a tag file — they only exist once
// vanilla's block-state registry is built.
const (
	flagPropagatesSkylight = 1 << iota
	flagCanOcclude
	flagShapeForOcclusion
	flagBlocksMotion
	flagFluid
	flagLeaves
)

func init() {
	if err := decodeBlockProperties(blockPropertiesBinary); err != nil {
		panic(fmt.Sprintf("world: decode vanilla block properties: %v", err))
	}
}

func decodeBlockProperties(data []byte) error {
	if len(data) < 16 {
		return fmt.Errorf("header is truncated")
	}
	if binary.BigEndian.Uint32(data[0:4]) != blockPropertiesMagic {
		return fmt.Errorf("invalid magic")
	}
	if version := binary.BigEndian.Uint32(data[4:8]); version != blockPropertiesVersion {
		return fmt.Errorf("unsupported version %d", version)
	}
	states := int(binary.BigEndian.Uint32(data[8:12]))
	shapes := int(binary.BigEndian.Uint32(data[12:16]))
	if states != totalBlockStates {
		return fmt.Errorf("state count %d, want %d", states, totalBlockStates)
	}
	want := 16 + states*5 + shapes*lightShapeBytes
	if len(data) != want {
		return fmt.Errorf("length %d, want %d", len(data), want)
	}

	offset := 16
	for id := 0; id < states; id++ {
		blockOpacity[id] = data[offset]
		blockEmission[id] = data[offset+1]
		blockStateFlags[id] = data[offset+2]
		blockLightShape[id] = binary.BigEndian.Uint16(data[offset+3 : offset+5])
		if int(blockLightShape[id]) >= shapes {
			return fmt.Errorf("state %d references shape %d of %d", id, blockLightShape[id], shapes)
		}
		offset += 5
	}
	lightFaceShapes = make([][lightShapeBytes]byte, shapes)
	for i := range lightFaceShapes {
		copy(lightFaceShapes[i][:], data[offset:offset+lightShapeBytes])
		offset += lightShapeBytes
	}
	return nil
}

func lightOpacity(state uint16) byte {
	if int(state) >= len(blockOpacity) {
		return 15
	}
	return blockOpacity[state]
}

func lightEmission(state uint16) byte {
	if int(state) >= len(blockEmission) {
		return 0
	}
	return blockEmission[state]
}

func stateFlags(state uint16) byte {
	if int(state) >= len(blockStateFlags) {
		return 0
	}
	return blockStateFlags[state]
}

// blocksMotionOrFluid is Heightmap.Types.MOTION_BLOCKING's predicate:
// blocksMotion() || !getFluidState().isEmpty(). Note that a waterlogged block
// satisfies the fluid half, which is why the flag is per state and not per
// block.
func blocksMotionOrFluid(state uint16) bool {
	return stateFlags(state)&(flagBlocksMotion|flagFluid) != 0
}

// blocksMotionNoLeaves is MOTION_BLOCKING_NO_LEAVES's predicate: the same, less
// anything that is a LeavesBlock. Vanilla tests instanceof, not the
// minecraft:leaves tag, and the two sets are not the same.
func blocksMotionNoLeaves(state uint16) bool {
	f := stateFlags(state)
	return f&(flagBlocksMotion|flagFluid) != 0 && f&flagLeaves == 0
}

// lightShapeOccludes mirrors Shapes.faceShapeOccludes for the 1/16-resolution
// face masks emitted from vanilla's VoxelShape data.
func lightShapeOccludes(from, into uint16, direction int) bool {
	if direction < 0 || direction >= 6 {
		return false
	}
	var fromShape, intoShape [lightShapeBytes]byte
	// Vanilla substitutes an empty shape unless both flags are true. Ordinary
	// full cubes are handled by dampening; only shape-aware blocks (slabs,
	// stairs, etc.) participate in face occlusion.
	const shapeAware = flagCanOcclude | flagShapeForOcclusion
	if int(from) < len(blockStateFlags) && blockStateFlags[from]&shapeAware == shapeAware {
		fromShape = lightFaceShapes[blockLightShape[from]]
	}
	if int(into) < len(blockStateFlags) && blockStateFlags[into]&shapeAware == shapeAware {
		intoShape = lightFaceShapes[blockLightShape[into]]
	}
	opposite := [...]int{1, 0, 3, 2, 5, 4}
	fromOffset := direction * 32
	intoOffset := opposite[direction] * 32
	for i := 0; i < 32; i++ {
		if fromShape[fromOffset+i]|intoShape[intoOffset+i] != 0xff {
			return false
		}
	}
	return true
}
