package inventory

import (
	"errors"
	"fmt"
	"github.com/df-mc/dragonfly/server/item"
	"math"
	"slices"
	"strings"
	"sync"
)

// Inventory represents an inventory containing items. These inventories may be carried by entities or may be
// held by blocks such as chests.
// The size of an inventory may be specified upon construction, but cannot be changed after. The zero value of
// an inventory is invalid. Use New() to obtain a new inventory.
// Inventory is safe for concurrent usage: Its values are protected by a mutex.
type Inventory struct {
	mu    sync.RWMutex
	h     Handler
	slots []item.Stack

	f         SlotFunc
	validator SlotValidatorFunc
}

// SlotFunc is a function called for each item changed in an Inventory.
type SlotFunc func(slot int, before, after item.Stack)

// SlotValidatorFunc is a function that limits changes in the Inventory slot.
type SlotValidatorFunc func(s item.Stack, slot int) bool

// ErrSlotOutOfRange is returned by any methods on inventory when a slot is passed which is not within the
// range of valid values for the inventory.
var ErrSlotOutOfRange = errors.New("slot is out of range: must be in range 0 <= slot < inventory.Size()")

// New creates a new inventory with the size passed. The inventory size cannot be changed after it has been
// constructed.
// A function may be passed which is called every time a slot is changed. The function may also be nil, if
// nothing needs to be done.
func New(size int, f SlotFunc) *Inventory {
	if size <= 0 {
		panic("inventory size must be at least 1")
	}
	if f == nil {
		f = func(slot int, before, after item.Stack) {}
	}
	return &Inventory{h: NopHandler{}, slots: make([]item.Stack, size), f: f, validator: func(s item.Stack, slot int) bool { return true }}
}

// Clone copies an Inventory and returns it, calling the SlotFunc passed for any
// slots changed in the new inventory.
func (inv *Inventory) Clone(f SlotFunc) *Inventory {
	if f == nil {
		f = func(slot int, before, after item.Stack) {}
	}
	return &Inventory{h: NopHandler{}, slots: inv.Slots(), f: f, validator: func(s item.Stack, slot int) bool { return true }}
}

// SlotFunc changes the function called when a slot in the inventory is changed.
func (inv *Inventory) SlotFunc(f SlotFunc) {
	inv.mu.Lock()
	defer inv.mu.Unlock()
	inv.f = f
}

// SlotValidatorFunc changes the function that limits item placement in the inventory slot.
func (inv *Inventory) SlotValidatorFunc(f SlotValidatorFunc) {
	inv.mu.Lock()
	defer inv.mu.Unlock()
	inv.validator = f
}

// Item attempts to obtain an item from a specific slot in the inventory. If an item was present in that slot,
// the item is returned and the error is nil. If no item was present in the slot, a Stack with air as its item
// and a count of 0 is returned. Stack.Empty() may be called to check if this is the case.
// Item only returns an error if the slot passed is out of range. (0 <= slot < inventory.Size())
func (inv *Inventory) Item(slot int) (item.Stack, error) {
	inv.mu.RLock()
	defer inv.mu.RUnlock()

	inv.check()
	if !inv.validSlot(slot) {
		return item.Stack{}, ErrSlotOutOfRange
	}
	return inv.slots[slot], nil
}

// SetItem sets a stack of items to a specific slot in the inventory. If an item is already present in the
// slot, that item will be overwritten.
// SetItem will return an error if the slot passed is out of range. (0 <= slot < inventory.Size())
func (inv *Inventory) SetItem(slot int, item item.Stack) error {
	inv.mu.Lock()

	inv.check()
	if !inv.validSlot(slot) {
		inv.mu.Unlock()
		return ErrSlotOutOfRange
	}
	f := inv.setItem(slot, item)

	inv.mu.Unlock()

	f()
	return nil
}

// Slots returns the all slots in the inventory as a slice. The index in the slice is the slot of the inventory that a
// specific item.Stack is in. Note that this item.Stack might be empty.
func (inv *Inventory) Slots() []item.Stack {
	inv.mu.RLock()
	defer inv.mu.RUnlock()
	return slices.Clone(inv.slots)
}

// Items returns a list of all contents of the inventory. This method excludes air items, so the method
// only ever returns item stacks which actually represent an item.
func (inv *Inventory) Items() []item.Stack {
	inv.mu.RLock()
	defer inv.mu.RUnlock()

	items := make([]item.Stack, 0, len(inv.slots))
	for _, it := range inv.slots {
		if !it.Empty() {
			items = append(items, it)
		}
	}
	return items
}

// First returns the first slot with an item if found. Second return value describes whether the item was found.
func (inv *Inventory) First(item item.Stack) (int, bool) {
	return inv.FirstFunc(item.Comparable)
}

// FirstFunc finds the first slot with an item.Stack that results in the comparable function passed returning true. The
// function returns false if no such item was found.
func (inv *Inventory) FirstFunc(comparable func(stack item.Stack) bool) (int, bool) {
	for slot, it := range inv.Slots() {
		if !it.Empty() && comparable(it) {
			return slot, true
		}
	}
	return -1, false
}

// FirstEmpty returns the first empty slot if found. Second return value describes whether an empty slot was found.
func (inv *Inventory) FirstEmpty() (int, bool) {
	for slot, it := range inv.Slots() {
		if it.Empty() {
			return slot, true
		}
	}
	return -1, false
}

// Swap swaps the items between two slots. Returns an error if either slot A or B are invalid.
func (inv *Inventory) Swap(slotA, slotB int) error {
	inv.mu.Lock()

	inv.check()
	if !inv.validSlot(slotA) || !inv.validSlot(slotB) {
		inv.mu.Unlock()
		return ErrSlotOutOfRange
	}
	a, b := inv.slots[slotA], inv.slots[slotB]
	fa, fb := inv.setItem(slotA, b), inv.setItem(slotB, a)

	inv.mu.Unlock()

	fa()
	fb()
	return nil
}

// AddItem attempts to add an item to the inventory. It does so in a couple of steps: It first iterates over
// the inventory to make sure no existing stacks of the same type exist. If these stacks do exist, the item
// added is first added on top of those stacks to make sure they are fully filled.
// If no existing stacks with leftover space are left, empty slots will be filled up with the remainder of the
// item added.
// If the item could not be fully added to the inventory, an error is returned along with the count that was
// added to the inventory.
func (inv *Inventory) AddItem(it item.Stack) (n int, err error) {
	if it.Empty() {
		return 0, nil
	}
	first := it.Count()
	emptySlots := make([]int, 0, 16)

	inv.mu.Lock()

	inv.check()
	for slot, invIt := range inv.slots {
		if invIt.Empty() {
			// This slot was empty, and we should first try to add the item stack to existing stacks.
			emptySlots = append(emptySlots, slot)
			continue
		}
		a, b := invIt.AddStack(it)
		if it.Count() == b.Count() {
			// Count stayed the same, meaning this slot either wasn't equal to this stack or was max size.
			continue
		}
		f := inv.setItem(slot, a)
		//noinspection GoDeferInLoop
		defer f()

		if it = b; it.Empty() {
			inv.mu.Unlock()
			// We were able to add the entire stack to existing stacks in the inventory.
			return first, nil
		}
	}
	for _, slot := range emptySlots {
		a, b := it.Grow(-math.MaxInt32).AddStack(it)

		f := inv.setItem(slot, a)
		//noinspection GoDeferInLoop
		defer f()

		if it = b; it.Empty() {
			inv.mu.Unlock()
			// We were able to add the entire stack to empty slots.
			return first, nil
		}
	}
	inv.mu.Unlock()
	// We were unable to clear out the entire stack to be added to the inventory: There wasn't enough space.
	return first - it.Count(), fmt.Errorf("could not add full item stack to inventory")
}

// RemoveItem attempts to remove an item from the inventory. It will visit all slots in the inventory and
// empties them until it.Count() items have been removed from the inventory.
// If less than it.Count() items were removed from the inventory, an error is returned.
func (inv *Inventory) RemoveItem(it item.Stack) error {
	return inv.RemoveItemFunc(it.Count(), it.Comparable)
}

// RemoveItemFunc removes up to n items from the Inventory. It will visit all slots in the inventory and empties them
// until n items have been removed from the inventory, assuming the comparable function returns true for the slots
// visited. No items will be deducted from slots if the comparable function returns false.
// If less than n items were removed, an error is returned.
func (inv *Inventory) RemoveItemFunc(n int, comparable func(stack item.Stack) bool) error {
	inv.mu.Lock()
	inv.check()
	for slot, slotIt := range inv.slots {
		if slotIt.Empty() || !comparable(slotIt) {
			continue
		}
		c := slotIt.Count() - n

		var f func()
		if c <= 0 {
			f = inv.setItem(slot, item.Stack{})
		} else {
			f = inv.setItem(slot, slotIt.Grow(-n))
		}

		//noinspection GoDeferInLoop
		defer f()

		if n -= slotIt.Count(); n <= 0 {
			break
		}
	}
	inv.mu.Unlock()

	if n > 0 {
		return fmt.Errorf("could not remove all items from the inventory")
	}
	return nil
}

// ContainsItem checks if the Inventory contains an item.Stack. It will visit all slots in the Inventory until it finds
// at enough items. If enough were found, true is returned.
func (inv *Inventory) ContainsItem(it item.Stack) bool {
	return inv.ContainsItemFunc(it.Count(), it.Comparable)
}

// ContainsItemFunc checks if the Inventory contains at least n items. It will visit all slots in the Inventory until it
// finds n items on which the comparable function returns true. ContainsItemFunc returns true if this is the case.
func (inv *Inventory) ContainsItemFunc(n int, comparable func(stack item.Stack) bool) bool {
	inv.mu.Lock()
	defer inv.mu.Unlock()

	inv.check()
	for _, slotIt := range inv.slots {
		if !slotIt.Empty() && comparable(slotIt) {
			if n -= slotIt.Count(); n <= 0 {
				break
			}
		}
	}
	return n <= 0
}

// Merge merges two inventories into one. The function passed is called for every slot change in the new inventory.
func (inv *Inventory) Merge(inv2 *Inventory, f func(int, item.Stack, item.Stack)) *Inventory {
	inv.mu.RLock()
	defer inv.mu.RUnlock()
	inv2.mu.RLock()
	defer inv2.mu.RUnlock()

	n := New(len(inv.slots)+len(inv2.slots), f)
	n.slots = make([]item.Stack, 0, len(inv.slots)+len(inv2.slots))
	n.slots = append(n.slots, inv.slots...)
	n.slots = append(n.slots, inv2.slots...)
	return n
}

// Empty checks if the inventory is fully empty: It iterates over the inventory and makes sure every stack in
// it is empty.
func (inv *Inventory) Empty() bool {
	inv.mu.RLock()
	defer inv.mu.RUnlock()

	inv.check()
	for _, it := range inv.slots {
		if !it.Empty() {
			return false
		}
	}
	return true
}

// Clear clears the entire inventory. All non-zero items are returned.
func (inv *Inventory) Clear() []item.Stack {
	inv.mu.Lock()

	inv.check()

	items := make([]item.Stack, 0, inv.size())
	for slot, i := range inv.slots {
		if !i.Empty() {
			items = append(items, i)
			f := inv.setItem(slot, item.Stack{})
			//noinspection GoDeferInLoop
			defer f()
		}
	}
	inv.mu.Unlock()

	return items
}

// Handle assigns a Handler to an Inventory so that its methods are called for the respective events. Nil may be passed
// to set the default NopHandler.
func (inv *Inventory) Handle(h Handler) {
	inv.mu.Lock()
	defer inv.mu.Unlock()

	inv.check()
	if h == nil {
		h = NopHandler{}
	}
	inv.h = h
}

// Handler returns the Handler currently assigned to the Inventory. This is the NopHandler by default.
func (inv *Inventory) Handler() Handler {
	inv.mu.RLock()
	defer inv.mu.RUnlock()

	inv.check()
	return inv.h
}

// setItem sets an item to a specific slot and overwrites the existing item. It calls the function which is
// called for every item change and does so without locking the inventory.
func (inv *Inventory) setItem(slot int, it item.Stack) func() {
	if !inv.validator(it, slot) {
		return func() {}
	}
	if it.Count() > it.MaxCount() {
		it = it.Grow(it.MaxCount() - it.Count())
	}
	before := inv.slots[slot]
	inv.slots[slot] = it
	return func() {
		inv.f(slot, before, it)
	}
}

// Size returns the size of the inventory. It is always the same value as that passed in the call to New() and
// is always at least 1.
func (inv *Inventory) Size() int {
	inv.mu.RLock()
	defer inv.mu.RUnlock()
	return inv.size()
}

// size returns the size of the inventory without locking.
func (inv *Inventory) size() int {
	return len(inv.slots)
}

// Close closes the inventory, freeing the function called for every slot change. It also clears any items
// that may currently be in the inventory.
// The returned error is always nil.
func (inv *Inventory) Close() error {
	inv.mu.Lock()
	defer inv.mu.Unlock()

	inv.check()
	inv.f = func(int, item.Stack, item.Stack) {}
	return nil
}

// String implements the fmt.Stringer interface.
func (inv *Inventory) String() string {
	inv.mu.RLock()
	defer inv.mu.RUnlock()

	s := make([]string, 0, inv.size())
	for _, it := range inv.slots {
		s = append(s, it.String())
	}
	return "(" + strings.Join(s, ", ") + ")"
}

// validSlot checks if the slot passed is valid for the inventory. It returns false if the slot is either
// smaller than 0 or bigger/equal to the size of the inventory's size.
func (inv *Inventory) validSlot(slot int) bool {
	return slot >= 0 && slot < inv.size()
}

// check panics if the inventory is valid, and panics if it is not. This typically happens if the inventory
// was not created using New().
func (inv *Inventory) check() {
	if inv.size() == 0 {
		panic("uninitialised inventory: inventory must be constructed using inventory.New()")
	}
}
