package tc.oc.pgm.utils;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.inject.Inject;

import com.google.common.base.Splitter;
import com.google.common.collect.BoundType;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;
import com.google.gson.JsonParseException;
import gnu.trove.list.TIntList;
import gnu.trove.list.array.TIntArrayList;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import org.apache.commons.codec.binary.Base64;
import org.bukkit.Bukkit;
import org.bukkit.DyeColor;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.material.MaterialData;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scoreboard.Team;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
import org.jdom2.Attribute;
import org.jdom2.Element;
import tc.oc.api.docs.SemanticVersion;
import tc.oc.commons.bukkit.inventory.Slot;
import tc.oc.commons.bukkit.localization.Translations;
import tc.oc.commons.bukkit.util.BukkitUtils;
import tc.oc.commons.bukkit.util.NMSHacks;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.chat.Components;
import tc.oc.commons.core.util.ArrayUtils;
import tc.oc.commons.core.util.NumberFactory;
import tc.oc.commons.core.util.Pair;
import tc.oc.commons.core.util.TimeUtils;
import tc.oc.pgm.xml.BoundedElement;
import tc.oc.pgm.xml.ElementFlattener;
import tc.oc.pgm.xml.InvalidXMLException;
import tc.oc.pgm.xml.Node;
import tc.oc.pgm.xml.parser.BooleanParser;
import tc.oc.pgm.xml.parser.NumberParser;
import tc.oc.pgm.xml.parser.PrimitiveParser;
import tc.oc.pgm.xml.parser.StringParser;
import tc.oc.pgm.xml.parser.TeamRelationParser;
import tc.oc.pgm.xml.parser.VectorParser;
import tc.oc.pgm.xml.property.DurationProperty;
import tc.oc.pgm.xml.property.NumberProperty;
import tc.oc.pgm.xml.property.PropertyBuilder;
import tc.oc.pgm.xml.property.PropertyBuilderFactory;

public class XMLUtils {

    @Inject private static PropertyBuilderFactory<Duration, DurationProperty> durationFactory;
    @Inject private static PrimitiveParser<org.bukkit.attribute.Attribute> attributeParser;

    /**
     * @param parentTagNames       Names of elements deeper than the root that can be traversed
     * @param childTagNames        Names of elements that can be included in the result
     * @param minChildDepth        Minimum depth of elements that can be in the result, relative to the children of root
     */
    public static List<Element> flattenElements(Element root, Set<String> parentTagNames, @Nullable Set<String> childTagNames, int minChildDepth) {
        return new ElementFlattener(parentTagNames, childTagNames, minChildDepth)
            .flattenChildren(root)
            .collect(Collectors.toList());
    }

    public static List<Element> flattenElements(Element root, Set<String> parentTagNames, @Nullable Set<String> childTagNames) {
        return flattenElements(root, parentTagNames, childTagNames, 1);
    }

    public static List<Element> flattenElements(Element root, Set<String> parentTagNames) {
        return flattenElements(root, parentTagNames, null);
    }

    public static List<Element> flattenElements(Element root, String parentTagName, @Nullable String childTagName, int minChildDepth) {
        return flattenElements(root, ImmutableSet.of(parentTagName), childTagName == null ? null : ImmutableSet.of(childTagName), minChildDepth);
    }

    public static List<Element> flattenElements(Element root, String parentTagName, @Nullable String childTagName) {
        return flattenElements(root, parentTagName, childTagName, 1);
    }

    public static List<Element> flattenElements(Element root, String parentTagName) {
        return flattenElements(root, parentTagName, null);
    }

    public static Iterable<Element> getChildren(Element parent, Collection<String> names) {
        return Iterables.filter(parent.getChildren(), el -> names.contains(el.getName()));
    }

    public static Iterable<Element> getChildren(Element parent, String...names) {
        return getChildren(parent, Arrays.asList(names));
    }

    public static @Nullable Attribute getAttribute(Element parent, Collection<String> names) {
        for(String name : names) {
            final Attribute attr = parent.getAttribute(name);
            if(attr != null) return attr;
        }
        return null;
    }

    public static @Nullable Attribute getAttribute(Element parent, String...names) {
        return getAttribute(parent, Arrays.asList(names));
    }

    public static Iterable<Attribute> getAttributes(Element parent, Collection<String> names) {
        return Iterables.filter(parent.getAttributes(), attr -> names.contains(attr.getName()));
    }

    public static Iterable<Attribute> getAttributes(Element parent, String...names) {
        return getAttributes(parent, Arrays.asList(names));
    }

    public static @Nullable Element getUniqueChild(Element parent, String name, String...aliases) throws InvalidXMLException {
        return getUniqueChild(parent, name, ImmutableSet.copyOf(aliases));
    }

    public static @Nullable Element getUniqueChild(Element parent, String name, Set<String> aliases) throws InvalidXMLException {
        List<Element> children = new ArrayList<>();
        for(String alias : Sets.union(ImmutableSet.of(name), aliases)) {
            children.addAll(parent.getChildren(alias));
        }

        if(children.size() > 1) {
            throw new InvalidXMLException("multiple '" + name + "' tags not allowed", parent);
        }
        return children.isEmpty() ? null : children.get(0);
    }

    public static Element getRequiredUniqueChild(Element parent, String name, String...aliases) throws InvalidXMLException {
        aliases = ArrayUtils.append(aliases, name);

        List<Element> children = new ArrayList<>();
        for(String alias : aliases) {
            children.addAll(parent.getChildren(alias));
        }

        if(children.size() > 1) {
            throw new InvalidXMLException("multiple '" + name + "' tags not allowed", parent);
        } else if (children.isEmpty()) {
            throw new InvalidXMLException("child tag '" + name + "' is required", parent);
        }
        return children.get(0);
    }

    public static Element getRequiredUniqueChild(Element parent) throws InvalidXMLException {
        if(parent.getChildren().size() > 1) {
            throw new InvalidXMLException("multiple elements not allowed", parent);
        } else if (parent.getChildren().isEmpty()) {
            throw new InvalidXMLException("child element is required", parent);
        }
        return parent.getChildren().get(0);
    }

    public static Attribute getRequiredAttribute(Element el, String name, String...aliases) throws InvalidXMLException {
        aliases = ArrayUtils.append(aliases, name);

        Attribute attr = null;
        for(String alias : aliases) {
            Attribute a = el.getAttribute(alias);
            if(a != null) {
                if(attr == null) {
                    attr = a;
                } else {
                    throw new InvalidXMLException("attributes '" + attr.getName() + "' and '" + alias + "' are aliases for the same thing, and cannot be combined", el);
                }
            }
        }

        if(attr == null) {
            throw new InvalidXMLException("attribute '" + name + "' is required", el);
        }

        return attr;
    }

    public static PropertyBuilder<Boolean, ?> parseBoolean(Element parent, String name) {
        return new PropertyBuilder<>(parent, name, BooleanParser.get());
    }

    private static Boolean parseBoolean(Node node, String value) throws InvalidXMLException {
        return BooleanParser.get().parse(node, value);
    }

    public static Boolean parseBoolean(Node node) throws InvalidXMLException {
        return node == null ? null : parseBoolean(node, node.getValue());
    }

    public static Boolean parseBoolean(@Nullable Node node, Boolean def) throws InvalidXMLException {
        return node == null ? def : parseBoolean(node);
    }

    public static Boolean parseBoolean(@Nullable Element el, Boolean def) throws InvalidXMLException {
        return el == null ? def : parseBoolean(new Node(el));
    }

    public static Boolean parseBoolean(@Nullable Attribute attr, Boolean def) throws InvalidXMLException {
        return attr == null ? def : parseBoolean(new Node(attr));
    }

    public static Optional<Boolean> parseOptionalBoolean(@Nullable Node node) throws InvalidXMLException {
        return node == null ? Optional.empty() : Optional.of(parseBoolean(node));
    }

    public static <T extends Number & Comparable<T>> NumberProperty<T> parseNumber(Element parent, String name, Class<T> type) {
        return new NumberProperty<>(parent, name, type);
    }

    public static <T extends Number> T parseNumber(Node node, String text, Class<T> type, boolean infinity) throws InvalidXMLException {
        final T value = NumberParser.get(type).parse(node, text);
        if(!infinity && !NumberFactory.get(type).isFinite(value)) {
            throw new InvalidXMLException("Number must be finite", node);
        }
        return value;
    }

    public static <T extends Number> Optional<T> parseOptionalNumber(@Nullable Node node, Class<T> type) throws InvalidXMLException {
        return Optional.ofNullable(parseNumber(node, type, null));
    }

    public static <T extends Number> T parseNumber(Node node, String text, Class<T> type, boolean infinity, T def) throws InvalidXMLException {
        return node == null ? def : parseNumber(node, text, type, infinity);
    }

    public static <T extends Number> T parseNumber(Node node, String text, Class<T> type, T def) throws InvalidXMLException {
        return parseNumber(node, text, type, false, def);
    }

    public static <T extends Number> T parseNumber(Node node, String text, Class<T> type) throws InvalidXMLException {
        return parseNumber(node, text, type, false);
    }

    public static <T extends Number> T parseNumber(Node node, Class<T> type, boolean infinity) throws InvalidXMLException {
        return parseNumber(node, node.getValue(), type, infinity);
    }

    public static <T extends Number> T parseNumber(Node node, Class<T> type) throws InvalidXMLException {
        return parseNumber(node, node.getValue(), type);
    }

    public static <T extends Number> T parseNumber(Attribute attr, Class<T> type) throws InvalidXMLException {
        return parseNumber(new Node(attr), type);
    }

    public static <T extends Number> T parseNumber(Element el, Class<T> type) throws InvalidXMLException {
        return parseNumber(new Node(el), type);
    }

    public static <T extends Number> T parseNumber(Node node, Class<T> type, boolean infinity, T def) throws InvalidXMLException {
        if(node == null) {
            return def;
        } else {
            return parseNumber(node, node.getValue(), type, infinity);
        }
    }

    public static <T extends Number> T parseNumber(Node node, Class<T> type, T def) throws InvalidXMLException {
        return parseNumber(node, type, false, def);
    }

    public static <T extends Number> T parseNumber(Element el, Class<T> type, T def) throws InvalidXMLException {
        if(el == null) {
            return def;
        } else {
            return parseNumber(el, type);
        }
    }

    public static <T extends Number> T parseNumber(Attribute attr, Class<T> type, T def) throws InvalidXMLException {
        if(attr == null) {
            return def;
        } else {
            return parseNumber(attr, type);
        }
    }

    // Note: all the range overloads allow infinities if they are within the range

    public static <T extends Number & Comparable<T>> T parseNumber(Element el, Class<T> type, Range<T> range) throws InvalidXMLException {
        return parseNumber(new Node(el), type, range);
    }

    public static <T extends Number & Comparable<T>> T parseNumber(Attribute attr, Class<T> type, Range<T> range) throws InvalidXMLException {
        return parseNumber(new Node(attr), type, range);
    }

    public static <T extends Number & Comparable<T>> T parseNumber(Node node, Class<T> type, Range<T> range) throws InvalidXMLException {
        return parseNumber(node, node.getValueNormalize(), type, range);
    }

    public static <T extends Number & Comparable<T>> T parseNumber(Node node, String text, Class<T> type, Range<T> range) throws InvalidXMLException {
        T value = parseNumber(node, text, type, true);
        if(!range.contains(value)) {
            throw new InvalidXMLException(value + " is not in the range " + range, node);
        }
        return value;
    }

    public static <T extends Number & Comparable<T>> T parseNumber(Node node, String text, Class<T> type, Range<T> range, T def) throws InvalidXMLException {
        return node == null ? def : parseNumber(node, text, type, range);
    }

    public static <T extends Number & Comparable<T>> T parseNumber(Node node, Class<T> type, Range<T> range, T def) throws InvalidXMLException {
        return node == null ? def : parseNumber(node, type, range);
    }

    private static final Pattern RANGE_RE = Pattern.compile("\\s*(\\(|\\[)\\s*([^,]+)\\s*,\\s*([^\\)\\]]+)\\s*(\\)|\\])\\s*");

    /**
     * Parse a range in the standard mathematical format e.g.
     *
     *     [0, 1) for a closed-open range from 0 to 1.
     *
     * Can also parse single numbers as a closed range e.g.
     *
     *     5 for a closed-closed range from 5 to 5.
     */
    public static <T extends Number & Comparable<T>> Range<T> parseNumericRange(Node node, Class<T> type) throws InvalidXMLException {
        String value = node.getValue();

        Matcher matcher = RANGE_RE.matcher(value);
        if(!matcher.matches()) {
            T number = parseNumber(node, value, type, (T) null);
            if(number != null) {
                return Range.singleton(number);
            }
            throw new InvalidXMLException("Invalid " + type.getSimpleName().toLowerCase() + " range '" + value + "'", node);
        }

        T lower = parseNumber(node, matcher.group(2), type, true);
        T upper = parseNumber(node, matcher.group(3), type, true);

        BoundType lowerType = null, upperType = null;
        if(!Double.isInfinite(lower.doubleValue())) {
            lowerType = "(".equals(matcher.group(1)) ? BoundType.OPEN : BoundType.CLOSED;
        }
        if(!Double.isInfinite(upper.doubleValue())) {
            upperType = ")".equals(matcher.group(4)) ? BoundType.OPEN : BoundType.CLOSED;
        }

        if(lower.compareTo(upper) == 1) {
            throw new InvalidXMLException("range lower bound (" + lower + ") cannot be greater than upper bound (" + upper + ")", node);
        }

        if(lowerType == null) {
            if(upperType == null) {
                return Range.all();
            } else {
                return Range.upTo(upper, upperType);
            }
        } else {
            if(upperType == null) {
                return Range.downTo(lower, lowerType);
            } else {
                return Range.range(lower, lowerType, upper, upperType);
            }
        }
    }

    /**
     * Parse a numeric range from attributes on the given element specifying the bounds of the range, specifically:
     *
     *      gt gte lt lte
     */
    public static <T extends Number & Comparable<T>> Range<T> parseNumericRange(Element el, Class<T> type) throws InvalidXMLException {
        return parseNumericRange(el, type, Range.all());
    }

    public static <T extends Number & Comparable<T>> Range<T> parseNumericRange(Element el, Class<T> type, Range<T> def) throws InvalidXMLException {
        Attribute lt = el.getAttribute("lt");
        Attribute lte = getAttribute(el, "lte", "max");
        Attribute gt = el.getAttribute("gt");
        Attribute gte = getAttribute(el, "gte", "min");

        if(lt != null && lte != null) throw new InvalidXMLException("Conflicting upper bound for numeric range", el);
        if(gt != null && gte != null) throw new InvalidXMLException("Conflicting lower bound for numeric range", el);

        BoundType lowerBoundType, upperBoundType;
        T lowerBound, upperBound;

        if(gt != null) {
            lowerBound = parseNumber(gt, type, (T) null);
            lowerBoundType = BoundType.OPEN;
        } else {
            lowerBound = parseNumber(gte, type, (T) null);
            lowerBoundType = BoundType.CLOSED;
        }

        if(lt != null) {
            upperBound = parseNumber(lt, type, (T) null);
            upperBoundType = BoundType.OPEN;
        } else {
            upperBound = parseNumber(lte, type, (T) null);
            upperBoundType = BoundType.CLOSED;
        }

        if(lowerBound == null) {
            if(upperBound == null) {
                return def;
            } else {
                return Range.upTo(upperBound, upperBoundType);
            }
        } else {
            if(upperBound == null) {
                return Range.downTo(lowerBound, lowerBoundType);
            } else {
                return Range.range(lowerBound, lowerBoundType, upperBound, upperBoundType);
            }
        }
    }

    public static DurationProperty parseDuration(Element parent, String name) {
        return durationFactory.property(parent, name);
    }

    public static Duration parseDuration(@Nullable Node node, Duration def) throws InvalidXMLException {
        if(node == null) return def;
        try {
            return TimeUtils.parseDuration(node.getValueNormalize());
        } catch(DateTimeParseException e) {
            throw new InvalidXMLException("Invalid time format", node, e);
        }
    }

    public static @Nullable Duration parseDuration(Node node) throws InvalidXMLException {
        return parseDuration(node, null);
    }

    public static Duration parseDuration(Element el, Duration def) throws InvalidXMLException {
        return parseDuration(Node.fromNullable(el), def);
    }

    public static Duration parseDuration(Attribute attr, Duration def) throws InvalidXMLException {
        return parseDuration(Node.fromNullable(attr), def);
    }

    public static Duration parseDuration(Attribute attr) throws InvalidXMLException {
        return parseDuration(attr, null);
    }

    public static Duration parseTickDuration(Node node, String text) throws InvalidXMLException {
        if("oo".equals(text)) return TimeUtils.INF_POSITIVE;
        try {
            return Duration.ofMillis(Integer.parseInt(text) * 50);
        } catch(NumberFormatException e) {
            return parseDuration(node);
        }
    }

    public static Duration parseTickDuration(Node node) throws InvalidXMLException {
        return parseTickDuration(node, node.getValueNormalize());
    }

    public static Duration parseTickDuration(Node node, Duration def) throws InvalidXMLException {
        return node == null ? def : parseDuration(node);
    }

    public static Duration parseSecondDuration(Node node, String text) throws InvalidXMLException {
        if("oo".equals(text)) return TimeUtils.INF_POSITIVE;
        try {
            return Duration.ofSeconds(Integer.parseInt(text));
        } catch(NumberFormatException e) {
            return parseDuration(node);
        }
    }

    public static Duration parseSecondDuration(Node node) throws InvalidXMLException {
        return parseSecondDuration(node, node.getValueNormalize());
    }

    public static Duration parseSecondDuration(Node node, Duration def) throws InvalidXMLException {
        return node == null ? def : parseSecondDuration(node);
    }

    public static Key parseKey(Node node, String text) throws InvalidXMLException {
        return Bukkit.key(text);
    }

    public static Key parseKey(Node node) throws InvalidXMLException {
        return parseKey(node, node.getValueNormalize());
    }

    public static Key parseKey(Node node, Key def) throws InvalidXMLException {
        return node == null ? def : parseKey(node);
    }

    public static Class<? extends Entity> parseEntityType(Element el) throws InvalidXMLException {
        return parseEntityType(new Node(el));
    }

    public static Class<? extends Entity> parseEntityTypeAttribute(Element el, String attributeName, Class<? extends Entity> def) throws InvalidXMLException {
        Node node = Node.fromAttr(el, attributeName);
        return node == null ? def : parseEntityType(node);
    }

    public static Class<? extends Entity> parseEntityType(Node node) throws InvalidXMLException {
        return parseEntityType(node, node.getValue());
    }

    public static Class<? extends Entity> parseEntityType(Node node, String value) throws InvalidXMLException {
        if(!value.matches("[a-zA-Z0-9_]+")) {
            throw new InvalidXMLException("Invalid entity type '" + value + "'", node);
        }

        try {
            return Class.forName("org.bukkit.entity." + value).asSubclass(Entity.class);
        }
        catch(ClassNotFoundException | ClassCastException e) {
            throw new InvalidXMLException("Invalid entity type '" + value + "'", node);
        }
    }

    public static <T extends Number> PropertyBuilder<ImVector, ?> parseVector(Element el, String name, Class<T> type) {
        return new PropertyBuilder<>(el, name, VectorParser.get(type));
    }

    public static <T extends Number> PropertyBuilder<ImVector, ?> parseVector(Element el, String name) {
        return parseVector(el, name, Double.class);
    }

    public static Vector parseVector(Node node, String value) throws InvalidXMLException {
        if(node == null) return null;

        String[] components = value.trim().split("\\s*,\\s*");
        if(components.length != 3) {
            throw new InvalidXMLException("Invalid vector format", node);
        }
        try {
            return new Vector(parseNumber(node, components[0], Double.class, true),
                              parseNumber(node, components[1], Double.class, true),
                              parseNumber(node, components[2], Double.class, true));
        }
        catch(NumberFormatException e) {
            throw new InvalidXMLException("Invalid vector format", node);
        }
    }

    public static Vector parseVector(Node node) throws InvalidXMLException {
        return node == null ? null : parseVector(node, node.getValue());
    }

    public static Vector parseVector(Node node, Vector def) throws InvalidXMLException {
        return node == null ? def : parseVector(node);
    }

    public static Vector parseVector(Attribute attr, String value) throws InvalidXMLException {
        return attr == null ? null : parseVector(new Node(attr), value);
    }

    public static Vector parseVector(Attribute attr) throws InvalidXMLException {
        return attr == null ? null : parseVector(attr, attr.getValue());
    }

    public static Vector parseVector(Attribute attr, Vector def) throws InvalidXMLException {
        return attr == null ? def : parseVector(attr);
    }

    public static Vector parse2DVector(Node node, String value) throws InvalidXMLException {
        String[] components = value.trim().split("\\s*,\\s*");
        if(components.length != 2) {
            throw new InvalidXMLException("Invalid 2D vector format", node);
        }
        try {
            return new Vector(parseNumber(node, components[0], Double.class, true),
                              0d,
                              parseNumber(node, components[1], Double.class, true));
        }
        catch(NumberFormatException e) {
            throw new InvalidXMLException("Invalid 2D vector format", node);
        }
    }

    public static Vector parse2DVector(Node node) throws InvalidXMLException {
        return parse2DVector(node, node.getValue());
    }

    public static Vector parse2DVector(Node node, Vector def) throws InvalidXMLException {
        return node == null ? def : parse2DVector(node);
    }

    public static BlockVector parseBlockVector(Node node, BlockVector def) throws InvalidXMLException {
        if(node == null) return def;

        String[] components = node.getValue().trim().split("\\s*,\\s*");
        if(components.length != 3) {
            throw new InvalidXMLException("Invalid block location", node);
        }
        try {
            return new BlockVector(Integer.parseInt(components[0]),
                                   Integer.parseInt(components[1]),
                                   Integer.parseInt(components[2]));
        }
        catch(NumberFormatException e) {
            throw new InvalidXMLException("Invalid block location", node);
        }
    }

    public static BlockVector parseBlockVector(Node node) throws InvalidXMLException {
        return parseBlockVector(node, null);
    }

    public static NumericModifier parseNumericModifier(Element el) throws InvalidXMLException {
        return new NumericModifier(parseNumber(el.getAttribute("add"), Double.class, 0d),
                                   parseNumber(el.getAttribute("mul"), Double.class, 0d));
    }

    public static NumericModifier parseNumericModifier(@Nullable Element el, NumericModifier def) throws InvalidXMLException {
        return el == null ? def : parseNumericModifier(el);
    }

    public static DyeColor parseDyeColor(Attribute attr) throws InvalidXMLException {
        String name = attr.getValue().replace(" ", "_").toUpperCase();
        try {
            return DyeColor.valueOf(name);
        }
        catch(IllegalArgumentException e) {
            throw new InvalidXMLException("Invalid dye color '" + attr.getValue() + "'", attr);
        }
    }

    public static DyeColor parseDyeColor(Attribute attr, DyeColor def) throws InvalidXMLException {
        return attr == null ? def : parseDyeColor(attr);
    }

    public static Material parseMaterial(Node node, String text) throws InvalidXMLException {
        Material material = Material.matchMaterial(text);
        if(material == null) {
            throw new InvalidXMLException("Unknown material '" + text + "'", node);
        }
        return material;
    }

    public static Material parseMaterial(Node node) throws InvalidXMLException {
        return parseMaterial(node, node.getValueNormalize());
    }

    public static Material parseBlockMaterial(Node node, String text) throws InvalidXMLException {
        Material material = parseMaterial(node, text);
        if(!material.isBlock()) {
            throw new InvalidXMLException("Material " + material.name() + " is not a block", node);
        }
        return material;
    }

    public static Material parseBlockMaterial(Node node) throws InvalidXMLException {
        return node == null ? null : parseBlockMaterial(node, node.getValueNormalize());
    }

    public static MaterialData parseMaterialData(Node node, String text) throws InvalidXMLException {
        String[] pieces = text.split(":");
        Material material = parseMaterial(node, pieces[0]);
        byte data;
        if(pieces.length > 1) {
            data = parseNumber(node, pieces[1], Byte.class);
        } else {
            data = 0;
        }
        return material.getNewData(data);
    }

    public static MaterialData parseMaterialData(Node node, MaterialData def) throws InvalidXMLException {
        return node == null ? def : parseMaterialData(node, node.getValueNormalize());
    }

    public static MaterialData parseMaterialData(Node node) throws InvalidXMLException {
        return parseMaterialData(node, (MaterialData) null);
    }

    public static MaterialData parseBlockMaterialData(Node node, String text) throws InvalidXMLException {
        if(node == null) return null;
        MaterialData material = parseMaterialData(node, text);
        if(!material.getItemType().isBlock()) {
            throw new InvalidXMLException("Material " + material.getItemType().name() + " is not a block", node);
        }
        return material;
    }

    public static MaterialData parseBlockMaterialData(Node node, MaterialData def) throws InvalidXMLException {
        return node == null ? def : parseBlockMaterialData(node, node.getValueNormalize());
    }

    public static MaterialData parseBlockMaterialData(Node node) throws InvalidXMLException {
        return parseBlockMaterialData(node, (MaterialData) null);
    }

    public static MaterialPattern parseMaterialPattern(Node node, String value) throws InvalidXMLException {
        try {
            return MaterialPattern.parse(value);
        }
        catch(IllegalArgumentException e) {
            throw new InvalidXMLException(e.getMessage(), node);
        }
    }

    public static MaterialPattern parseMaterialPattern(Node node) throws InvalidXMLException {
        return parseMaterialPattern(node, node.getValue());
    }

    public static MaterialPattern parseMaterialPattern(Node node, MaterialPattern def) throws InvalidXMLException {
        return node == null ? def : parseMaterialPattern(node);
    }

    public static MaterialPattern parseMaterialPattern(Element el) throws InvalidXMLException {
        return parseMaterialPattern(new Node(el));
    }

    public static MaterialPattern parseMaterialPattern(Attribute attr) throws InvalidXMLException {
        return parseMaterialPattern(new Node(attr));
    }

    public static ImmutableSet<MaterialPattern> parseMaterialPatternSet(Node node) throws InvalidXMLException {
        ImmutableSet.Builder<MaterialPattern> patterns = ImmutableSet.builder();
        for(String value : Splitter.on(";").split(node.getValue())) {
            patterns.add(parseMaterialPattern(node, value));
        }
        return patterns.build();
    }

    public static MaterialMatcher parseMaterialMatcher(Element el) throws InvalidXMLException {
        return parseMaterialMatcher(el, NoMaterialMatcher.INSTANCE);
    }

    public static MaterialMatcher parseMaterialMatcher(Element el, MaterialMatcher empty) throws InvalidXMLException {
        Set<MaterialMatcher> matchers = new HashSet<>();

        final Attribute attrMaterial = el.getAttribute("material");
        if(attrMaterial != null) {
            matchers.add(parseMaterialPattern(attrMaterial));
        }

        for(Element elChild : el.getChildren()) {
            switch(elChild.getName()) {
                case "all-materials":
                case "all-items":
                    return AllMaterialMatcher.INSTANCE;

                case "all-blocks":
                    matchers.add(BlockMaterialMatcher.INSTANCE);
                    break;

                case "material":
                case "item":
                    matchers.add(parseMaterialPattern(elChild));
                    break;

                default:
                    throw new InvalidXMLException("Unknown material matcher tag", elChild);
            }
        }

        return CompoundMaterialMatcher.of(matchers, empty);
    }

    public static PotionEffectType parsePotionEffectType(Node node) throws InvalidXMLException {
        return parsePotionEffectType(node, node.getValue());
    }

    public static PotionEffectType parsePotionEffectType(Node node, String text) throws InvalidXMLException {
        PotionEffectType type = PotionEffectType.getByName(text.toUpperCase().replace(" ", "_"));
        if(type == null) type = Bukkit.potionEffectRegistry().get(parseKey(node, text));
        if(type == null) {
            throw new InvalidXMLException("Unknown potion effect '" + node.getValue() + "'", node);
        }
        return type;
    }

    private static PotionEffect createPotionEffect(Node node, PotionEffectType type, Duration duration, int amplifier, boolean ambient) throws InvalidXMLException {
        if(PotionEffectType.HEALTH_BOOST.equals(type) && amplifier <= -6) {
            // HB -5 kills the player instantly and sets their max health to 0,
            // and we don't seem to be able to change it before the vanilla death screen shows.
            throw new InvalidXMLException("Health boost level -5 and below is not supported", node);
        }
        return new PotionEffect(type,
                                TimeUtils.isInfPositive(duration) ? Integer.MAX_VALUE : (int) (duration.toMillis() / 50),
                                Duration.ZERO.equals(duration) ? 0 : amplifier, // negative amplifier may cause an error, but we can ignore it if duration is zero
                                ambient);
    }

    public static PotionEffect parsePotionEffect(Element el) throws InvalidXMLException {
        Node node = new Node(el);
        PotionEffectType type = parsePotionEffectType(node);
        Duration duration = parseSecondDuration(Node.fromAttr(el, "duration"), TimeUtils.INF_POSITIVE);
        int amplifier = parseNumber(Node.fromAttr(el, "amplifier"), Integer.class, 1) - 1;
        boolean ambient = parseBoolean(Node.fromAttr(el, "ambient"), false);

        return createPotionEffect(node, type, duration, amplifier, ambient);
    }

    public static PotionEffect parseCompactPotionEffect(Node node, String text) throws InvalidXMLException {
        String[] parts = text.split(":");

        if(parts.length == 0) throw new InvalidXMLException("Missing potion effect", node);
        PotionEffectType type = parsePotionEffectType(node, parts[0]);
        Duration duration = TimeUtils.INF_POSITIVE;
        int amplifier = 0;
        boolean ambient = false;

        if(parts.length >= 2) {
            duration = parseTickDuration(node, parts[1]);
            if(parts.length >= 3) {
                amplifier = parseNumber(node, parts[2], Integer.class);
                if(parts.length >= 4) {
                    ambient = parseBoolean(node, parts[3]);
                }
            }
        }

        return createPotionEffect(node, type, duration, amplifier, ambient);
    }

    public static PotionBrew parsePotion(Node node, String text) throws InvalidXMLException {
        final PotionBrew brew = Bukkit.potionRegistry().get(parseKey(node, text));
        if(brew == null) {
            throw new InvalidXMLException("Unknown potion '" + text + "'", node);
        }
        return brew;
    }

    public static PotionBrew parsePotion(Node node) throws InvalidXMLException {
        return parsePotion(node, node.getValueNormalize());
    }

    public static PotionBrew parsePotion(Node node, PotionBrew def) throws InvalidXMLException {
        return node == null ? def : parsePotion(node);
    }

    public static PotionBrew parsePotionOrFallback(Node node) throws InvalidXMLException {
        final PotionBrew brew = parsePotion(node, (PotionBrew) null);
        return brew != null ? brew : Bukkit.potionRegistry().getFallback();
    }

    public static <T extends Enum<T>> T parseEnum(Node node, String text, Class<T> type, String readableType) throws InvalidXMLException {
        text = text.trim().replace(' ', '_');
        try {
            // First, try the fast way
            return Enum.valueOf(type, text);
        } catch(IllegalArgumentException ex) {
            // If that fails, search for a case-insensitive match, without assuming enums are always uppercase
            for(T value : type.getEnumConstants()) {
                if(value.name().equalsIgnoreCase(text)) return value;
            }
            throw new InvalidXMLException("Unknown " + readableType + " '" + text + "'", node);
        }
    }

    public static <T extends Enum<T>> T parseEnum(@Nullable Node node, Class<T> type, String readableType, @Nullable T def) throws InvalidXMLException {
        if(node == null) return def;
        return parseEnum(node, node.getValueNormalize(), type, readableType);
    }

    public static <T extends Enum<T>> T parseEnum(@Nullable Node node, Class<T> type, String readableType) throws InvalidXMLException {
        return parseEnum(node, type, readableType, null);
    }

    public static <T extends Enum<T>> T parseEnum(Element el, Class<T> type) throws InvalidXMLException {
        return parseEnum(new Node(el), type, type.getSimpleName());
    }

    public static <T extends Enum<T>> T parseEnum(Element el, Class<T> type, String readableType) throws InvalidXMLException {
        return parseEnum(new Node(el), type, readableType);
    }

    public static <T extends Enum<T>> T parseEnum(Attribute attr, Class<T> type, String readableType) throws InvalidXMLException {
        return parseEnum(new Node(attr), type, readableType);
    }

    public static ChatColor parseChatColor(@Nullable Node node) throws InvalidXMLException {
        return parseEnum(node, ChatColor.class, "color");
    }

    public static ChatColor parseChatColor(@Nullable Node node, ChatColor def) throws InvalidXMLException {
        return node == null ? def : parseChatColor(node);
    }

    public static String getNormalizedNullableText(Element el) {
        String text = el.getTextNormalize();
        if(text == null || "".equals(text)) {
            return null;
        } else {
            return text;
        }
    }

    public static String getNullableAttribute(Element el, String...attrs) {
        String text = null;
        for(String attr : attrs) {
            text = el.getAttributeValue(attr);
            if(text != null) break;
        }
        return text;
    }

    public static UUID parseUuid(@Nullable Node node) throws InvalidXMLException {
        if(node == null) return null;
        String raw = node.getValue();
        try {
            return UUID.fromString(raw);
        }
        catch(IllegalArgumentException e) {
            throw new InvalidXMLException("Invalid UUID format (must be 8-4-4-4-12)", node, e);
        }
    }

    private static final Pattern USERNAME_REGEX = Pattern.compile("[a-zA-Z0-9_]{1,16}");
    public static String parseUsername(@Nullable Node node) throws InvalidXMLException {
        if(node == null) return null;
        String name = node.getValueNormalize();
        if(!USERNAME_REGEX.matcher(name).matches()) {
            throw new InvalidXMLException("Invalid Minecraft username '" + name + "'", node);
        }
        return name;
    }

    public static Skin parseUnsignedSkin(@Nullable Node node) throws InvalidXMLException {
        if(node == null) return null;
        String data = node.getValueNormalize();
        try {
            Base64.decodeBase64(data.getBytes());
        } catch(IllegalArgumentException e) {
            throw new InvalidXMLException("Skin data is not valid base64", node);
        }
        return new Skin(data, null);
    }

    /**
     * Guess if the given text is a JSON object by looking for the curly braces at either end
     */
    public static boolean looksLikeJson(String text) {
        text = text.trim();
        return text.startsWith("{") && text.endsWith("}");
    }

    /**
     * Parse a piece of formatted text, which can be either plain text with legacy
     * formatting codes, or JSON chat components.
     */
    public static BaseComponent parseFormattedText(@Nullable Node node, BaseComponent def) throws InvalidXMLException {
        if(node == null) return def;

        // <blah translate="x"/> is shorthand for <blah>{"translate":"x"}</blah>
        if(node.isElement()) {
            final Attribute translate = node.asElement().getAttribute("translate");
            if(translate != null) {
                return new TranslatableComponent(translate.getValue());
            }
        }

        String text = node.getValueNormalize();
        if(looksLikeJson(text)) {
            try {
                return Components.concat(ComponentSerializer.parse(node.getValue()));
            } catch(JsonParseException e) {
                throw new InvalidXMLException(e.getMessage(), node, e);
            }
        } else {
            return Components.concat(TextComponent.fromLegacyText(BukkitUtils.colorize(text)));
        }
    }

    /**
     * Parse a piece of formatted text, which can be either plain text with legacy
     * formatting codes, or JSON chat components.
     */
    public static @Nullable BaseComponent parseFormattedText(@Nullable Node node) throws InvalidXMLException {
        return parseFormattedText(node, null);
    }

    /**
     * Parse a piece of formatted text, which can be either plain text with legacy
     * formatting codes, or JSON chat components.
     */
    public static BaseComponent parseFormattedText(Element parent, String property, BaseComponent def) throws InvalidXMLException {
        return parseFormattedText(Node.fromChildOrAttr(parent, property), def);
    }

    /**
     * Parse a piece of formatted text, which can be either plain text with legacy
     * formatting codes, or JSON chat components.
     */
    public static BaseComponent parseFormattedText(Element parent, String property) throws InvalidXMLException {
        return parseFormattedText(Node.fromChildOrAttr(parent, property));
    }

    public static BaseComponent parseLocalizedText(Element el) throws InvalidXMLException {
        final Attribute translate = el.getAttribute("translate");
        if(translate != null) {
            return new TranslatableComponent(translate.getValue());
        } else {
            return new Component(el.getTextNormalize());
        }
    }

    public static BaseComponent parseLocalizedText(@Nullable Element el, BaseComponent def) throws InvalidXMLException {
        return el == null ? def : parseLocalizedText(el);
    }

    public static BaseComponent parseLocalizedText(Node node) throws InvalidXMLException {
        if(node.isElement()) {
            return parseLocalizedText(node.asElement());
        } else {
            return new Component(node.getValueNormalize());
        }
    }

    public static PropertyBuilder<String, ?> parseMessageKey(Element el, String name) throws InvalidXMLException {
        return new PropertyBuilder<>(el, name, StringParser.get())
            .validate((value, node) -> {
                if(!Translations.get().hasKey(value)) {
                    throw new InvalidXMLException("Unknown message key '" + value + "'", node);
                }
            });
    }

    public static PropertyBuilder<Team.OptionStatus, ?> parseNameTagVisibility(Element parent, String name) throws InvalidXMLException {
        return new PropertyBuilder<>(parent, name, new TeamRelationParser());
    }

    public static Enchantment parseEnchantment(Node node) throws InvalidXMLException {
        return parseEnchantment(node, node.getValueNormalize());
    }

    public static Enchantment parseEnchantment(Node node, String text) throws InvalidXMLException {
        Enchantment enchantment = Enchantment.getByName(text.toUpperCase().replace(" ", "_"));
        if(enchantment == null) enchantment = NMSHacks.getEnchantment(text);

        if(enchantment == null) {
            throw new InvalidXMLException("Unknown enchantment '" + text + "'", node);
        }

        return enchantment;
    }

    public static org.bukkit.attribute.Attribute parseAttribute(Node node, String text) throws InvalidXMLException {
        return attributeParser.parse(node, text);
    }

    public static org.bukkit.attribute.Attribute parseAttribute(Node node) throws InvalidXMLException {
        return parseAttribute(node, node.getValueNormalize());
    }

    public static AttributeModifier.Operation parseAttributeOperation(Node node, String text) throws InvalidXMLException {
        switch(text.toLowerCase()) {
            case "add": return AttributeModifier.Operation.ADD_NUMBER;
            case "base": return AttributeModifier.Operation.ADD_SCALAR;
            case "multiply": return AttributeModifier.Operation.MULTIPLY_SCALAR_1;
        }
        throw new InvalidXMLException("Unknown attribute modifier operation '" + text + "'", node);
    }

    public static AttributeModifier.Operation parseAttributeOperation(Node node) throws InvalidXMLException {
        return parseAttributeOperation(node, node.getValueNormalize());
    }

    public static AttributeModifier.Operation parseAttributeOperation(Node node, AttributeModifier.Operation def) throws InvalidXMLException {
        return node == null ? def : parseAttributeOperation(node);
    }

    public static Pair<org.bukkit.attribute.Attribute, AttributeModifier> parseCompactAttributeModifier(Node node, String text) throws InvalidXMLException {
        final String[] parts = text.split(":");
        if(parts.length != 3) {
            throw new InvalidXMLException("Bad attribute modifier format", node);
        }

        return Pair.create(
            parseAttribute(node, parts[0]),
            new AttributeModifier(
                "FromXML",
                parseNumber(node, parts[2], Double.class),
                parseAttributeOperation(node, parts[1])
            )
        );
    }

    public static Pair<org.bukkit.attribute.Attribute, AttributeModifier> parseAttributeModifier(Element el) throws InvalidXMLException {
        return Pair.create(
            parseAttribute(new Node(el)),
            new AttributeModifier(
                "FromXML",
                parseNumber(Node.fromRequiredAttr(el, "amount"), Double.class),
                parseAttributeOperation(Node.fromAttr(el, "operation"), AttributeModifier.Operation.ADD_NUMBER)
            )
        );
    }

    public static Pair<org.bukkit.attribute.Attribute, ItemAttributeModifier> parseItemAttributeModifier(Element el) throws InvalidXMLException {
        return Pair.create(
            parseAttribute(new Node(el)),
            new ItemAttributeModifier(
                parseEquipmentSlot(Node.fromAttr(el, "slot"), null),
                parseAttributeModifier(el).second
            )
        );
    }

    public static GameMode parseGameMode(Node node, String text) throws InvalidXMLException {
        text = text.trim();
        try {
            return GameMode.valueOf(text.toUpperCase());
        } catch(IllegalArgumentException e) {
            throw new InvalidXMLException("Unknown game-mode '" + text + "'", node);
        }
    }

    public static GameMode parseGameMode(Node node) throws InvalidXMLException {
        return parseGameMode(node, node.getValueNormalize());
    }

    public static GameMode parseGameMode(Node node, GameMode def) throws InvalidXMLException {
        return node == null ? def : parseGameMode(node);
    }

    public static Path parseRelativePath(Node node) throws InvalidXMLException {
        return parseRelativePath(node, null);
    }

    public static Path parseRelativePath(Node node, Path def) throws InvalidXMLException {
        if(node == null) return def;
        final String text = node.getValueNormalize();
        try {
            Path path = Paths.get(text);
            if(path.isAbsolute()) {
                throw new InvalidPathException(text, "Path is not relative");
            }
            for(Path part : path) {
                if(part.toString().trim().startsWith("..")) {
                    throw new InvalidPathException(text, "Path contains an invalid component");
                }
            }
            return path;
        } catch(InvalidPathException e) {
            throw new InvalidXMLException("Invalid relative path '" + text + "'", node, e);
        }
    }

    public static Path parseRelativePath(Path basePath, Node node, Path def) throws InvalidXMLException {
        if(node == null) return def;

        final Path path;
        try {
            path = basePath.resolve(node.getValue()).toRealPath();
        } catch(IOException e) {
            throw new InvalidXMLException("Error resolving relative file path", node);
        }

        if(!path.startsWith(basePath)) {
            throw new InvalidXMLException("Invalid relative file path", node);
        }

        return path;
    }

    public static Path parseRelativeFolder(Path basePath, Node node, Path def) throws InvalidXMLException {
        Path path = parseRelativePath(basePath, node, def);
        if(!Objects.equals(path, def) && !Files.isDirectory(path)) {
            throw new InvalidXMLException("Folder does not exist", node);
        }
        return path;
    }

    public static SemanticVersion parseSemanticVersion(Node node) throws InvalidXMLException {
        if(node == null) return null;

        String[] parts = node.getValueNormalize().split("\\.", 3);
        if(parts.length < 1 || parts.length > 3) {
            throw new InvalidXMLException("Version must be 1 to 3 whole numbers, separated by periods", node);
        }

        int major = parseNumber(node, parts[0], Integer.class);
        int minor = parts.length < 2 ? 0 : parseNumber(node, parts[1], Integer.class);
        int patch = parts.length < 3 ? 0 : parseNumber(node, parts[2], Integer.class);

        return new SemanticVersion(major, minor, patch);
    }

    public static Slot.Player parsePlayerSlot(Node node) throws InvalidXMLException {
        String value = node.getValue();
        Slot slot;
        try {
            slot = Slot.Player.forIndex(Integer.parseInt(value));
            if(slot == null) {
                throw new InvalidXMLException("Invalid inventory slot index (must be between 0 and 35)", node);
            }
        } catch(NumberFormatException e) {
            slot = Slot.forKey(value);
            if(slot == null) {
                throw new InvalidXMLException("Invalid inventory slot name", node);
            }
        }

        if(slot instanceof Slot.Player) {
            return (Slot.Player) slot;
        }

        throw new InvalidXMLException(value + " is not a player slot", node);
    }

    public static EquipmentSlot parseEquipmentSlot(Node node, EquipmentSlot def) throws InvalidXMLException {
        return node == null ? def : parseEquipmentSlot(node);
    }

    public static EquipmentSlot parseEquipmentSlot(Node node) throws InvalidXMLException {
        final EquipmentSlot slot = parsePlayerSlot(node).toEquipmentSlot();
        if(slot == null) {
            throw new InvalidXMLException("Not an equipment slot", node);
        }
        return slot;
    }

    /**
     * Return the path of the given Element in its Document as a list of indexes
     * relative to parent elements.
     *
     * The root element of a document has an empty path.
     */
    public static TIntList indexPath(Element el) {
        return indexPath(el, 0);
    }

    private static TIntList indexPath(Element child, int size) {
        final Element parent = child.getParentElement();
        if(parent == null) {
            return new TIntArrayList(size);
        } else {
            final TIntList path = indexPath(parent, size + 1);
            final int index = ((BoundedElement) child).indexInParent();
            if(index < 0) {
                throw new IllegalStateException("Parent element " + parent + " does not contain its child element " + child);
            }
            path.add(index);
            return path;
        }
    }
}
