package world

import (
	"sync"

	"regionio/internal/worldgen"
)

// vanillaTerrainCache stores immutable, undecorated terrain snapshots shared
// by overlapping region requests. A region generator must still clone these
// chunks before mutable feature replay, but neighboring cache misses no longer
// rerun the expensive density/carver stage for the same coordinates.
type vanillaTerrainCache struct {
	mu     sync.Mutex
	chunks map[[2]int32]*Chunk
	loads  map[[2]int32]*terrainLoad
	max    int
}

type terrainLoad struct {
	done  chan struct{}
	chunk *Chunk
}

func newVanillaTerrainCache(max int) *vanillaTerrainCache {
	return &vanillaTerrainCache{
		chunks: make(map[[2]int32]*Chunk),
		loads:  make(map[[2]int32]*terrainLoad),
		max:    max,
	}
}

func (c *vanillaTerrainCache) get(key [2]int32, build func() *Chunk) *Chunk {
	c.mu.Lock()
	if chunk := c.chunks[key]; chunk != nil {
		c.mu.Unlock()
		return chunk
	}
	if load := c.loads[key]; load != nil {
		c.mu.Unlock()
		<-load.done
		return load.chunk
	}
	load := &terrainLoad{done: make(chan struct{})}
	c.loads[key] = load
	c.mu.Unlock()

	chunk := build()
	c.mu.Lock()
	if existing := c.chunks[key]; existing != nil {
		load.chunk = existing
		delete(c.loads, key)
		close(load.done)
		c.mu.Unlock()
		return existing
	}
	if len(c.chunks) >= c.max {
		// The cache is an optimization only. Evict one arbitrary old entry when
		// full; correctness never depends on retaining a particular chunk.
		for oldKey := range c.chunks {
			delete(c.chunks, oldKey)
			break
		}
	}
	c.chunks[key] = chunk
	load.chunk = chunk
	delete(c.loads, key)
	close(load.done)
	c.mu.Unlock()
	return chunk
}

func terrainClone(chunk *Chunk) *Chunk {
	clone, _ := chunk.snapshot()
	return clone
}

// NewVanillaRegionGenerator builds a target from a mutable five-by-five base
// neighborhood. Vanilla feature placement for a center chunk can inspect and
// write into adjacent chunks; the radius-two base supplies the complete source
// biome neighborhood needed by the nine source centers around that target.
//
// This generator is intentionally separate from NewVanillaGenerator while its
// full decoration parity is being measured. It uses the vanilla-compatible
// Xoroshiro feature RNG and region ore replay, then applies the remaining
// non-ore decoration to the target.
func NewVanillaRegionGenerator(seed int64) Generator {
	od, fluidPicker, veins, carver := vanillaGeneratorInputs(seed)
	return vanillaRegionGeneratorFromInputs(seed, od, fluidPicker, veins, carver, newVanillaTerrainCache(256))
}

// NewVanillaRegionBatchGenerator builds one complete 3x3 target batch from a
// shared 7x7 base terrain neighborhood. Each target receives private clones of
// its 5x5 mutable decoration region, so cross-chunk feature writes cannot leak
// into the neighboring target's generation.
func NewVanillaRegionBatchGenerator(seed int64) BatchGenerator {
	od, fluidPicker, veins, carver := vanillaGeneratorInputs(seed)
	return vanillaRegionBatchGeneratorFromInputs(seed, od, fluidPicker, veins, carver, newVanillaTerrainCache(256))
}

// NewVanillaRegionGenerators returns the region-faithful single and batch
// generators sharing one immutable worldgen input set.
func NewVanillaRegionGenerators(seed int64) (Generator, BatchGenerator) {
	od, fluidPicker, veins, carver := vanillaGeneratorInputs(seed)
	terrain := newVanillaTerrainCache(256)
	return vanillaRegionGeneratorFromInputs(seed, od, fluidPicker, veins, carver, terrain),
		vanillaRegionBatchGeneratorFromInputs(seed, od, fluidPicker, veins, carver, terrain)
}

func vanillaRegionGeneratorFromInputs(seed int64, od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, terrain *vanillaTerrainCache) Generator {
	return func(targetX, targetZ int32) *Chunk {
		chunks := make([]*Chunk, 0, 25)
		for cx := targetX - 2; cx <= targetX+2; cx++ {
			for cz := targetZ - 2; cz <= targetZ+2; cz++ {
				key := [2]int32{cx, cz}
				base := terrain.get(key, func() *Chunk {
					return generateVanillaWithoutDecoration(od, fluidPicker, veins, carver, seed, cx, cz)
				})
				chunks = append(chunks, terrainClone(base))
			}
		}
		region, err := newDecorationRegion(chunks)
		if err != nil {
			panic("world: creating decoration region: " + err.Error())
		}
		if err := region.replayScheduledOres(od, seed, targetX, targetZ); err != nil {
			panic("world: replaying region ores: " + err.Error())
		}
		target := region.chunks[[2]int32{targetX, targetZ}]
		decorateGeneratedNonOre(target, od, seed)
		return target
	}
}

func vanillaRegionBatchGeneratorFromInputs(seed int64, od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, terrain *vanillaTerrainCache) BatchGenerator {
	return func(targetX, targetZ int32) (map[[2]int32]*Chunk, error) {
		base := make(map[[2]int32]*Chunk, 49)
		for cx := targetX - 3; cx <= targetX+3; cx++ {
			for cz := targetZ - 3; cz <= targetZ+3; cz++ {
				key := [2]int32{cx, cz}
				base[key] = terrain.get(key, func() *Chunk {
					return generateVanillaWithoutDecoration(od, fluidPicker, veins, carver, seed, cx, cz)
				})
			}
		}
		batch := make(map[[2]int32]*Chunk, 9)
		for cx := targetX - 1; cx <= targetX+1; cx++ {
			for cz := targetZ - 1; cz <= targetZ+1; cz++ {
				chunks := make([]*Chunk, 0, 25)
				for sx := cx - 2; sx <= cx+2; sx++ {
					for sz := cz - 2; sz <= cz+2; sz++ {
						baseChunk := base[[2]int32{sx, sz}]
						clone, _ := baseChunk.snapshot()
						chunks = append(chunks, clone)
					}
				}
				region, err := newDecorationRegion(chunks)
				if err != nil {
					return nil, err
				}
				if err := region.replayScheduledOres(od, seed, cx, cz); err != nil {
					return nil, err
				}
				target := region.chunks[[2]int32{cx, cz}]
				decorateGeneratedNonOre(target, od, seed)
				batch[[2]int32{cx, cz}] = target
			}
		}
		return batch, nil
	}
}

func decorateGeneratedNonOre(c *Chunk, od *worldgen.OverworldDensity, seed int64) {
	var surfTop [16][16]int
	var grass [16][16]bool
	var biomeName [16][16]string
	baseX, baseZ := int(c.X)*16, int(c.Z)*16
	for x := 0; x < 16; x++ {
		for z := 0; z < 16; z++ {
			surfTop[x][z], grass[x][z] = classifyColumnAtSurface(c, x, z)
			biomeName[x][z] = BiomeNameAt(od, baseX+x, baseZ+z)
		}
	}
	r := newChunkRand(c.X, c.Z, seed)
	decorateNonOre(c, od, c.X, c.Z, seed, &surfTop, &grass, &biomeName, &r)
}

func classifyColumnAtSurface(c *Chunk, x, z int) (top int, grass bool) {
	var column [WorldHeight]uint16
	for i := 0; i < WorldHeight; i++ {
		column[i] = c.GetBlock(x, MinY+i, z)
	}
	return classifyColumn(&column)
}

