package item

import (
	"fmt"
	"maps"
	"reflect"
	"slices"
	"sort"
	"strings"
	"sync/atomic"

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

// Stack represents a stack of items. The stack shares the same item type and has a count which specifies the
// size of the stack.
type Stack struct {
	id int32

	item  world.Item
	count int

	customName string
	lore       []string

	damage      int
	unbreakable bool

	anvilCost int

	data map[string]any

	enchantments map[EnchantmentType]Enchantment
}

// NewStack returns a new stack using the item type and the count passed. NewStack panics if the count passed
// is negative or if the item type passed is nil.
func NewStack(t world.Item, count int) Stack {
	if count < 0 {
		panic("cannot use negative count for item stack")
	}
	if t == nil {
		panic("cannot have a stack with item type nil")
	}
	return Stack{item: t, count: count, id: newID()}
}

// Count returns the amount of items that is present on the stack. The count is guaranteed never to be
// negative.
func (s Stack) Count() int {
	return s.count
}

// MaxCount returns the maximum count that the stack is able to hold when added to an inventory or when added
// to an item entity.
func (s Stack) MaxCount() int {
	if counter, ok := s.item.(MaxCounter); ok {
		return counter.MaxCount()
	}
	return 64
}

// Grow grows the Stack's count by n, returning the resulting Stack. If a positive number is passed, the stack
// is grown, whereas if a negative size is passed, the resulting Stack will have a lower count. The count of
// the returned Stack will never be negative.
func (s Stack) Grow(n int) Stack {
	s.count += n
	if s.count < 0 {
		s.count = 0
	}
	s.id = newID()
	return s
}

// Durability returns the current durability of the item stack. If the item is not one that implements the
// Durable interface, BaseDurability will always return -1.
// The closer the durability returned is to 0, the closer the item is to being broken.
func (s Stack) Durability() int {
	if durable, ok := s.Item().(Durable); ok {
		return durable.DurabilityInfo().MaxDurability - s.damage
	}
	return -1
}

// MaxDurability returns the maximum durability that the item stack is able to have. If the item does not
// implement the Durable interface, MaxDurability will always return -1.
func (s Stack) MaxDurability() int {
	if durable, ok := s.Item().(Durable); ok {
		return durable.DurabilityInfo().MaxDurability
	}
	return -1
}

// Damage returns a new stack that is damaged by the amount passed. (Meaning, its durability lowered by the
// amount passed.) If the item does not implement the Durable interface, the original stack is returned.
// The damage passed may be negative to add durability.
// If the final durability reaches 0 or below, the item returned is the resulting item of the breaking of the
// item. If the final durability reaches a number higher than the maximum durability, the stack returned will
// get the maximum durability.
func (s Stack) Damage(d int) Stack {
	durable, ok := s.Item().(Durable)
	if !ok || s.unbreakable {
		return s
	}
	durability := s.Durability()
	info := durable.DurabilityInfo()
	if durability-d <= 0 {
		if info.Persistent {
			// Persistent items can't be broken.
			return s
		}
		// A durability of 0, so the item is broken.
		return info.BrokenItem()
	}
	if durability-d > info.MaxDurability {
		// We've passed the maximum durability, so we just need to make sure the final durability of the item
		// will be equal to the max.
		s.damage, d = 0, 0
	}
	s.damage += d
	return s
}

// WithDurability returns a new item stack with the durability passed. If the item does not implement the
// Durable interface, the original stack is returned.
// The closer the durability d is to 0, the closer the item is to being broken. If a durability of 0 is passed,
// a stack with the item type of the BrokenItem is returned. If a durability is passed that exceeds the
// maximum durability, the stack returned will have the maximum durability.
func (s Stack) WithDurability(d int) Stack {
	durable, ok := s.Item().(Durable)
	if !ok {
		return s
	}
	maxDurability := durable.DurabilityInfo().MaxDurability
	if d > maxDurability {
		// A durability bigger than the max, so the item has no damage at all.
		s.damage = 0
		return s
	}
	if d == 0 {
		// A durability of 0, so the item is broken.
		return durable.DurabilityInfo().BrokenItem()
	}
	s.damage = maxDurability - d
	return s
}

// Unbreakable checks if the item stack is unbreakable.
func (s Stack) Unbreakable() bool {
	return s.unbreakable
}

// AsUnbreakable returns a copy of the Stack with the unbreakable tag set. If the item does not implement the
// Durable interface, the original stack is returned.
func (s Stack) AsUnbreakable() Stack {
	if _, ok := s.Item().(Durable); !ok {
		return s
	}
	s.unbreakable = true
	return s
}

// AsBreakable returns a copy of the Stack without the unbreakable tag set. If the item does not implement the
// Durable interface, the original stack is returned.
func (s Stack) AsBreakable() Stack {
	if _, ok := s.Item().(Durable); !ok {
		return s
	}
	s.unbreakable = false
	return s
}

// Empty checks if the stack is empty (has a count of 0).
func (s Stack) Empty() bool {
	if s.Count() == 0 || s.item == nil {
		return true
	}
	name, _ := s.item.EncodeItem()
	return name == "minecraft:air"
}

// Item returns the item that the stack holds. If the stack is considered empty (Stack.Empty()), Item will
// always return nil.
func (s Stack) Item() world.Item {
	if s.Empty() || s.item == nil {
		return nil
	}
	return s.item
}

// AttackDamage returns the attack damage to the stack. By default, the value returned is 1.0. If the item
// held implements the item.Weapon interface, this damage may be different.
func (s Stack) AttackDamage() float64 {
	if weapon, ok := s.Item().(Weapon); ok {
		// Bonus attack damage from weapons is a bit quirky in Bedrock Edition: Even though tools say they
		// have, for example, + 5 Attack Damage, it is actually 1 + 5, while punching with a hand in Bedrock
		// Edition deals 2 damage, not 1 like in Java Edition.
		// The tooltip displayed in-game is therefore not exactly correct.
		return weapon.AttackDamage() + 1
	}
	return 1.0
}

// WithCustomName returns a copy of the Stack with the custom name passed. The custom name is formatted
// according to the rules of fmt.Sprintln.
func (s Stack) WithCustomName(a ...any) Stack {
	s.customName = format(a)
	if nameable, ok := s.Item().(nameable); ok {
		s.item = nameable.WithName(a...)
	}
	return s
}

// CustomName returns the custom name set for the Stack. An empty string is returned if the Stack has no
// custom name set.
func (s Stack) CustomName() string {
	return s.customName
}

// WithLore returns a copy of the Stack with the lore passed. Each string passed is put on a different line,
// where the first string is at the top and the last at the bottom.
// The lore may be cleared by passing no lines into the Stack.
func (s Stack) WithLore(lines ...string) Stack {
	s.lore = lines
	return s
}

// Lore returns the lore set for the Stack. If no lore is present, the slice returned has a len of 0.
func (s Stack) Lore() []string {
	if s.Empty() {
		return nil
	}
	return s.lore
}

// WithValue returns the current Stack with a value set at a specific key. This method may be used to
// associate custom data with the item stack, which will persist through server restarts.
// The value stored may later be obtained by making a call to Stack.Value().
//
// WithValue may be called with a nil value, in which case the value at the key will be cleared.
//
// WithValue stores Values by encoding them using the encoding/gob package. Users of WithValue must ensure
// that their value is valid for encoding with this package.
func (s Stack) WithValue(key string, val any) Stack {
	s.data = cloneMap(s.data)
	if val != nil {
		s.data[key] = val
	} else {
		delete(s.data, key)
		if len(s.data) == 0 {
			s.data = nil
		}
	}
	return s
}

// Value attempts to return a value set to the Stack using Stack.WithValue(). If a value is found by the key
// passed, it is returned and ok is true. If not found, the value returned is nil and ok is false.
func (s Stack) Value(key string) (val any, ok bool) {
	if s.Empty() {
		return nil, false
	}
	val, ok = s.data[key]
	return val, ok
}

// WithEnchantments returns the current stack with the passed enchantments. If an enchantment is not compatible
// with the item stack, it will not be applied.
func (s Stack) WithEnchantments(enchants ...Enchantment) Stack {
	if _, ok := s.item.(Book); ok {
		s.item = EnchantedBook{}
	}
	s.enchantments = cloneMap(s.enchantments)
	for _, enchant := range enchants {
		if _, ok := s.Item().(EnchantedBook); !ok && !enchant.t.CompatibleWithItem(s.item) {
			// Enchantment is not compatible with the item.
			continue
		}
		compatible := true
		for _, otherEnchant := range s.enchantments {
			addingType := enchant.t
			existingType := otherEnchant.Type()
			addingAcceptsExisting := addingType.CompatibleWithEnchantment(existingType)
			existingAcceptsAdding := existingType.CompatibleWithEnchantment(addingType)
			if addingType != existingType && (!addingAcceptsExisting || !existingAcceptsAdding) {
				compatible = false
				break
			}
		}
		if !compatible {
			// Enchantment is not compatible with another enchantment on the item.
			continue
		}
		s.enchantments[enchant.t] = enchant
	}
	return s
}

// WithForcedEnchantments returns the current stack with the passed enchantments applied,
// bypassing compatibility checks that would normally prevent incompatible enchantments
// from being applied together.
func (s Stack) WithForcedEnchantments(enchants ...Enchantment) Stack {
	s.enchantments = cloneMap(s.enchantments)
	for _, enchant := range enchants {
		s.enchantments[enchant.t] = enchant
	}
	return s
}

// WithoutEnchantments returns the current stack but with the passed enchantments removed.
func (s Stack) WithoutEnchantments(enchants ...EnchantmentType) Stack {
	s.enchantments = cloneMap(s.enchantments)
	for _, enchant := range enchants {
		delete(s.enchantments, enchant)
	}
	if _, ok := s.item.(EnchantedBook); ok && len(s.enchantments) == 0 {
		s.item = Book{}
	}
	return s
}

// Enchantment attempts to return an Enchantment set to the Stack using Stack.WithEnchantment(). If an Enchantment
// is found by the EnchantmentType, the enchantment and the bool true is returned.
func (s Stack) Enchantment(enchant EnchantmentType) (Enchantment, bool) {
	if s.Empty() {
		return Enchantment{}, false
	}
	ench, ok := s.enchantments[enchant]
	return ench, ok
}

// Enchantments returns an array of all Enchantments on the item. Enchantments returns the enchantments of a Stack in a
// deterministic order.
func (s Stack) Enchantments() []Enchantment {
	if s.Empty() {
		return nil
	}
	e := slices.Collect(maps.Values(s.enchantments))
	sort.Slice(e, func(i, j int) bool {
		id1, _ := EnchantmentID(e[i].t)
		id2, _ := EnchantmentID(e[j].t)
		return id1 < id2
	})
	return e
}

// AnvilCost returns the number of experience levels to add to the base level cost when repairing, combining, or
// renaming this item with an anvil.
func (s Stack) AnvilCost() int {
	return s.anvilCost
}

// WithAnvilCost returns the current Stack with the anvil cost set to the passed value.
func (s Stack) WithAnvilCost(anvilCost int) Stack {
	i := s.Item()
	_, repairable := i.(Repairable)
	_, enchantedBook := i.(EnchantedBook)
	if !repairable && !enchantedBook {
		// This item can't have a repair cost.
		return s
	}
	s.anvilCost = anvilCost
	return s
}

// WithItem returns a Stack with the item type passed, copying all the
// properties from s to the new stack. Damage to an item, enchantments and anvil
// costs are only copied if they are still applicable to the new item type.
func (s Stack) WithItem(t world.Item) Stack {
	cp := NewStack(t, s.count).
		Damage(s.damage).
		WithCustomName(s.customName).
		WithLore(s.lore...).
		WithEnchantments(s.Enchantments()...).
		WithAnvilCost(s.anvilCost)
	cp.unbreakable = s.unbreakable && s.MaxDurability() != -1
	cp.data = s.data
	return cp
}

// AddStack adds another stack to the stack and returns both stacks. The first stack returned will have as
// many items in it as possible to fit in the stack, according to a max count of either 64 or otherwise as
// returned by Item.MaxCount(). The second stack will have the leftover items: It may be empty if the count of
// both stacks together don't exceed the max count.
// If the two stacks are not comparable, AddStack will return both the original stack and the stack passed.
func (s Stack) AddStack(s2 Stack) (a, b Stack) {
	if s.Count() >= s.MaxCount() {
		// No more items could be added to the original stack.
		return s, s2
	}
	if !s.Comparable(s2) {
		// The items are not comparable and thus cannot be stacked together.
		return s, s2
	}
	diff := s.MaxCount() - s.Count()
	if s2.Count() < diff {
		diff = s2.Count()
	}

	s.count, s2.count = s.count+diff, s2.count-diff
	s.id, s2.id = newID(), newID()
	return s, s2
}

// Equal checks if the two stacks are equal. Equal is equivalent to a Stack.Comparable check while also
// checking the count and durability.
func (s Stack) Equal(s2 Stack) bool {
	return s.Comparable(s2) && s.count == s2.count && s.damage == s2.damage
}

// Comparable checks if two stacks can be considered comparable. True is returned if the two stacks have an
// equal item type and have equal enchantments, lore and custom names, or if one of the stacks is empty.
// Comparable does not check if the two stacks have the same durability.
func (s Stack) Comparable(s2 Stack) bool {
	if s.Empty() || s2.Empty() {
		return true
	}

	name, meta := s.Item().EncodeItem()
	name2, meta2 := s2.Item().EncodeItem()
	if name != name2 || meta != meta2 || s.anvilCost != s2.anvilCost || s.customName != s2.customName {
		return false
	}
	for !slices.Equal(s.lore, s2.lore) {
		return false
	}
	if len(s.enchantments) != len(s2.enchantments) {
		return false
	}
	for i := range s.enchantments {
		if s.enchantments[i] != s2.enchantments[i] {
			return false
		}
	}
	if !reflect.DeepEqual(s.data, s2.data) {
		return false
	}
	if nbt, ok := s.Item().(world.NBTer); ok {
		nbt2, ok := s2.Item().(world.NBTer)
		return ok && reflect.DeepEqual(nbt.EncodeNBT(), nbt2.EncodeNBT())
	}
	return true
}

// String implements the fmt.Stringer interface.
func (s Stack) String() string {
	if s.item == nil {
		return fmt.Sprintf("Stack<nil> x%v", s.count)
	}
	return fmt.Sprintf("Stack<%T%+v>(custom name='%v', lore='%v', damage=%v, anvilCost=%v) x%v", s.item, s.item, s.customName, s.lore, s.damage, s.anvilCost, s.count)
}

// Values returns all values associated with the stack by users. The map returned is a copy of the original:
// Modifying it will not modify the item stack.
func (s Stack) Values() map[string]any {
	if s.Empty() {
		return nil
	}
	return maps.Clone(s.data)
}

// cloneMap calls maps.Clone, but initialises m if it does not yet exist.
func cloneMap[M ~map[K]V, K comparable, V any](m M) M {
	if m == nil {
		m = make(M)
	}
	return maps.Clone(m)
}

// stackID is a counter for unique stack IDs.
var stackID = new(int32)

// newID returns a new unique stack ID.
func newID() int32 {
	return atomic.AddInt32(stackID, 1)
}

// id returns the unique ID of the stack passed.
// noinspection GoUnusedFunction
//
//lint:ignore U1000 Function is used through compiler directives.
func id(s Stack) int32 {
	if s.Empty() {
		return 0
	}
	return s.id
}

// format is a utility function to format a list of Values to have spaces between them, but no newline at the
// end, which is typically used for sending messages, popups and tips.
func format(a []any) string {
	return strings.TrimSuffix(fmt.Sprintln(a...), "\n")
}
