package tc.oc.pgm.crafting;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.Nullable;

import com.google.common.collect.ImmutableSet;
import org.bukkit.inventory.FurnaceRecipe;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Recipe;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.ShapelessRecipe;
import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import tc.oc.pgm.kits.ItemParser;
import tc.oc.pgm.map.MapModule;
import tc.oc.pgm.map.MapModuleContext;
import tc.oc.pgm.map.MapModuleFactory;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModuleFactory;
import tc.oc.pgm.module.ModuleDescription;
import tc.oc.pgm.module.ModuleLoadException;
import tc.oc.pgm.utils.MaterialPattern;
import tc.oc.pgm.utils.XMLUtils;
import tc.oc.pgm.xml.InvalidXMLException;

@ModuleDescription(name = "Crafting")
public class CraftingModule implements MapModule, MatchModuleFactory<CraftingMatchModule> {

    private final Set<Recipe> customRecipes;
    private final Set<MaterialPattern> disabledRecipes;

    public CraftingModule(Set<Recipe> customRecipes, Set<MaterialPattern> disabledRecipes) {
        this.customRecipes = ImmutableSet.copyOf(customRecipes);
        this.disabledRecipes = ImmutableSet.copyOf(disabledRecipes);
    }

    @Override
    public CraftingMatchModule createMatchModule(Match match) throws ModuleLoadException {
        return new CraftingMatchModule(match, customRecipes, disabledRecipes);
    }

    public static class Factory extends MapModuleFactory<CraftingModule> {
        @Override
        public @Nullable CraftingModule parse(MapModuleContext context, Logger logger, Document doc) throws InvalidXMLException {
            Set<Recipe> customRecipes = new HashSet<>();
            Set<MaterialPattern> disabledRecipes = new HashSet<>();

            for(Element elCrafting : doc.getRootElement().getChildren("crafting")) {
                for(Element elDisable : elCrafting.getChildren("disable")) {
                    disabledRecipes.add(XMLUtils.parseMaterialPattern(elDisable));
                }

                for(Element elRecipe : XMLUtils.getChildren(elCrafting, "shapeless", "shaped", "smelt")) {
                    Recipe recipe;
                    switch(elRecipe.getName()) {
                        case "shapeless":
                            recipe = parseShapelessRecipe(context, elRecipe);
                            break;

                        case "shaped":
                            recipe = parseShapedRecipe(context, elRecipe);
                            break;

                        case "smelt":
                            recipe = parseSmeltingRecipe(context, elRecipe);
                            break;

                        default: throw new IllegalStateException();
                    }

                    customRecipes.add(recipe);
                    if(XMLUtils.parseBoolean(elRecipe.getAttribute("override"), false)) {
                        // Disable specific material + data
                        disabledRecipes.add(new MaterialPattern(recipe.getResult().getData()));
                    } else if(XMLUtils.parseBoolean(elRecipe.getAttribute("override-all"), false)) {
                        // Disable all of this material
                        disabledRecipes.add(new MaterialPattern(recipe.getResult().getType()));
                    }
                }
            }

            return customRecipes.isEmpty() && disabledRecipes.isEmpty() ? null : new CraftingModule(customRecipes, disabledRecipes);
        }

        private ItemStack parseRecipeResult(MapModuleContext context, Element elRecipe) throws InvalidXMLException {
            return context.needModule(ItemParser.class)
                          .parseItem(XMLUtils.getRequiredUniqueChild(elRecipe, "result"), false)
                          .immutableCopy();
        }

        public Recipe parseShapelessRecipe(MapModuleContext context, Element elRecipe) throws InvalidXMLException {
            ShapelessRecipe recipe = new ShapelessRecipe(parseRecipeResult(context, elRecipe));

            for(Element elIngredient : XMLUtils.getChildren(elRecipe, "ingredient", "i")) {
                MaterialPattern item = XMLUtils.parseMaterialPattern(elIngredient);
                int count = XMLUtils.parseNumber(elIngredient.getAttribute("amount"), Integer.class, 1);
                if(item.dataMatters()) {
                    recipe.addIngredient(count, item.getMaterialData());
                } else {
                    recipe.addIngredient(count, item.getMaterial());
                }
            }

            if(recipe.getIngredientList().isEmpty()) {
                throw new InvalidXMLException("Crafting recipe must have at least one ingredient", elRecipe);
            }

            return recipe;
        }

        public Recipe parseShapedRecipe(MapModuleContext context, Element elRecipe) throws InvalidXMLException {
            ShapedRecipe recipe = new ShapedRecipe(parseRecipeResult(context, elRecipe));

            Element elShape = XMLUtils.getRequiredUniqueChild(elRecipe, "shape");
            List<String> rows = new ArrayList<>(3);

            for(Element elRow : elShape.getChildren("row")) {
                String row = elRow.getTextNormalize();

                if(rows.size() >= 3) {
                    throw new InvalidXMLException("Shape must have no more than 3 rows (" + row + ")", elShape);
                }

                if(rows.isEmpty()) {
                    if(row.length() > 3) {
                        throw new InvalidXMLException("Shape must have no more than 3 columns (" + row + ")", elShape);
                    }
                } else if(row.length() != rows.get(0).length()) {
                    throw new InvalidXMLException("All rows must be the same width", elShape);
                }

                rows.add(row);
            }

            if(rows.isEmpty()) {
                throw new InvalidXMLException("Shape must have at least one row", elShape);
            }

            recipe.shape(rows.toArray(new String[rows.size()]));
            Set<Character> keys = recipe.getIngredientMap().keySet(); // All shape symbols are present and mapped to null at this point

            for(Element elIngredient : elRecipe.getChildren("ingredient")) {
                MaterialPattern item = XMLUtils.parseMaterialPattern(elIngredient);
                Attribute attrSymbol = XMLUtils.getRequiredAttribute(elIngredient, "symbol");
                String symbol = attrSymbol.getValue();

                if(symbol.length() != 1) {
                    throw new InvalidXMLException("Ingredient key must be a single character from the recipe shape", attrSymbol);
                }

                char key = symbol.charAt(0);
                if(!keys.contains(key)) {
                    throw new InvalidXMLException("Ingredient key '" + key + "' does not appear in the recipe shape", attrSymbol);
                }

                if(item.dataMatters()) {
                    recipe.setIngredient(key, item.getMaterialData());
                } else {
                    recipe.setIngredient(key, item.getMaterial());
                }
            }

            if(recipe.getIngredientMap().isEmpty()) {
                throw new InvalidXMLException("Crafting recipe must have at least one ingredient", elRecipe);
            }

            return recipe;
        }

        public Recipe parseSmeltingRecipe(MapModuleContext context, Element elRecipe) throws InvalidXMLException {
            MaterialPattern ingredient = XMLUtils.parseMaterialPattern(XMLUtils.getRequiredUniqueChild(elRecipe, "ingredient", "i"));
            ItemStack result = parseRecipeResult(context, elRecipe);
            if(ingredient.dataMatters()) {
                return new FurnaceRecipe(result, ingredient.getMaterialData());
            } else {
                return new FurnaceRecipe(result, ingredient.getMaterial());
            }
        }
    }
}
