package entity

import (
	"github.com/df-mc/dragonfly/server/block/cube"
	"github.com/df-mc/dragonfly/server/world"
	"github.com/go-gl/mathgl/mgl64"
	"math"
	"time"
)

// ExperienceOrbBehaviourConfig holds optional parameters for the creation of
// an ExperienceOrbBehaviour.
type ExperienceOrbBehaviourConfig struct {
	// 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
	// ExistenceDuration specifies how long the experience orb should last. The
	// default is time.Minute * 5.
	ExistenceDuration time.Duration
	// Experience is the amount of experience held by the orb. Default is 1.
	Experience int
}

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

// New creates an ExperienceOrbBehaviour using the parameters in conf.
func (conf ExperienceOrbBehaviourConfig) New() *ExperienceOrbBehaviour {
	if conf.Experience == 0 {
		conf.Experience = 1
	}
	if conf.ExistenceDuration == 0 {
		conf.ExistenceDuration = time.Minute * 5
	}
	b := &ExperienceOrbBehaviour{conf: conf, lastSearch: time.Now()}
	b.passive = PassiveBehaviourConfig{
		Gravity:           conf.Gravity,
		Drag:              conf.Drag,
		ExistenceDuration: conf.ExistenceDuration,
		Tick:              b.tick,
	}.New()
	return b
}

// ExperienceOrbBehaviour implements Behaviour for an experience orb entity.
type ExperienceOrbBehaviour struct {
	conf ExperienceOrbBehaviourConfig

	passive *PassiveBehaviour

	lastSearch time.Time
	target     *world.EntityHandle
}

// Experience returns the amount of experience the orb carries.
func (exp *ExperienceOrbBehaviour) Experience() int {
	return exp.conf.Experience
}

// Tick finds a target for the experience orb and moves the orb towards it.
func (exp *ExperienceOrbBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
	return exp.passive.Tick(e, tx)
}

// followBox is the bounding box used to search for collectors to follow for experience orbs.
var followBox = cube.Box(-8, -8, -8, 8, 8, 8)

// tick finds a target for the experience orb and moves the orb towards it.
func (exp *ExperienceOrbBehaviour) tick(e *Ent, tx *world.Tx) {
	targetEnt, ok := exp.target.Entity(tx)
	target, _ := targetEnt.(experienceCollector)

	pos := e.Position()
	hasTarget := ok && !target.Dead() && pos.Sub(target.Position()).Len() <= 8
	if !hasTarget && time.Since(exp.lastSearch) >= time.Second {
		exp.findTarget(tx, pos)
	} else if hasTarget {
		exp.moveToTarget(e, target)
	}
}

// findTarget attempts to find a target for an experience orb in w around pos.
func (exp *ExperienceOrbBehaviour) findTarget(tx *world.Tx, pos mgl64.Vec3) {
	exp.target = nil
	for o := range tx.EntitiesWithin(followBox.Translate(pos)) {
		if ec, ok := o.(experienceCollector); ok && ec.CanCollectExperience() {
			exp.target = o.H()
			break
		}
	}
	exp.lastSearch = time.Now()
}

// moveToTarget applies velocity to the experience orb so that it moves towards
// its current target. If it intersects with the target, the orb is collected.
func (exp *ExperienceOrbBehaviour) moveToTarget(e *Ent, target experienceCollector) {
	pos, dst := e.Position(), target.Position()
	if o, ok := target.(Eyed); ok {
		dst[1] += o.EyeHeight() / 2
	}
	diff := dst.Sub(pos).Mul(0.125)
	if dist := diff.LenSqr(); dist < 1 {
		e.SetVelocity(e.Velocity().Add(diff.Normalize().Mul(0.2 * math.Pow(1-math.Sqrt(dist), 2))))
	}

	if e.H().Type().BBox(e).Translate(pos).IntersectsWith(target.H().Type().BBox(target).Translate(target.Position())) && target.CollectExperience(exp.conf.Experience) {
		_ = e.Close()
	}
}

// experienceCollector represents an entity that can collect experience orbs.
type experienceCollector interface {
	Living
	// CollectExperience makes the player collect the experience points passed,
	// adding it to the experience manager. A bool is returned indicating
	// whether the player was able to collect the experience, or not, due to the
	// 100ms delay between experience collection.
	CollectExperience(value int) bool
	// CanCollectExperience returns whether the player can collect experience or not.
	CanCollectExperience() bool
}
