package tc.oc.pgm.controlpoint;

import java.time.Duration;
import java.util.stream.Stream;
import javax.inject.Inject;

import com.google.common.collect.Range;
import org.bukkit.Material;
import org.bukkit.util.Vector;
import org.jdom2.Element;
import tc.oc.pgm.features.FeatureDefinitionParser;
import tc.oc.pgm.features.FeatureParser;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.filters.matcher.block.MaterialFilter;
import tc.oc.pgm.filters.operator.AnyFilter;
import tc.oc.pgm.filters.parser.FilterParser;
import tc.oc.pgm.regions.BlockBoundedValidation;
import tc.oc.pgm.regions.Region;
import tc.oc.pgm.regions.RegionParser;
import tc.oc.pgm.teams.TeamFactory;
import tc.oc.pgm.utils.MaterialPattern;
import tc.oc.pgm.utils.XMLUtils;
import tc.oc.pgm.xml.InvalidXMLException;
import tc.oc.pgm.xml.Node;

public final class ControlPointParser implements FeatureDefinitionParser<ControlPointDefinition> {

    private static final Filter VISUAL_MATERIALS = AnyFilter.of(Stream.of(Material.WOOL,
                                                                          Material.CARPET,
                                                                          Material.STAINED_CLAY,
                                                                          Material.STAINED_GLASS,
                                                                          Material.STAINED_GLASS_PANE)
                                                                      .map(material -> MaterialFilter.of(MaterialPattern.accepting(material))));

    private final RegionParser regionParser;
    private final FilterParser filterParser;
    private final FeatureParser<TeamFactory> teamParser;

    @Inject private ControlPointParser(RegionParser regionParser, FilterParser filterParser, FeatureParser<TeamFactory> teamParser) {
        this.regionParser = regionParser;
        this.filterParser = filterParser;
        this.teamParser = teamParser;
    }

    @Override
    public ControlPointDefinition parseElement(Element elControlPoint) throws InvalidXMLException {
        final boolean koth = "hill".equals(elControlPoint.getName());

        Region captureRegion = regionParser.property(elControlPoint, "capture-region")
                                           .alias("capture")
                                           .union();
        Region progressDisplayRegion = regionParser.property(elControlPoint, "progress-display-region")
                                                   .alias("progress-display", "progress")
                                                   .validate(BlockBoundedValidation.INSTANCE)
                                                   .optionalUnion(null);
        Region ownerDisplayRegion = regionParser.property(elControlPoint, "owner-display-region")
                                                .alias("owner-display", "captured")
                                                .validate(BlockBoundedValidation.INSTANCE)
                                                .optionalUnion(null);
        Filter captureFilter = filterParser.property(elControlPoint, "capture-filter").optional(null);
        Filter playerFilter = filterParser.property(elControlPoint, "player-filter").optional(null);

        Filter visualMaterials = filterParser.property(elControlPoint, "visual-materials")
                                             .optionalMulti()
                                             .<Filter>map(AnyFilter::new)
                                             .orElse(VISUAL_MATERIALS);

        String name = elControlPoint.getAttributeValue("name", "Hill");
        TeamFactory initialOwner = teamParser.property(elControlPoint, "initial-owner").optional(null);
        Vector capturableDisplayBeacon = XMLUtils.parseVector(elControlPoint.getAttribute("beacon"));
        Duration timeToCapture = XMLUtils.parseDuration(elControlPoint.getAttribute("capture-time"), Duration.ofSeconds(30));

        double timeMultiplier = XMLUtils.parseNumber(elControlPoint.getAttribute("time-multiplier"), Double.class, koth ? 0.1D : 0D);

        final double recoveryRate, decayRate;
        final Node attrIncremental = Node.fromAttr(elControlPoint, "incremental");
        final Node attrRecovery = Node.fromAttr(elControlPoint, "recovery");
        final Node attrDecay = Node.fromAttr(elControlPoint, "decay");

        if(attrIncremental == null) {
            recoveryRate = XMLUtils.parseNumber(attrRecovery, Double.class, Range.atLeast(0D), koth ? 1D : Double.POSITIVE_INFINITY);
            decayRate = XMLUtils.parseNumber(attrDecay, Double.class, Range.atLeast(0D), koth ? 0D : Double.POSITIVE_INFINITY);
        } else {
            if(attrRecovery != null || attrDecay != null) {
                throw new InvalidXMLException("Cannot combine this attribute with 'incremental'", attrRecovery != null ? attrRecovery : attrDecay);
            }
            final boolean incremental = XMLUtils.parseBoolean(attrIncremental, koth);
            recoveryRate = incremental ? 1D : Double.POSITIVE_INFINITY;
            decayRate = incremental ? 0D : Double.POSITIVE_INFINITY;
        }

        boolean neutralState = XMLUtils.parseBoolean(elControlPoint.getAttribute("neutral-state"), koth);
        boolean permanent = XMLUtils.parseBoolean(elControlPoint.getAttribute("permanent"), false);
        float pointsOwned = XMLUtils.parseNumber(elControlPoint.getAttribute("owner-points"), Float.class, 0f);
        float pointsPerSecond = XMLUtils.parseNumber(elControlPoint.getAttribute("points"), Float.class, 1f);
        float pointsGrowth = XMLUtils.parseNumber(elControlPoint.getAttribute("points-growth"), Float.class, Float.POSITIVE_INFINITY);
        boolean showProgress = XMLUtils.parseBoolean(elControlPoint.getAttribute("show-progress"), koth);
        boolean visible = XMLUtils.parseBoolean(elControlPoint.getAttribute("show"), true);
        Boolean required = XMLUtils.parseBoolean(elControlPoint.getAttribute("required"), null);

        ControlPointDefinition.CaptureCondition captureCondition =
            XMLUtils.parseEnum(Node.fromAttr(elControlPoint, "capture-rule"),
                               ControlPointDefinition.CaptureCondition.class,
                               "capture rule",
                               ControlPointDefinition.CaptureCondition.EXCLUSIVE);

        return new ControlPointDefinitionImpl(
            name, required, visible,
            captureRegion, captureFilter, playerFilter,
            progressDisplayRegion, ownerDisplayRegion, visualMaterials,
            capturableDisplayBeacon == null ? null : capturableDisplayBeacon.toBlockVector(),
            timeToCapture, timeMultiplier, recoveryRate, decayRate, initialOwner, captureCondition,
            neutralState, permanent, pointsOwned, pointsPerSecond, pointsGrowth, showProgress
        );
    }
}
