package com.earth2me.essentials.items;

import com.earth2me.essentials.ManagedFile;
import com.earth2me.essentials.utils.EnumUtil;
import com.earth2me.essentials.utils.MaterialUtil;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import net.ess3.api.IEssentials;
import org.bukkit.Material;
import org.bukkit.entity.EntityType;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.potion.PotionData;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static com.earth2me.essentials.I18n.tl;

public class FlatItemDb extends AbstractItemDb {
    protected static final Logger LOGGER = Logger.getLogger("Essentials");
    private static final Gson gson = new Gson();

    // Maps primary name to ItemData
    private final transient Map<String, ItemData> items = new HashMap<>();

    // Maps alias to primary name
    private final transient Map<String, String> itemAliases = new HashMap<>();

    // Every known alias
    private final transient Set<String> allAliases = new HashSet<>();

    private transient ManagedFile file = null;

    public FlatItemDb(final IEssentials ess) {
        super(ess);
    }

    @Override
    public void reloadConfig() {
        if (file == null) {
            file = new ManagedFile("items.json", ess);
        }

        this.rebuild();
        LOGGER.info(String.format("Loaded %s items from items.json.", listNames().size()));
    }

    private void rebuild() {
        this.reset();

        final String json = file.getLines().stream()
            .filter(line -> !line.startsWith("#"))
            .collect(Collectors.joining());

        this.loadJSON(String.join("\n", json));

        ready = true;
    }

    private void reset() {
        ready = false;
        items.clear();
        itemAliases.clear();
        allAliases.clear();
    }

    private void loadJSON(final String source) {
        final JsonObject map = new JsonParser().parse(source).getAsJsonObject();

        for (final Map.Entry<String, JsonElement> entry : map.entrySet()) {
            final String key = entry.getKey();
            final JsonElement element = entry.getValue();
            boolean valid = false;

            if (element.isJsonObject()) {
                final ItemData data = gson.fromJson(element, ItemData.class);
                items.put(key, data);
                valid = true;
            } else {
                try {
                    final String target = element.getAsString();
                    itemAliases.put(key, target);
                    valid = true;
                } catch (final Exception ignored) {
                }
            }

            if (valid) {
                allAliases.add(key);
            } else {
                LOGGER.warning(String.format("Failed to add item: \"%s\": %s", key, element.toString()));
            }
        }
    }

    @Override
    public ItemStack get(String id, final boolean useResolvers) throws Exception {
        if (useResolvers) {
            final ItemStack resolved = tryResolverDeserialize(id);
            if (resolved != null) {
                return resolved;
            }
        }

        id = id.toLowerCase();
        final String[] split = id.split(":");

        final ItemData data = getByName(split[0]);

        if (data == null) {
            throw new Exception(tl("unknownItemName", id));
        }

        final Material material = data.getMaterial();

        if (!material.isItem()) throw new Exception(tl("unableToSpawnItem", id));

        final ItemStack stack = new ItemStack(material);
        stack.setAmount(material.getMaxStackSize());

        final PotionData potionData = data.getPotionData();
        final ItemMeta meta = stack.getItemMeta();

        if (potionData != null && meta instanceof PotionMeta) {
            final PotionMeta potionMeta = (PotionMeta) meta;
            potionMeta.setBasePotionData(potionData);
        }

        // For some reason, Damageable doesn't extend ItemMeta but CB implements them in the same
        // class. As to why, your guess is as good as mine.
        if (split.length > 1 && meta instanceof Damageable) {
            final Damageable damageMeta = (Damageable) meta;
            damageMeta.setDamage(Integer.parseInt(split[1]));
        }

        stack.setItemMeta(meta);

        // The spawner provider will update the meta again, so we need to call it after
        // setItemMeta to prevent a race condition
        final EntityType entity = data.getEntity();
        if (entity != null && material.toString().contains("SPAWNER")) {
            ess.getSpawnerItemProvider().setEntityType(stack, entity);
        }

        return stack;
    }

    private ItemData getByName(String name) {
        name = name.toLowerCase();
        if (items.containsKey(name)) {
            return items.get(name);
        } else if (itemAliases.containsKey(name)) {
            return items.get(itemAliases.get(name));
        }

        return null;
    }

    @Override
    public List<String> nameList(final ItemStack item) {
        final List<String> names = new ArrayList<>();
        final String primaryName = name(item);
        names.add(primaryName);

        for (final Map.Entry<String, String> entry : itemAliases.entrySet()) {
            if (entry.getValue().equalsIgnoreCase(primaryName)) {
                names.add(entry.getKey());
            }
        }

        return names;
    }

    @Override
    public String name(final ItemStack item) {
        final ItemData data = lookup(item);

        for (final Map.Entry<String, ItemData> entry : items.entrySet()) {
            if (entry.getValue().equals(data)) {
                return entry.getKey();
            }
        }

        return null;
    }

    @Override
    @Deprecated
    public int getLegacyId(final Material material) {
        throw new UnsupportedOperationException("Legacy IDs aren't supported on this version.");
    }

    private ItemData lookup(final ItemStack item) {
        final Material type = item.getType();

        if (MaterialUtil.isPotion(type) && item.getItemMeta() instanceof PotionMeta) {
            final PotionData potion = ((PotionMeta) item.getItemMeta()).getBasePotionData();
            return new ItemData(type, potion);
        } else if (type.toString().contains("SPAWNER")) {
            final EntityType entity = ess.getSpawnerItemProvider().getEntityType(item);
            return new ItemData(type, entity);
        } else {
            return new ItemData(type);
        }
    }

    @Override
    public Collection<String> listNames() {
        final Set<String> names = new HashSet<>(allAliases);
        names.addAll(getResolverNames());
        return names;
    }

    public static class ItemData {
        private Material material;
        private String[] fallbacks = null;
        private PotionData potionData = null;
        private EntityType entity = null;

        ItemData(final Material material) {
            this.material = material;
        }

        ItemData(final Material material, final PotionData potionData) {
            this.material = material;
            this.potionData = potionData;
        }

        ItemData(final Material material, final EntityType entity) {
            this.material = material;
            this.entity = entity;
        }

        @Override
        public int hashCode() {
            return (31 * material.hashCode()) ^ potionData.hashCode();
        }

        @Override
        public boolean equals(final Object o) {
            if (o == null) {
                return false;
            }
            if (!(o instanceof ItemData)) {
                return false;
            }
            final ItemData that = (ItemData) o;
            return this.getMaterial() == that.getMaterial() && potionDataEquals(that) && entityEquals(that);
        }

        public Material getMaterial() {
            if (material == null && fallbacks != null) {
                material = EnumUtil.getMaterial(fallbacks);
                fallbacks = null; // If fallback fails, don't keep trying to look up fallbacks
            }

            return material;
        }

        public PotionData getPotionData() {
            return this.potionData;
        }

        public EntityType getEntity() {
            return this.entity;
        }

        private boolean potionDataEquals(final ItemData o) {
            if (this.potionData == null && o.getPotionData() == null) {
                return true;
            } else if (this.potionData != null && o.getPotionData() != null) {
                return this.potionData.equals(o.getPotionData());
            } else {
                return false;
            }
        }

        private boolean entityEquals(final ItemData o) {
            if (this.entity == null && o.getEntity() == null) { // neither have an entity
                return true;
            } else if (this.entity != null && o.getEntity() != null) { // both have an entity; check if it's the same one
                return this.entity.equals(o.getEntity());
            } else { // one has an entity but the other doesn't, so they can't be equal
                return false;
            }
        }
    }
}
