package block

import (
	"github.com/df-mc/dragonfly/server/block/cube"
	"github.com/df-mc/dragonfly/server/entity/effect"
	"github.com/df-mc/dragonfly/server/internal/nbtconv"
	"github.com/df-mc/dragonfly/server/item"
	"github.com/df-mc/dragonfly/server/world"
	"math"
	"time"
)

// Beacon is a block that projects a light beam skyward, and can provide status effects such as speed, Jump
// Boost, haste, regeneration, resistance, or strength to nearby players.
type Beacon struct {
	solid
	transparent
	clicksAndSticks
	sourceWaterDisplacer

	// Primary and Secondary are the primary and secondary effects broadcast to nearby entities by the
	// beacon.
	Primary, Secondary effect.LastingType
	// level is the amount of the pyramid's levels, it is defined by the mineral blocks which build up the
	// pyramid, and can be 0-4.
	level int
}

// BeaconSource represents a block which is capable of contributing to powering a beacon pyramid.
type BeaconSource interface {
	// PowersBeacon returns a bool which indicates whether this block can contribute to powering up a
	// beacon pyramid.
	PowersBeacon() bool
}

// BreakInfo ...
func (b Beacon) BreakInfo() BreakInfo {
	return newBreakInfo(3, alwaysHarvestable, nothingEffective, oneOf(Beacon{}))
}

// Activate manages the opening of a beacon by activating it.
func (b Beacon) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *item.UseContext) bool {
	if opener, ok := u.(ContainerOpener); ok {
		opener.OpenBlockContainer(pos, tx)
		return true
	}
	return true
}

// DecodeNBT ...
func (b Beacon) DecodeNBT(data map[string]any) any {
	b.level = int(nbtconv.Int32(data, "Levels"))
	if primary, ok := effect.ByID(int(nbtconv.Int32(data, "Primary"))); ok {
		b.Primary = primary.(effect.LastingType)
	}
	if secondary, ok := effect.ByID(int(nbtconv.Int32(data, "Secondary"))); ok {
		b.Secondary = secondary.(effect.LastingType)
	}
	return b
}

// EncodeNBT ...
func (b Beacon) EncodeNBT() map[string]any {
	m := map[string]any{
		"id":     "Beacon",
		"Levels": int32(b.level),
	}
	if primary, ok := effect.ID(b.Primary); ok {
		m["Primary"] = int32(primary)
	}
	if secondary, ok := effect.ID(b.Secondary); ok {
		m["Secondary"] = int32(secondary)
	}
	return m
}

// SideClosed ...
func (b Beacon) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool {
	return false
}

// LightEmissionLevel ...
func (Beacon) LightEmissionLevel() uint8 {
	return 15
}

// Level returns an integer 0-4 which defines the current pyramid level of the beacon.
func (b Beacon) Level() int {
	return b.level
}

// Tick recalculates level, recalculates the active state of the beacon, and powers players,
// once every 80 ticks (4 seconds).
func (b Beacon) Tick(currentTick int64, pos cube.Pos, tx *world.Tx) {
	if currentTick%80 == 0 {
		before := b.level
		// Recalculating pyramid level and powering up players in range once every 4 seconds.
		b.level = b.recalculateLevel(pos, tx)
		if before != b.level {
			tx.SetBlock(pos, b, nil)
		}
		if b.level == 0 {
			return
		}
		if !b.obstructed(pos, tx) {
			b.broadcastBeaconEffects(pos, tx)
		}
	}
}

// recalculateLevel recalculates the level of the beacon's pyramid and returns it. The level can be 0-4.
func (b Beacon) recalculateLevel(pos cube.Pos, tx *world.Tx) int {
	var lvl int
	iter := 1
	// This loop goes over all 4 possible pyramid levels.
	for y := pos.Y() - 1; y >= pos.Y()-4; y-- {
		for x := pos.X() - iter; x <= pos.X()+iter; x++ {
			for z := pos.Z() - iter; z <= pos.Z()+iter; z++ {
				if s, ok := tx.Block(cube.Pos{x, y, z}).(BeaconSource); !ok || !s.PowersBeacon() {
					return lvl
				}
			}
		}
		iter++
		lvl++
	}
	return lvl
}

// obstructed determines whether the beacon is currently obstructed.
func (b Beacon) obstructed(pos cube.Pos, tx *world.Tx) bool {
	// Fast obstructed light calculation.
	if tx.SkyLight(pos.Side(cube.FaceUp)) == 15 {
		return false
	}
	// Slow obstructed light calculation, if the fast way out didn't suffice.
	return tx.HighestLightBlocker(pos.X(), pos.Z()) > pos[1]
}

// broadcastBeaconEffects determines the entities in range which could receive the beacon's powers, and
// determines the powers (effects) that these entities could get. Afterwards, the entities in range that are
// beaconAffected get their according effect(s).
func (b Beacon) broadcastBeaconEffects(pos cube.Pos, tx *world.Tx) {
	seconds := 9 + b.level*2
	if b.level == 4 {
		seconds--
	}
	dur := time.Duration(seconds) * time.Second

	// Establishing what effects are active with the current amount of beacon levels.
	primary, secondary := b.Primary, effect.LastingType(nil)
	switch b.level {
	case 0:
		primary = nil
	case 1:
		switch primary {
		case effect.Resistance, effect.JumpBoost, effect.Strength:
			primary = nil
		}
	case 2:
		if primary == effect.Strength {
			primary = nil
		}
	case 3:
		// Accept all effects for primary, but leave secondary as nil.
	default:
		secondary = b.Secondary
	}
	var primaryEff, secondaryEff effect.Effect
	// Determining whether the primary power is set.
	if primary != nil {
		primaryEff = effect.NewAmbient(primary, 1, dur)
		// Secondary power can only be set if the primary power is set.
		if secondary != nil {
			// It is possible to select 2 primary powers if the beacon's level is 4. This then means that the effect
			// should get a level of 2.
			if primary == secondary {
				primaryEff = effect.NewAmbient(primary, 2, dur)
			} else {
				secondaryEff = effect.NewAmbient(secondary, 1, dur)
			}
		}
	}

	// Finding entities in range.
	r := 10 + (b.level * 10)
	entitiesInRange := tx.EntitiesWithin(cube.Box(
		float64(pos.X()-r), -math.MaxFloat64, float64(pos.Z()-r),
		float64(pos.X()+r), math.MaxFloat64, float64(pos.Z()+r),
	))
	for e := range entitiesInRange {
		if p, ok := e.(beaconAffected); ok {
			if primaryEff.Type() != nil {
				p.AddEffect(primaryEff)
			}
			if secondaryEff.Type() != nil {
				p.AddEffect(secondaryEff)
			}
		}
	}
}

// beaconAffected represents an entity that can be powered by a beacon. Only players will implement this.
type beaconAffected interface {
	// AddEffect adds a specific effect to the entity that implements this interface.
	AddEffect(e effect.Effect)
	// BeaconAffected returns whether this entity can be powered by a beacon.
	BeaconAffected() bool
}

// EncodeItem ...
func (Beacon) EncodeItem() (name string, meta int16) {
	return "minecraft:beacon", 0
}

// EncodeBlock ...
func (Beacon) EncodeBlock() (string, map[string]any) {
	return "minecraft:beacon", nil
}
