package tc.oc.commons.bukkit.inventory;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Table;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import tc.oc.commons.bukkit.item.ItemUtils;
import tc.oc.commons.core.util.Streams;
/**
* Derived from the names found in {@link net.minecraft.server.CommandReplaceItem}.
* If we ever implement applying kits to other types of inventories, this should be
* expanded to include those slot names as well.
*/
public abstract class Slot<I extends Inventory, H extends InventoryHolder> {
static {
all = new HashSet<>();
byKey = new HashMap<>();
byIndex = HashBasedTable.create();
byInventoryType = ImmutableMap.<Class<? extends Inventory>, Class<? extends Slot>>builder()
.put(PlayerInventory.class, Player.class)
.put(Inventory.class, Container.class)
.build();
Container.init();
Player.init();
EnderChest.init();
}
private static final Set<Slot> all;
private static final Map<String, Slot> byKey;
private static final Table<Class<? extends Slot>, Integer, Slot> byIndex;
private static final Map<Class<? extends Inventory>, Class<? extends Slot>> byInventoryType;
/**
* Convert a Mojang slot name (used by /replaceitem) to a {@link Slot} object.
* The "slot." at the beginning of the name is optional.
* Returns null if the name is invalid.
*/
public static @Nullable Slot forKey(String key) {
if(key.startsWith("slot.")) {
key = key.substring("slot.".length());
}
return byKey.get(key);
}
public static @Nullable <S extends Slot> S forIndex(Class<S> type, int index) {
return (S) byIndex.get(type, index);
}
public static @Nullable <S extends Slot> S atPosition(Class<S> type, int column, int row) {
return forIndex(type, row * 9 + column);
}
public static <I extends Inventory> Class<? extends Slot<I, ?>> typeForInventory(Class<I> inv) {
for(Map.Entry<Class<? extends Inventory>, Class<? extends Slot>> entry : byInventoryType.entrySet()) {
if(entry.getKey().isAssignableFrom(inv)) {
return (Class<? extends Slot<I, ?>>) entry.getValue();
}
}
throw new IllegalStateException("Weird inventory type " + inv);
}
public static <I extends Inventory> Stream<? extends Slot<I, ?>> forInventory(Class<I> invType) {
return Streams.instancesOf(all.stream(), typeForInventory(invType));
}
public static @Nullable <I extends Inventory> Slot<I, ?> forInventoryIndex(Class<I> inv, int index) {
return forIndex(typeForInventory(inv), index);
}
public static @Nullable Slot<?, ?> forViewIndex(InventoryView view, int rawIndex) {
final int cookedIndex = view.convertSlot(rawIndex);
return forInventoryIndex((rawIndex == cookedIndex ? view.getTopInventory()
: view.getBottomInventory()).getClass(),
cookedIndex);
}
private final @Nullable String key;
private final int index; // -1 = no slot
Slot(Class<? extends Slot> type, @Nullable String key, int index) {
this.key = key;
this.index = index;
all.add(this);
if(key != null) {
byKey.put(key, this);
}
if(index >= 0) {
byIndex.put(type, index, this);
}
}
@Override
public String toString() {
return key != null ? getKey() : super.toString();
}
/**
* @return the name of this slot, as used by the /replaceitem command
*/
public @Nullable String getKey() {
return key == null ? null : "slot." + key;
}
public boolean hasIndex() {
return index >= 0;
}
/**
* @return a slot index that can be passed to {@link Inventory#getItem} et al.
*/
public int getIndex() {
if(!hasIndex()) {
throw new UnsupportedOperationException("Slot " + this + " has no index");
}
return index;
}
public int getColumn() {
return getIndex() % 9;
}
public int getRow() {
return getIndex() / 9;
}
public int maxStackSize() {
return 64;
}
public int maxStackSize(Material material) {
return Math.min(maxStackSize(), material.getMaxStackSize());
}
public int maxStackSize(ItemStack item) {
return maxStackSize(item.getType());
}
public int maxTransferrableIn(ItemStack source) {
return Math.min(source.getAmount(), maxStackSize(source));
}
public int maxTransferrableIn(ItemStack source, I inv) {
final ItemStack dest = getItem(inv);
if(ItemUtils.isNothing(dest)) {
return maxTransferrableIn(source);
} else if(dest.isSimilar(source)) {
return Math.min(source.getAmount(), Math.max(0, maxStackSize(dest) - dest.getAmount()));
} else {
return 0;
}
}
public boolean isEquipment() {
return false;
}
public EquipmentSlot toEquipmentSlot() {
throw new UnsupportedOperationException("Slot " + this + " is not an equipment slot");
}
public I getInventory(H holder) {
return (I) holder.getInventory();
}
protected static @Nullable ItemStack airToNull(ItemStack stack) {
return stack == null || stack.getType() == Material.AIR ? null : stack;
}
public @Nullable ItemStack getItem(H holder) {
return getItem(getInventory(holder));
}
public Optional<ItemStack> item(H holder) {
return Optional.ofNullable(getItem(holder));
}
public void putItem(H holder, ItemStack stack) {
putItem(getInventory(holder), stack);
}
/**
* @return the item in this slot of the given holder's inventory, or null if the slot is empty.
* This will never return a stack of {@link Material#AIR}.
*/
public @Nullable ItemStack getItem(I inv) {
return airToNull(inv.getItem(getIndex()));
}
public Optional<ItemStack> item(I inv) {
return Optional.ofNullable(getItem(inv));
}
public int amount(I inv) {
return ItemUtils.amount(getItem(inv));
}
public boolean isEmpty(I inv) {
return amount(inv) == 0;
}
/**
* Put the given stack in this slot of the given holder's inventory.
*/
public void putItem(I inv, ItemStack stack) {
inv.setItem(getIndex(), airToNull(stack));
}
public static class Container extends Slot<Inventory, InventoryHolder> {
static void init() {
for(int i = 0; i < 54; i++) {
new Container("container." + i, i);
}
}
public static @Nullable Container forIndex(int index) {
return forIndex(Container.class, index);
}
Container(String key, int index) {
super(Container.class, key, index);
}
}
public static abstract class Player extends Slot<PlayerInventory, org.bukkit.entity.Player> {
static void init() {
Storage.init();
Equipment.init();
Cursor.init();
}
public static Stream<Player> player() {
return Streams.concat(Storage.storage(),
Equipment.equipment(),
Stream.of(Cursor.cursor()));
}
Player(String key, int index) {
super(Player.class, key, index);
}
public static @Nullable Player forIndex(int index) {
return forIndex(Player.class, index);
}
@Override
public @Nullable ItemStack getItem(PlayerInventory inv) {
return isEquipment() ? airToNull(inv.getItem(toEquipmentSlot()))
: super.getItem(inv);
}
@Override
public void putItem(PlayerInventory inv, ItemStack stack) {
if(isEquipment()) {
inv.setItem(toEquipmentSlot(), stack);
} else {
super.putItem(inv, stack);
}
}
}
public static class Storage extends Player {
static void init() {
Hotbar.init();
Pockets.init();
}
public static Stream<? extends Storage> storage() {
return Stream.concat(Hotbar.hotbar(), Pockets.pockets());
}
Storage(String key, int index) {
super(key, index);
}
}
// TODO: make row/column correct for Hotbar and Pockets
public static class Hotbar extends Storage {
static void init() {
hotbar = new Hotbar[9];
for(int i = 0; i < 9; i++) {
hotbar[i] = new Hotbar("hotbar." + i, i);
}
}
private static Hotbar[] hotbar;
public static Stream<? extends Hotbar> hotbar() {
return Stream.of(hotbar);
}
public static Hotbar forPosition(int pos) {
return (Hotbar) forIndex(pos);
}
protected Hotbar(String key, int index) {
super(key, index);
}
}
public static class Pockets extends Storage {
static void init() {
pockets = new Pockets[27];
for(int i = 0; i < 27; i++) {
pockets[i] = new Pockets("inventory." + i, 9 + i);
}
MainHand.init();
}
private static Pockets[] pockets;
public static Stream<? extends Pockets> pockets() {
return Stream.of(pockets);
}
protected Pockets(String key, int index) {
super(key, index);
}
}
public abstract static class Equipment extends Player {
static void init() {
OffHand.init();
Armor.init();
}
public static Stream<? extends Equipment> equipment() {
return Stream.concat(Stream.of(OffHand.offHand()), Armor.armor());
}
private final EquipmentSlot equipmentSlot;
Equipment(String key, int index, EquipmentSlot equipmentSlot) {
super(key, index);
this.equipmentSlot = equipmentSlot;
}
@Override
public int maxStackSize() {
return 1;
}
@Override
public boolean isEquipment() {
return true;
}
@Override
public EquipmentSlot toEquipmentSlot() {
return equipmentSlot;
}
}
public static class MainHand extends Hotbar {
static void init() {
mainHand = new MainHand();
}
private static MainHand mainHand;
public static MainHand mainHand() { return mainHand; }
MainHand() {
super("weapon.mainhand", -1);
}
@Override
public boolean isEquipment() {
return true;
}
@Override
public EquipmentSlot toEquipmentSlot() {
return EquipmentSlot.HAND;
}
}
public static class OffHand extends Equipment {
static void init() {
offHand = new OffHand();
}
private static OffHand offHand;
public static OffHand offHand() { return offHand; }
protected OffHand() {
super("weapon.offhand", 40, EquipmentSlot.OFF_HAND);
}
}
public static class Armor extends Equipment {
static void init() {
new Armor("armor.feet", EquipmentSlot.FEET, ArmorType.BOOTS);
new Armor("armor.legs", EquipmentSlot.LEGS, ArmorType.LEGGINGS);
new Armor("armor.chest", EquipmentSlot.CHEST, ArmorType.CHESTPLATE);
new Armor("armor.head", EquipmentSlot.HEAD, ArmorType.HELMET);
}
private static final Map<ArmorType, Armor> byArmorType = new EnumMap<>(ArmorType.class);
public static Stream<? extends Armor> armor() {
return byArmorType.values().stream();
}
private final ArmorType armorType;
Armor(String key, EquipmentSlot equipmentSlot, ArmorType armorType) {
super(key, armorType.inventorySlot(), equipmentSlot);
this.armorType = armorType;
byArmorType.put(armorType, this);
}
public ArmorType getArmorType() {
return armorType;
}
public static Armor forType(ArmorType armorType) {
return byArmorType.get(armorType);
}
}
public static class EnderChest extends Slot<Inventory, org.bukkit.entity.Player> {
static void init() {
for(int i = 0; i < 27; i++) {
new EnderChest("enderchest." + i, i);
}
}
EnderChest(String key, int index) {
super(EnderChest.class, key, index);
}
@Override
public Inventory getInventory(org.bukkit.entity.Player holder) {
return holder.getEnderChest();
}
}
public static class Cursor extends Player {
static void init() {
cursor = new Cursor();
}
private static Cursor cursor;
public static Cursor cursor() { return cursor; }
Cursor() {
super(null, -1);
}
@Override
public String toString() {
return "cursor";
}
@Override
public @Nullable ItemStack getItem(org.bukkit.entity.Player holder) {
return airToNull(holder.getItemOnCursor());
}
@Override
public void putItem(org.bukkit.entity.Player holder, @Nullable ItemStack stack) {
holder.setItemOnCursor(stack);
}
@Override
public @Nullable ItemStack getItem(PlayerInventory inv) {
return getItem((org.bukkit.entity.Player) inv.getHolder());
}
@Override
public void putItem(PlayerInventory inv, ItemStack stack) {
putItem((org.bukkit.entity.Player) inv.getHolder(), stack);
}
}
}