package world

import (
	"time"

	"github.com/df-mc/dragonfly/server/block/cube"
	"github.com/go-gl/mathgl/mgl64"
)

// weather implements weather related methods for World. World embeds this
// struct, so any exported methods on weather are exported methods on World.
type weather struct{ w *World }

// StopWeatherCycle disables weather cycle of the World.
func (w weather) StopWeatherCycle() {
	w.enableWeatherCycle(false)
}

// StartWeatherCycle enables weather cycle of the World.
func (w weather) StartWeatherCycle() {
	w.enableWeatherCycle(true)
}

// snowingAt checks if it is snowing at a specific cube.Pos in the World. True
// is returned if the temperature in the Biome at that position is sufficiently
// low, if it is raining and if it's above the top-most obstructing block.
func (w weather) snowingAt(pos cube.Pos) bool {
	if w.w == nil || !w.w.Dimension().WeatherCycle() {
		return false
	}
	if b := w.w.biome(pos); b.Rainfall() == 0 || w.w.temperature(pos) > 0.15 {
		return false
	}
	w.w.set.Lock()
	raining := w.w.set.Raining
	w.w.set.Unlock()
	return raining && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1]
}

// rainingAt checks if it is raining at a specific cube.Pos in the World. True
// is returned if it is raining, if the temperature is high enough in the biome
// for it not to be snow and if the block is above the top-most obstructing
// block.
func (w weather) rainingAt(pos cube.Pos) bool {
	if w.w == nil || !w.w.Dimension().WeatherCycle() {
		return false
	}
	if b := w.w.biome(pos); b.Rainfall() == 0 || w.w.temperature(pos) <= 0.15 {
		return false
	}
	w.w.set.Lock()
	a := w.w.set.Raining
	w.w.set.Unlock()
	return a && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1]
}

// thunderingAt checks if it is thundering at a specific cube.Pos in the World.
// True is returned if rainingAt returns true and if it is thundering in the
// world.
func (w weather) thunderingAt(pos cube.Pos) bool {
	raining := w.rainingAt(pos)
	w.w.set.Lock()
	a := w.w.set.Thundering && raining
	w.w.set.Unlock()
	return a && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1]
}

// raining checks if it is raining anywhere in the World.
func (w weather) raining() bool {
	if w.w == nil || !w.w.Dimension().WeatherCycle() {
		return false
	}
	w.w.set.Lock()
	defer w.w.set.Unlock()
	return w.w.set.Raining
}

// thundering checks if it is thundering anywhere in the World.
func (w weather) thundering() bool {
	if w.w == nil || !w.w.Dimension().WeatherCycle() {
		return false
	}
	w.w.set.Lock()
	defer w.w.set.Unlock()
	return w.w.set.Thundering
}

// StartRaining makes it rain in the World. The time.Duration passed will
// determine how long it will rain.
func (w weather) StartRaining(dur time.Duration) {
	w.w.set.Lock()
	defer w.w.set.Unlock()
	w.setRaining(true, dur)
}

// StopRaining makes it stop raining in the World.
func (w weather) StopRaining() {
	w.w.set.Lock()
	defer w.w.set.Unlock()

	if w.w.set.Raining {
		w.setRaining(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
		if w.w.set.Thundering {
			// Also reset thunder if it was previously thundering.
			w.setThunder(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
		}
	}
}

// StartThundering makes it thunder in the World. The time.Duration passed will
// determine how long it will thunder. StartThundering will also make it rain
// if it wasn't already raining. In this case the rain will, like the thunder,
// last for the time.Duration passed.
func (w weather) StartThundering(dur time.Duration) {
	w.w.set.Lock()
	defer w.w.set.Unlock()

	w.setThunder(true, dur)
	w.setRaining(true, dur)
}

// StopThundering makes it stop thundering in the current world.
func (w weather) StopThundering() {
	w.w.set.Lock()
	defer w.w.set.Unlock()
	if w.w.set.Thundering && w.w.set.Raining {
		w.setThunder(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
	}
}

// advanceWeather advances the weather counters of the World. Rain and thunder
// are stopped/started when the rain and thunder times reach 0.
func (w weather) advanceWeather() {
	w.w.set.RainTime--
	w.w.set.ThunderTime--

	if w.w.set.RainTime <= 0 {
		// Wiki: The rain counter counts down to zero, and each time it reaches
		// zero, the rain is toggled on or off. When the rain is turned on, the
		// counter is reset to a value between 12,000-23,999 ticks (0.5-1 game
		// days) and when the rain is turned off it is reset to a value of
		// 12,000-179,999 ticks (0.5-7.5 game days).
		if w.w.set.Raining {
			w.w.setRaining(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
		} else {
			w.w.setRaining(true, time.Second*time.Duration(w.w.r.IntN(600)+600))
		}
	}
	if w.w.set.ThunderTime <= 0 {
		// Wiki: the thunder counter toggles thunder on/off when it reaches
		// zero, but clear weather overrides the "on" state. When thunder is
		// turned on, the thunder counter is reset to 3,600-15,999 ticks (3-13
		// minutes), and when thunder is turned off the counter rests to
		// 12,000-179,999 ticks (0.5-7.5 days).
		if w.w.set.Thundering {
			w.w.setThunder(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
		} else {
			w.w.setThunder(true, time.Second*time.Duration(w.w.r.IntN(620)+180))
		}
	}
}

// setRaining toggles raining depending on the raining argument. This does not
// lock the world mutex as opposed to StartRaining and StopRaining.
func (w weather) setRaining(raining bool, x time.Duration) {
	w.w.set.Raining = raining
	w.w.set.RainTime = int64(x.Seconds() * 20)
}

// setThunder toggles thundering depending on the thundering argument. This
// does not lock the world mutex as opposed to StartThundering and
// StopThundering.
func (w weather) setThunder(thundering bool, x time.Duration) {
	w.w.set.Thundering = thundering
	w.w.set.ThunderTime = int64(x.Seconds() * 20)
}

// enableWeatherCycle either enables or disables the weather cycle of the World.
func (w weather) enableWeatherCycle(v bool) {
	if w.w == nil {
		return
	}
	w.w.set.Lock()
	defer w.w.set.Unlock()
	w.w.set.WeatherCycle = v
}

// tickLightning iterates over all loaded chunks in the World, striking
// lightning in each one with a 1/100,000 chance.
func (w weather) tickLightning(tx *Tx) {
	positions := make([]ChunkPos, 0, len(w.w.chunks)/100000)
	for pos := range w.w.chunks {
		// Wiki: For each loaded chunk, every tick there is a 1⁄100,000 chance
		// of an attempted lightning strike during a thunderstorm
		if w.w.r.IntN(100000) == 0 {
			positions = append(positions, pos)
		}
	}

	for _, pos := range positions {
		w.w.strikeLightning(tx, pos)
	}
}

// strikeLightning attempts to strike lightning in the world at a specific
// ChunkPos. The final position is influenced by living entities that might be
// near the lightning strike. If there is no rain at the final position
// selected, the lightning strike will fail.
func (w weather) strikeLightning(tx *Tx, c ChunkPos) {
	if w.w.conf.Entities.conf.Lightning == nil {
		return
	}
	if pos := w.lightningPosition(tx, c); tx.ThunderingAt(cube.PosFromVec3(pos)) {
		tx.AddEntity(w.w.conf.Entities.conf.Lightning(EntitySpawnOpts{Position: pos}))
	}
}

// lightningPosition finds a random position in the ChunkPos to strike
// lightning and adjusts the position to any of the living entities found in or
// above the position if any are found.
func (w weather) lightningPosition(tx *Tx, c ChunkPos) mgl64.Vec3 {
	v := w.w.r.Int32()
	x, z := float64(c[0]<<4+(v&0xf)), float64(c[1]<<4+((v>>8)&0xf))

	vec := w.adjustPositionToEntities(tx, mgl64.Vec3{x, float64(tx.HighestBlock(int(x), int(z)) + 1), z})
	if pos := cube.PosFromVec3(vec); len(tx.Block(pos).Model().BBox(pos, tx)) != 0 {
		// If lightning is about to strike inside a block that is not fully
		// transparent. In this case, move the lightning up by one block so that
		// it strikes above the block.
		return vec.Add(mgl64.Vec3{0, 1})
	}
	return vec
}

// adjustPositionToEntities adjusts the mgl64.Vec3 passed to the position of
// any Entity found in the 3x3 column upwards from the mgl64.Vec3. If multiple
// entities are found, the position of one of the entities is selected
// randomly.
func (w weather) adjustPositionToEntities(tx *Tx, vec mgl64.Vec3) mgl64.Vec3 {
	max := vec.Add(mgl64.Vec3{0, float64(w.w.Range().Max())})

	list := make([]mgl64.Vec3, 0, 16)
	for e := range tx.EntitiesWithin(cube.Box(vec[0], vec[1], vec[2], max[0], max[1], max[2]).GrowVec3(mgl64.Vec3{3, 3, 3})) {
		if h, ok := e.(interface{ Health() float64 }); ok && h.Health() > 0 {
			// Any (living) entity that is positioned higher than the highest
			// block at its position is eligible to be struck by lightning. We
			// first save all entity positions where this is the case.
			pos := cube.PosFromVec3(e.Position())
			if tx.HighestBlock(pos[0], pos[1]) < pos[2] {
				list = append(list, e.Position())
			}
		}
	}
	// We then select one of the positions of entities higher than the highest
	// block and adjust the position of the lightning to it, so that the entity
	// is struck directly.
	if len(list) > 0 {
		vec = list[w.w.r.IntN(len(list))]
	}
	return vec
}
