package com.zivilon.cinder_loe.droptables;

import com.zivilon.cinder_loe.util.ILootableEntity;
import com.zivilon.cinder_loe.util.Utilities;
import com.zivilon.cinder_loe.CinderLoE;

import net.minecraft.nbt.*;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;

import cpw.mods.fml.common.registry.FMLControlledNamespacedRegistry;

import lotr.common.entity.npc.LOTREntityNPC;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

public class DropTable {
    public static Random random = new Random();
    public List<DropInstance> drop_list;

    public DropTable() {
        drop_list = new ArrayList<DropInstance>();
    }

    public static void drop_items(LOTREntityNPC entity, DropContext[] context_flags, int looting_level) {
        List<ItemStack> drops = generate_drops(entity, context_flags, looting_level);
        if (drops == null || drops.size() < 1) return;
        for (ItemStack drop : drops) {
            entity.npcDropItem(drop, 0.0F, false, false);
        }
    }

    public static List<ItemStack> generate_drops(LOTREntityNPC entity, DropContext[] context_flags, int looting_level) {
        DropTable table = ((ILootableEntity)entity).get_drop_table();
        if (table == null) {
            return null;
        }
        if (context_flags == null) {
            context_flags = new DropContext[0];
        }
        List<ItemStack> results = new ArrayList<>();
        for (DropInstance drop : table.drop_list) {
            ItemStack stack = get_drop(drop, looting_level, context_flags);
            if (stack != null) {
                results.add(stack);
            }
        }
        return results;
    }

    public static ItemStack get_drop(DropInstance drop, int looting_level, DropContext[] context) {
        if (!(Utilities.array_contains_array(context, drop.conditions))) return null;
        if (drop instanceof SingleItemDrop) {
            return get_single_drop((SingleItemDrop)drop, looting_level);
        }
        if (drop instanceof ItemGroup) {
            return get_group_drop((ItemGroup)drop, looting_level);
        }
        return null;
    }

    public static ItemStack get_single_drop(SingleItemDrop drop, int looting_level) {
        float chance = drop.drop_chance;
        if (drop.looting_affects_chance)
            chance = 1.0F - (float)Math.pow(1.0F - chance, 1.0F + 0.1F * looting_level);
        if (chance < random.nextFloat()) return null;

        int range = drop.max_amount - drop.min_amount + 1;
        if (drop.looting_affects_quantity && looting_level > 0)
            range = (int)Math.ceil(range * (1.25F * looting_level));
        int count = random.nextInt(range) + drop.min_amount;
        ItemStack stack = new ItemStack(drop.item, count);
        if (drop.nbt != null)
            stack.setTagCompound((NBTTagCompound)drop.nbt.copy());
        return stack;
    }

    public static ItemStack get_group_drop(ItemGroup drop, int looting_level) {
        float chance = drop.drop_chance;
        if (drop.looting_affects_chance)
            chance = 1.0F - (float)Math.pow(1.0F - chance, 1.0F + 0.1F * looting_level);
        if (chance < random.nextFloat()) return null;

        int total_weight = drop.entries.stream().mapToInt(e -> e.weight).sum();
        int pick = random.nextInt(total_weight);
        for (ItemGroupEntry entry : drop.entries) {
            if (pick < entry.weight) {
                int range = entry.max_amount - entry.min_amount + 1;
                if (entry.looting_affects_quantity && looting_level > 0)
                    range = (int)Math.ceil(range * (1.25F * looting_level));
                int count = random.nextInt(range) + entry.min_amount;
                ItemStack stack = new ItemStack(entry.item, count);
                if (entry.nbt != null)
                    stack.setTagCompound((NBTTagCompound)entry.nbt.copy());
                return stack;
            }
            pick -= entry.weight;
        }
        return null;
    }

    public static NBTTagCompound serialize_to_nbt(DropTable table) {
        NBTTagCompound nbt = new NBTTagCompound();
        NBTTagList drop_list = new NBTTagList();
        for (DropInstance drop : table.drop_list) {
            NBTTagCompound drop_tag = new NBTTagCompound();
            drop_tag.setString("type", drop instanceof ItemGroup ? "group" : "single");
            drop_tag.setFloat("drop_chance", drop.drop_chance);
            drop_tag.setInteger("min_amount", drop.min_amount);
            drop_tag.setInteger("max_amount", drop.max_amount);
            drop_tag.setBoolean("looting_affects_chance", drop.looting_affects_chance);
            NBTTagList condition_list = new NBTTagList();
            if (drop instanceof SingleItemDrop) {
                SingleItemDrop single = (SingleItemDrop)drop;
                Item drop_item = single.item;
                drop_tag.setBoolean("looting_affects_quantity", single.looting_affects_quantity);
                drop_tag.setInteger("id", ((FMLControlledNamespacedRegistry)Item.itemRegistry).getId(drop_item));
                if (single.nbt != null)
                     drop_tag.setTag("nbt", single.nbt);
            }
            if (drop instanceof ItemGroup) {
                ItemGroup group = (ItemGroup) drop;
                NBTTagList entries = new NBTTagList();
                for (ItemGroupEntry entry : group.entries) {
                    NBTTagCompound entry_tag = new NBTTagCompound();
                    entry_tag.setInteger("id", Item.getIdFromItem(entry.item));
                    entry_tag.setInteger("weight", entry.weight);
                    entry_tag.setInteger("min", entry.min_amount);
                    entry_tag.setInteger("max", entry.max_amount);
                    entry_tag.setBoolean("looting_quantity", entry.looting_affects_quantity);
                    if (entry.nbt != null) {
                        entry_tag.setTag("nbt", entry.nbt.copy());
                    }
                    entries.appendTag(entry_tag);
                }
                drop_tag.setTag("entries", entries);
            }
            for (DropContext condition : drop.conditions) {
                condition_list.appendTag(new NBTTagString(condition.name()));
            }
            drop_tag.setTag("conditions", condition_list);
            drop_list.appendTag(drop_tag);
            nbt.setTag("Drops", drop_list);
        }
        return nbt;
    }

    public static DropTable deserialize_from_nbt(NBTTagCompound tag) {
        DropTable table = new DropTable();

        if (!tag.hasKey("Drops")) return table;

        NBTTagList drop_list = tag.getTagList("Drops", 10);

        for (int i = 0; i < drop_list.tagCount(); i++) {
            NBTTagCompound drop_tag = drop_list.getCompoundTagAt(i);
            String type = drop_tag.getString("type");

            float drop_chance = drop_tag.getFloat("drop_chance");
            int min = drop_tag.getInteger("min_amount");
            int max = drop_tag.getInteger("max_amount");
            boolean looting_chance = drop_tag.getBoolean("looting_affects_chance");

            // Read conditions
            DropContext[] conditions;
            if (drop_tag.hasKey("conditions")) {
                NBTTagList cond_list = drop_tag.getTagList("conditions", 8);
                conditions = new DropContext[cond_list.tagCount()];
                for (int j = 0; j < cond_list.tagCount(); j++) {
                    String ctx = cond_list.getStringTagAt(j);
                    conditions[j] = DropContext.valueOf(ctx);
                }
            } else {
                conditions = new DropContext[0];
            }

            // Construct drop
            if (type.equals("single")) {
                Item item = Item.getItemById(drop_tag.getInteger("id"));
                boolean looting_quantity = drop_tag.hasKey("looting_affects_quantity") && drop_tag.getBoolean("looting_affects_quantity");
                NBTTagCompound nbt = drop_tag.hasKey("nbt") ? drop_tag.getCompoundTag("nbt") : null;
                SingleItemDrop single = new SingleItemDrop(item, nbt, drop_chance, min, max, looting_quantity, looting_chance, conditions);
                if (single != null) table.drop_list.add(single);
                if (single == null) System.out.println("[DropTable_deserializer] WARNING: Single drop was null!");
            } else if (type.equals("group")) {
                List<ItemGroupEntry> entries = new ArrayList<>();
                if (drop_tag.hasKey("entries")) {
                    NBTTagList entry_list = drop_tag.getTagList("entries", 10); // 10 = compound
                    for (int j = 0; j < entry_list.tagCount(); j++) {
                        NBTTagCompound entry_tag = entry_list.getCompoundTagAt(j);
                        Item item = Item.getItemById(entry_tag.getInteger("id"));
                        int weight = entry_tag.getInteger("weight");
                        int minAmt = entry_tag.getInteger("min");
                        int maxAmt = entry_tag.getInteger("max");
                        boolean lootQty = entry_tag.getBoolean("looting_quantity");
                        NBTTagCompound nbt = entry_tag.hasKey("nbt") ? entry_tag.getCompoundTag("nbt") : null;
                        entries.add(new ItemGroupEntry(item, nbt, weight, minAmt, maxAmt, lootQty));
                    }
                }
                ItemGroup group = new ItemGroup(drop_chance, min, max, looting_chance, conditions, entries.toArray(new ItemGroupEntry[0]));
                if (group != null) table.drop_list.add(group);
                if (group == null) System.out.println("[DropTable_deserializer] WARNING: Group drop was null!");
            }
        }

        return table;
    }


    public static class DropInstance {
        public float drop_chance;
        public int min_amount;
        public int max_amount;
        public boolean looting_affects_chance;
        public DropContext[] conditions;

        public DropInstance(float drop_chance, int min_amount, int max_amount, boolean looting_chance, DropContext[] conditions) {
            this.drop_chance = drop_chance;
            this.min_amount = min_amount;
            this.max_amount = max_amount;
            this.looting_affects_chance = looting_chance;
            this.conditions = conditions;
        }
    }
    public static class SingleItemDrop extends DropInstance {
        public Item item;
        public NBTTagCompound nbt;
        boolean looting_affects_quantity;

        public SingleItemDrop(Item item, NBTTagCompound nbt, float chance, int min, int max, boolean looting_quantity, boolean looting_chance, DropContext[] conditions) {
            super(chance, min, max, looting_chance, conditions);
            this.item = item;
            this.nbt = nbt;
            this.looting_affects_quantity = looting_quantity;
        }
        public SingleItemDrop(Item item, NBTTagCompound nbt, float chance, DropContext[] conditions) {
            super(chance, 1, 1, false, conditions);
            this.item = item;
            this.nbt = nbt;
            this.looting_affects_quantity = false;
        }
    }
    public static class ItemGroup extends DropInstance {
        public List<ItemGroupEntry> entries;

        public ItemGroup(float chance, int min, int max, boolean looting_chance, DropContext[] conditions, ItemGroupEntry... drops) {
            super(chance, min, max, looting_chance, conditions);
            this.entries = Arrays.asList(drops);
        }
    }

    public static class ItemGroupEntry {
        public Item item;
        public int min_amount;
        public int max_amount;
        public NBTTagCompound nbt;
        public int weight;
        public boolean looting_affects_quantity;

        public ItemGroupEntry(Item item, NBTTagCompound nbt, int weight, int min_amount, int max_amount, boolean looting_quantity) {
            this.item = item;
            this.nbt = nbt;
            this.weight = weight;
            this.min_amount = min_amount;
            this.max_amount = max_amount;
            this.looting_affects_quantity = looting_quantity;
        }
        public ItemGroupEntry(Item item, NBTTagCompound nbt, int weight) {
            this.item = item;
            this.nbt = nbt;
            this.weight = weight;
            this.min_amount = 1;
            this.max_amount = 1;
            this.looting_affects_quantity = false;
        }
    }
}
