package item

import (
	"time"
	_ "unsafe"

	"github.com/df-mc/dragonfly/server/block/cube"
	"github.com/df-mc/dragonfly/server/world"
	"github.com/df-mc/dragonfly/server/world/sound"
)

// Crossbow is a ranged weapon similar to a bow that uses arrows or fireworks
// as ammunition.
type Crossbow struct {
	// Item is the item the crossbow is charged with.
	Item Stack
}

// Charge starts the charging process and checks if the charge duration meets
// the required duration.
func (c Crossbow) Charge(releaser Releaser, _ *world.Tx, ctx *UseContext, duration time.Duration) bool {
	if !c.Item.Empty() {
		return false
	}

	creative := releaser.GameMode().CreativeInventory()
	held, left := releaser.HeldItems()

	if chargeDuration, _ := c.chargeDuration(held); duration < chargeDuration {
		return false
	}
	projectileItem, ok := c.findProjectile(releaser, ctx)
	if !ok {
		return false
	}
	c.Item = projectileItem.Grow(-projectileItem.Count() + 1)
	if !creative {
		ctx.Consume(c.Item)
	}

	releaser.SetHeldItems(held.WithItem(c), left)
	return true
}

// ContinueCharge ...
func (c Crossbow) ContinueCharge(releaser Releaser, tx *world.Tx, ctx *UseContext, duration time.Duration) {
	if !c.Item.Empty() {
		return
	}

	held, _ := releaser.HeldItems()
	if _, ok := c.findProjectile(releaser, ctx); !ok {
		return
	}

	chargeDuration, qcLevel := c.chargeDuration(held)
	if duration.Seconds() <= 0.1 {
		tx.PlaySound(releaser.Position(), sound.CrossbowLoad{Stage: sound.CrossbowLoadingStart, QuickCharge: qcLevel > 0})
	}

	// Base reload time is 25 ticks; each Quick Charge level reduces by 5 ticks
	multiplier := 25.0 / float64(25-(5*qcLevel))

	// Adjust ticks based on the multiplier
	adjustedTicks := int(float64(duration.Milliseconds()) / (50 / multiplier))

	// Play sound after every 16 ticks (adjusted by Quick Charge)
	if adjustedTicks%16 == 0 {
		tx.PlaySound(releaser.Position(), sound.CrossbowLoad{Stage: sound.CrossbowLoadingMiddle, QuickCharge: qcLevel > 0})
	}

	if progress := float64(duration) / float64(chargeDuration); progress >= 1 {
		tx.PlaySound(releaser.Position(), sound.CrossbowLoad{Stage: sound.CrossbowLoadingEnd, QuickCharge: qcLevel > 0})
	}
}

// chargeDuration calculates the duration required to charge the crossbow and
// the quick charge enchantment level, if any.
func (c Crossbow) chargeDuration(s Stack) (dur time.Duration, quickChargeLvl int) {
	dur, lvl := time.Duration(1.25*float64(time.Second)), 0
	for _, enchant := range s.Enchantments() {
		if q, ok := enchant.Type().(interface{ ChargeDuration(int) time.Duration }); ok {
			dur = min(dur, q.ChargeDuration(enchant.Level()))
			lvl = enchant.Level()
		}
	}
	return dur, lvl
}

// findProjectile looks through the inventory of a Releaser to find a projectile
// to insert into the crossbow. It first checks the left hand for fireworks or
// arrows, and searches the rest of the inventory for arrows if no valid
// projectile was in the left hand. False is returned if no valid projectile was
// anywhere in the inventory.
func (c Crossbow) findProjectile(r Releaser, ctx *UseContext) (Stack, bool) {
	_, left := r.HeldItems()
	_, isFirework := left.Item().(Firework)
	_, isArrow := left.Item().(Arrow)
	if isFirework || isArrow {
		return left, true
	}
	if res, ok := ctx.FirstFunc(func(stack Stack) bool {
		_, ok := stack.Item().(Arrow)
		return ok
	}); ok {
		return res, true
	}
	if r.GameMode().CreativeInventory() {
		// No projectiles in inventory but the player is in creative mode, so
		// return an arrow anyway.
		return NewStack(Arrow{}, 1), true
	}
	return Stack{}, false
}

// ReleaseCharge checks if the item is fully charged and, if so, releases it.
func (c Crossbow) ReleaseCharge(releaser Releaser, tx *world.Tx, ctx *UseContext) bool {
	if c.Item.Empty() {
		return false
	}

	held, _ := releaser.HeldItems()
	creative := releaser.GameMode().CreativeInventory()

	pierceLevel, multishot := 0, false
	for _, enchant := range held.Enchantments() {
		if _, ok := enchant.Type().(interface{ MultipleProjectiles() bool }); ok {
			multishot = true
		}
		if _, ok := enchant.Type().(interface{ Pierces() bool }); ok {
			pierceLevel = enchant.Level()
		}
	}

	arrowConf := world.ArrowSpawnConfig{
		Damage:              9,
		Owner:               releaser,
		Critical:            true,
		ObtainArrowOnPickup: !creative,
		PiercingLevel:       pierceLevel,
	}
	c.shoot(releaser, tx, 0, arrowConf)
	if multishot {
		arrowConf.ObtainArrowOnPickup = false
		c.shoot(releaser, tx, -10, arrowConf)
		c.shoot(releaser, tx, 10, arrowConf)
	}
	c.applyDamage(ctx)

	c.Item = Stack{}
	held, left := releaser.HeldItems()
	crossbow := held.WithItem(c)
	releaser.SetHeldItems(crossbow, left)
	tx.PlaySound(releaser.Position(), sound.CrossbowShoot{})
	return true
}

// CanCharge ...
func (c Crossbow) CanCharge(releaser Releaser, _ *world.Tx, ctx *UseContext) bool {
	_, found := c.findProjectile(releaser, ctx)
	return found && !c.Item.Empty()
}

// shoot fires the crossbow's loaded projectiles.
func (c Crossbow) shoot(releaser Releaser, tx *world.Tx, offsetAngle float64, arrowConf world.ArrowSpawnConfig) {
	rot := releaser.Rotation()
	dirVec := cube.Rotation{rot[0] + offsetAngle, rot[1]}.Vec3()

	if firework, ok := c.Item.Item().(Firework); ok {
		createFirework := tx.World().EntityRegistry().Config().Firework
		projectile := createFirework(world.EntitySpawnOpts{
			Position: torsoPosition(releaser),
			Velocity: dirVec.Mul(0.8),
			Rotation: rot.Neg(),
		}, firework, releaser, 1.0, 0, false)
		tx.AddEntity(projectile)
	} else {
		createArrow := tx.World().EntityRegistry().Config().Arrow
		arrowConf.Tip = c.Item.Item().(Arrow).Tip
		arrow := createArrow(world.EntitySpawnOpts{
			Position: torsoPosition(releaser),
			Velocity: dirVec.Mul(5.15),
			Rotation: rot.Neg(),
		}, arrowConf)
		tx.AddEntity(arrow)
	}
}

// applyDamage applies damage on a UseContext based on the projectile loaded
// in the crossboww.
func (c Crossbow) applyDamage(ctx *UseContext) {
	if _, ok := c.Item.Item().(Firework); ok {
		ctx.DamageItem(3)
	} else {
		ctx.DamageItem(1)
	}
}

// MaxCount always returns 1.
func (Crossbow) MaxCount() int {
	return 1
}

// DurabilityInfo ...
func (Crossbow) DurabilityInfo() DurabilityInfo {
	return DurabilityInfo{
		MaxDurability: 464,
		BrokenItem:    simpleItem(Stack{}),
	}
}

// FuelInfo ...
func (Crossbow) FuelInfo() FuelInfo {
	return newFuelInfo(time.Second * 15)
}

// EnchantmentValue ...
func (Crossbow) EnchantmentValue() int {
	return 1
}

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

// DecodeNBT ...
func (c Crossbow) DecodeNBT(data map[string]any) any {
	c.Item = mapItem(data, "chargedItem")
	return c
}

// EncodeNBT ...
func (c Crossbow) EncodeNBT() map[string]any {
	if !c.Item.Empty() {
		return map[string]any{"chargedItem": writeItem(c.Item, true)}
	}
	return nil
}

// noinspection ALL
//
//go:linkname writeItem github.com/df-mc/dragonfly/server/internal/nbtconv.WriteItem
func writeItem(s Stack, disk bool) map[string]any

// noinspection ALL
//
//go:linkname mapItem github.com/df-mc/dragonfly/server/internal/nbtconv.MapItem
func mapItem(x map[string]any, k string) Stack
