package entity

import (
	"iter"
	"math"
	"math/rand/v2"
	"slices"
	"time"

	"github.com/df-mc/dragonfly/server/block"
	"github.com/df-mc/dragonfly/server/block/cube"
	"github.com/df-mc/dragonfly/server/block/cube/trace"
	"github.com/df-mc/dragonfly/server/entity/effect"
	"github.com/df-mc/dragonfly/server/item"
	"github.com/df-mc/dragonfly/server/item/potion"
	"github.com/df-mc/dragonfly/server/world"
	"github.com/go-gl/mathgl/mgl64"
)

// ProjectileBehaviourConfig allows the configuration of projectiles. Calling
// ProjectileBehaviourConfig.New() creates a ProjectileBehaviour using these
// settings.
type ProjectileBehaviourConfig struct {
	Owner *world.EntityHandle
	// Gravity is the amount of Y velocity subtracted every tick.
	Gravity float64
	// Drag is used to reduce all axes of the velocity every tick. Velocity is
	// multiplied with (1-Drag) every tick.
	Drag float64
	// Damage specifies the base damage dealt by the Projectile. If set to a
	// negative number, entities hit are not hurt at all and are not knocked
	// back. The base damage is multiplied with the velocity of the projectile
	// to calculate the final damage of the projectile.
	Damage float64
	// Potion is the potion effect that is applied to an entity when the
	// projectile hits it.
	Potion potion.Potion
	// KnockBackForceAddend is the additional horizontal velocity that is
	// applied to an entity when it is hit by the projectile.
	KnockBackForceAddend float64
	// KnockBackHeightAddend is the additional vertical velocity that is applied
	// to an entity when it is hit by the projectile.
	KnockBackHeightAddend float64
	// Particle is a particle that is spawned when the projectile hits a
	// target, either a block or an entity. No particle is spawned if left nil.
	Particle world.Particle
	// ParticleCount is the amount of particles that should be spawned if
	// Particle is not nil. ParticleCount will be set to 1 if Particle is not
	// nil and ParticleCount is 0.
	ParticleCount int
	// Sound is a sound that is played when the projectile hits a target, either
	// a block or an entity. No sound is played if left nil.
	Sound world.Sound
	// Critical specifies if the projectile is critical. This spawns critical
	// hit particles behind the projectile and causes it to deal up to 50% more
	// damage.
	Critical bool
	// Hit is a function that is called when the projectile Ent hits a target
	// (the trace.Result). The target is either of the type trace.EntityResult
	// or trace.BlockResult. Hit may be set to run additional behaviour when a
	// projectile hits a target.
	Hit func(e *Ent, tx *world.Tx, target trace.Result)
	// SurviveBlockCollision specifies if a projectile with this
	// ProjectileBehaviour should survive collision with a block. If set to
	// false, the projectile will break when hitting a block (like a snowball).
	// If set to true, the projectile will survive like an arrow does.
	SurviveBlockCollision bool
	// BlockCollisionVelocityMultiplier is the multiplier used to modify the
	// velocity of a projectile that has SurviveBlockCollision set to true. The
	// default, 0, will cause the projectile to lose its velocity completely. A
	// multiplier such as 0.5 will reduce the projectile's velocity, but retain
	// half of it after inverting the axis on which the projectile collided.
	BlockCollisionVelocityMultiplier float64
	// DisablePickup specifies if picking up the projectile should be disabled,
	// which is relevant in the case SurviveBlockCollision is set to true. Some
	// projectiles, such as arrows, cannot be picked up if they are shot by
	// monsters like skeletons.
	DisablePickup bool
	// PickupItem is the item that is given to a player when it picks up this
	// projectile. If left as an empty item.Stack, no item is given upon pickup.
	PickupItem item.Stack
	// CollisionPosition specifies the position that the projectile is stuck
	// in. If non-empty, the entity will not move.
	CollisionPosition cube.Pos
	// PiercingLevel is the crossbow Piercing enchantment level. The projectile
	// passes through PiercingLevel entities and damages PiercingLevel+1 in
	// total. A value of 0 means no piercing.
	PiercingLevel int
}

func (conf ProjectileBehaviourConfig) Apply(data *world.EntityData) {
	data.Data = conf.New()
}

// New creates a new ProjectileBehaviour using conf. The owner passed may be nil
// if the projectile does not have one.
func (conf ProjectileBehaviourConfig) New() *ProjectileBehaviour {
	if conf.ParticleCount == 0 && conf.Particle != nil {
		conf.ParticleCount = 1
	}
	return &ProjectileBehaviour{conf: conf, collided: conf.CollisionPosition != cube.Pos{}, collisionPos: conf.CollisionPosition, mc: &MovementComputer{
		Gravity:           conf.Gravity,
		Drag:              conf.Drag,
		DragBeforeGravity: true,
	}}
}

// ProjectileBehaviour implements the behaviour of projectiles. Its specifics
// may be configured using ProjectileBehaviourConfig.
type ProjectileBehaviour struct {
	conf        ProjectileBehaviourConfig
	mc          *MovementComputer
	ageCollided int
	close       bool

	collisionPos cube.Pos
	collided     bool

	collidedEntities []*world.EntityHandle
}

// Owner returns the owner of the projectile.
func (lt *ProjectileBehaviour) Owner() *world.EntityHandle {
	return lt.conf.Owner
}

// Explode adds velocity to a projectile to blast it away from the explosion's
// source.
func (lt *ProjectileBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, _ block.ExplosionConfig) {
	e.data.Vel = e.Velocity().Add(e.Position().Sub(src).Normalize().Mul(impact))
}

// Potion returns the potion.Potion that is applied to an entity if hit by the
// projectile.
func (lt *ProjectileBehaviour) Potion() potion.Potion {
	return lt.conf.Potion
}

// Critical returns true if ProjectileBehaviourConfig.Critical was set to true
// and if the projectile has not collided.
func (lt *ProjectileBehaviour) Critical() bool {
	return lt.conf.Critical && !lt.collided
}

// Tick runs the tick-based behaviour of a ProjectileBehaviour and returns the
// Movement within the tick. Tick handles the movement, collision and hitting
// of a projectile.
func (lt *ProjectileBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
	if lt.close {
		_ = e.Close()
		return nil
	}

	if lt.collided && lt.tickAttached(e, tx) {
		if lt.ageCollided > 1200 {
			lt.close = true
		}
		return nil
	}
	vel := e.Velocity()
	m, result := lt.tickMovement(e, tx)
	e.data.Pos, e.data.Vel = m.pos, m.vel

	lt.collisionPos, lt.collided, lt.ageCollided = cube.Pos{}, false, 0

	if result == nil {
		return m
	}

	for i := 0; i < lt.conf.ParticleCount; i++ {
		tx.AddParticle(result.Position(), lt.conf.Particle)
	}
	if lt.conf.Sound != nil {
		tx.PlaySound(result.Position(), lt.conf.Sound)
	}

	switch r := result.(type) {
	case trace.EntityResult:
		if l, ok := r.Entity().(Living); ok {
			if lt.conf.Damage >= 0 {
				lt.hitEntity(l, e, vel)
			}
			lt.collidedEntities = append(lt.collidedEntities, l.H())
		}
	case trace.BlockResult:
		bpos := r.BlockPosition()
		if h, ok := tx.Block(bpos).(block.ProjectileHitter); ok {
			h.ProjectileHit(bpos, tx, e, r.Face())
		}
		if lt.conf.SurviveBlockCollision {
			lt.hitBlockSurviving(e, r, m, tx)
			return m
		}
		lt.close = true
	}
	if lt.conf.Hit != nil {
		lt.conf.Hit(e, tx, result)
	}

	if len(lt.collidedEntities) > lt.conf.PiercingLevel {
		lt.close = true
	}
	return m
}

// tickAttached performs the attached logic for a projectile. It checks if the
// projectile is still attached to a block and if it can be picked up.
func (lt *ProjectileBehaviour) tickAttached(e *Ent, tx *world.Tx) bool {
	boxes := tx.Block(lt.collisionPos).Model().BBox(lt.collisionPos, tx)
	box := e.H().Type().BBox(e).Translate(e.Position())

	for _, bb := range boxes {
		if box.IntersectsWith(bb.Translate(lt.collisionPos.Vec3()).Grow(0.05)) {
			if lt.ageCollided > 5 && !lt.conf.DisablePickup {
				lt.tryPickup(e, tx)
			}
			lt.ageCollided++
			return true
		}
	}
	return false
}

// tryPickup checks for nearby projectile collectors and closes the entity if
// one was found.
func (lt *ProjectileBehaviour) tryPickup(e *Ent, tx *world.Tx) {
	translated := e.H().Type().BBox(e).Translate(e.Position())
	grown := translated.GrowVec3(mgl64.Vec3{1, 0.5, 1})
	for other := range tx.EntitiesWithin(translated.Grow(2)) {
		if !other.H().Type().BBox(other).Translate(other.Position()).IntersectsWith(grown) {
			continue
		}
		collector, ok := other.(Collector)
		if !ok {
			continue
		}
		if _, ok := collector.Collect(lt.conf.PickupItem); !ok {
			continue
		}

		// A collector was within range and able to pick up the entity.
		lt.close = true
		for _, viewer := range tx.Viewers(e.Position()) {
			viewer.ViewEntityAction(e, PickedUpAction{Collector: collector})
		}
	}
}

// hitBlockSurviving is called if
// ProjectileBehaviourConfig.SurviveBlockCollision is set to true and the
// projectile collides with a block. If the resulting velocity is roughly 0,
// it sets the projectile as having collided with the block.
func (lt *ProjectileBehaviour) hitBlockSurviving(e *Ent, r trace.BlockResult, m *Movement, tx *world.Tx) {
	// Create an epsilon for deciding if the projectile has slowed down enough
	// for us to consider it as having collided for the final time. We take the
	// square root because FloatEqualThreshold squares it, which is not what we
	// want.
	eps := math.Sqrt(0.1 * (1 - lt.conf.BlockCollisionVelocityMultiplier))
	if mgl64.FloatEqualThreshold(e.Velocity().Len(), 0, eps) {
		e.SetVelocity(mgl64.Vec3{})
		lt.collisionPos, lt.collided = r.BlockPosition(), true

		for _, v := range tx.Viewers(m.pos) {
			v.ViewEntityAction(e, ArrowShakeAction{Duration: time.Millisecond * 350})
			v.ViewEntityState(e)
		}
		return
	}
}

// hitEntity is called when a projectile hits a Living. It deals damage to the
// entity and knocks it back. Additionally, it applies any potion effects and
// fire if applicable.
func (lt *ProjectileBehaviour) hitEntity(l Living, e *Ent, vel mgl64.Vec3) {
	owner, _ := lt.conf.Owner.Entity(e.tx)
	src := ProjectileDamageSource{Projectile: e, Owner: owner}
	dmg := math.Ceil(lt.conf.Damage * vel.Len())
	if lt.conf.Critical {
		dmg += rand.Float64() * dmg / 2
	}
	// TODO: Piercing arrows should bypass shield blocking when shields are implemented.
	if _, vulnerable := l.Hurt(dmg, src); vulnerable {
		l.KnockBack(l.Position().Sub(vel), 0.45+lt.conf.KnockBackForceAddend, 0.3608+lt.conf.KnockBackHeightAddend)

		for _, eff := range lt.conf.Potion.Effects() {
			if lasting, ok := eff.Type().(effect.LastingType); ok {
				l.AddEffect(effect.New(lasting, eff.Level(), time.Duration(float64(eff.Duration())/8)))
				continue
			}
			l.AddEffect(eff)
		}
		if flammable, ok := l.(Flammable); ok && e.OnFireDuration() > 0 {
			flammable.SetOnFire(time.Second * 5)
		}
	}
}

// tickMovement ticks the movement of a projectile. It updates the position and
// rotation of the projectile based on its velocity and updates the velocity
// based on gravity and drag.
func (lt *ProjectileBehaviour) tickMovement(e *Ent, tx *world.Tx) (*Movement, trace.Result) {
	pos, vel := e.Position(), e.Velocity()
	viewers := tx.Viewers(pos)

	velBefore := vel
	vel = lt.mc.applyHorizontalForces(tx, pos, lt.mc.applyVerticalForces(vel))
	rot := cube.Rotation{
		mgl64.RadToDeg(math.Atan2(vel[0], vel[2])),
		mgl64.RadToDeg(math.Atan2(vel[1], math.Hypot(vel[0], vel[2]))),
	}

	var (
		end = pos.Add(vel)
		hit trace.Result
		ok  bool
	)
	if !mgl64.FloatEqual(end.Sub(pos).LenSqr(), 0) {
		if hit, ok = trace.Perform(pos, end, tx, e.H().Type().BBox(e).Grow(1.0), lt.ignores(e)); ok {
			if _, ok := hit.(trace.BlockResult); ok {
				// Undo the gravity because the velocity as a result of gravity
				// at the point of collision should be 0.
				vel[1] = (vel[1] + lt.mc.Gravity) / (1 - lt.mc.Drag)
				x, y, z := vel.Mul(lt.conf.BlockCollisionVelocityMultiplier).Elem()
				// Calculate multipliers for all coordinates: 1 for the ones that
				// weren't on the same axis as the one collided with, -1 for the one
				// that was on that axis to deflect the projectile.
				mx, my, mz := hit.Face().Axis().Vec3().Mul(-2).Add(mgl64.Vec3{1, 1, 1}).Elem()

				vel = mgl64.Vec3{x * mx, y * my, z * mz}
			} else if lt.conf.PiercingLevel == 0 {
				vel = zeroVec3
			}
			end = hit.Position()
		}
	}
	return &Movement{v: viewers, e: e, pos: end, vel: vel, dpos: end.Sub(pos), dvel: vel.Sub(velBefore), rot: rot}, hit
}

// ignores returns a function to ignore entities in trace.Perform that are
// either a spectator, not living, the entity itself, its owner in the first
// 5 ticks, or an entity it already collided with.
func (lt *ProjectileBehaviour) ignores(e *Ent) trace.EntityFilter {
	return func(seq iter.Seq[world.Entity]) iter.Seq[world.Entity] {
		return func(yield func(world.Entity) bool) {
			for other := range seq {
				g, ok := other.(interface{ GameMode() world.GameMode })
				spectator := ok && !g.GameMode().HasCollision()
				itself := e.H() == other.H()
				_, living := other.(Living)
				owner := e.data.Age < time.Second/4 && lt.conf.Owner == other.H()
				collidedEntity := slices.Contains(lt.collidedEntities, other.H())
				if spectator || itself || !living || owner || collidedEntity {
					continue
				}
				if !yield(other) {
					return
				}
			}
		}
	}
}
