package tc.oc.pgm.filters.parser;

import java.time.Duration;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Provider;

import com.google.common.collect.Range;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.jdom2.Element;
import tc.oc.api.docs.SemanticVersion;
import tc.oc.commons.bukkit.localization.MessageTemplate;
import tc.oc.commons.core.formatting.StringUtils;
import tc.oc.commons.core.util.Comparables;
import tc.oc.pgm.classes.ClassModule;
import tc.oc.pgm.classes.PlayerClass;
import tc.oc.pgm.features.FeatureDefinitionContext;
import tc.oc.pgm.features.FeatureDefinitionParser;
import tc.oc.pgm.features.FeatureParser;
import tc.oc.pgm.features.MagicMethodFeatureParser;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.filters.matcher.CauseFilter;
import tc.oc.pgm.filters.matcher.StaticFilter;
import tc.oc.pgm.filters.matcher.block.MaterialFilter;
import tc.oc.pgm.filters.matcher.block.StructuralLoadFilter;
import tc.oc.pgm.filters.matcher.block.VoidFilter;
import tc.oc.pgm.filters.matcher.damage.AttackerFilter;
import tc.oc.pgm.filters.matcher.damage.DamagerFilter;
import tc.oc.pgm.filters.matcher.damage.RelationFilter;
import tc.oc.pgm.filters.matcher.damage.VictimFilter;
import tc.oc.pgm.filters.matcher.entity.EntityTypeFilter;
import tc.oc.pgm.filters.matcher.entity.SpawnReasonFilter;
import tc.oc.pgm.filters.matcher.match.FlagStateFilter;
import tc.oc.pgm.filters.matcher.match.LegacyRandomFilter;
import tc.oc.pgm.filters.matcher.match.MatchMutationFilter;
import tc.oc.pgm.filters.matcher.match.MatchStateFilter;
import tc.oc.pgm.filters.matcher.match.MonostableFilter;
import tc.oc.pgm.filters.matcher.match.PlayerCountFilter;
import tc.oc.pgm.filters.matcher.match.RandomFilter;
import tc.oc.pgm.filters.matcher.party.CompetitorFilter;
import tc.oc.pgm.filters.matcher.party.GoalFilter;
import tc.oc.pgm.filters.matcher.party.RankFilter;
import tc.oc.pgm.filters.matcher.party.ScoreFilter;
import tc.oc.pgm.filters.matcher.party.TeamFilter;
import tc.oc.pgm.filters.matcher.player.AttributeFilter;
import tc.oc.pgm.filters.matcher.player.CanFlyFilter;
import tc.oc.pgm.filters.matcher.player.CarryingFlagFilter;
import tc.oc.pgm.filters.matcher.player.CarryingItemFilter;
import tc.oc.pgm.filters.matcher.player.HoldingItemFilter;
import tc.oc.pgm.filters.matcher.player.KillStreakFilter;
import tc.oc.pgm.filters.matcher.player.ParticipatingFilter;
import tc.oc.pgm.filters.matcher.player.PlayerClassFilter;
import tc.oc.pgm.filters.matcher.player.PoseFilter;
import tc.oc.pgm.filters.matcher.player.WearingItemFilter;
import tc.oc.pgm.filters.operator.AllFilter;
import tc.oc.pgm.filters.operator.AnyFilter;
import tc.oc.pgm.filters.operator.FallthroughFilter;
import tc.oc.pgm.filters.operator.InverseFilter;
import tc.oc.pgm.filters.operator.OneFilter;
import tc.oc.pgm.filters.operator.SameTeamFilter;
import tc.oc.pgm.filters.operator.TeamFilterAdapter;
import tc.oc.pgm.flag.FlagDefinition;
import tc.oc.pgm.flag.Post;
import tc.oc.pgm.flag.state.Captured;
import tc.oc.pgm.flag.state.Carried;
import tc.oc.pgm.flag.state.Dropped;
import tc.oc.pgm.flag.state.Returned;
import tc.oc.pgm.flag.state.State;
import tc.oc.pgm.goals.GoalDefinition;
import tc.oc.pgm.itemmeta.ItemModifier;
import tc.oc.pgm.kits.ItemParser;
import tc.oc.pgm.map.MapProto;
import tc.oc.pgm.map.ProtoVersions;
import tc.oc.pgm.match.MatchState;
import tc.oc.pgm.match.PlayerRelation;
import tc.oc.pgm.mutation.Mutation;
import tc.oc.pgm.teams.TeamFactory;
import tc.oc.pgm.utils.MethodParser;
import tc.oc.pgm.utils.XMLUtils;
import tc.oc.pgm.xml.InvalidXMLException;
import tc.oc.pgm.xml.Node;
import tc.oc.pgm.xml.parser.Parser;
import tc.oc.pgm.xml.property.MessageTemplateProperty;
import tc.oc.pgm.xml.property.PropertyBuilderFactory;

import static tc.oc.commons.core.exception.LambdaExceptionUtils.rethrowFunction;

public class FilterDefinitionParser extends MagicMethodFeatureParser<Filter> implements FeatureDefinitionParser<Filter> {

    @Inject protected @MapProto SemanticVersion proto;
    @Inject protected FeatureDefinitionContext features;
    @Inject protected ItemParser itemParser;
    @Inject protected Parser<Attribute> attributeParser;
    @Inject protected PropertyBuilderFactory<MessageTemplate, MessageTemplateProperty> messageTemplates;
    @Inject protected CauseFilter.Factory causeFilters;
    @Inject protected FilterParser filterParser;
    @Inject protected FeatureParser<TeamFactory> teamParser;
    @Inject protected Provider<ClassModule> classModule;
    @Inject protected ItemModifier itemModifier;

    @MethodParser("allow")
    public Filter parseAllow(Element el) throws InvalidXMLException {
        return new FallthroughFilter(Filter.QueryResponse.ALLOW, filterParser.parseChild(el));
    }

    @MethodParser("deny")
    public Filter parseDeny(Element el) throws InvalidXMLException {
        return new FallthroughFilter(Filter.QueryResponse.DENY, filterParser.parseChild(el));
    }

    @MethodParser("always")
    public Filter parseAlways(Element el) {
        return new StaticFilter(Filter.QueryResponse.ALLOW);
    }

    @MethodParser("never")
    public Filter parseNever(Element el) {
        return new StaticFilter(Filter.QueryResponse.DENY);
    }

    @MethodParser("any")
    public Filter parseAny(Element el) throws InvalidXMLException {
        return new AnyFilter(filterParser.parseChildList(el));
    }

    @MethodParser("all")
    public Filter parseAll(Element el) throws InvalidXMLException {
        return new AllFilter(filterParser.parseChildList(el));
    }

    @MethodParser("one")
    public Filter parseOne(Element el) throws InvalidXMLException {
        return new OneFilter(filterParser.parseChildList(el));
    }

    @MethodParser("not")
    public Filter parseNot(Element el) throws InvalidXMLException {
        return new InverseFilter(filterParser.parseChild(el));
    }

    @MethodParser("team")
    public TeamFilter parseTeam(Element el) throws InvalidXMLException {
        return new TeamFilter(teamParser.parseReference(Node.of(el)));
    }

    @MethodParser("same-team")
    public SameTeamFilter parseSameTeam(Element el) throws InvalidXMLException {
        return new SameTeamFilter(filterParser.parseChild(el));
    }

    @MethodParser("attacker")
    public AttackerFilter parseAttacker(Element el) throws InvalidXMLException {
        return new AttackerFilter(filterParser.parseChild(el));
    }

    @MethodParser("victim")
    public VictimFilter parseVictim(Element el) throws InvalidXMLException {
        return new VictimFilter(filterParser.parseChild(el));
    }

    @MethodParser
    public Filter damager(Element el) throws InvalidXMLException {
        return new DamagerFilter(filterParser.parseChild(el));
    }

    @MethodParser("class")
    public PlayerClassFilter parseClass(Element el) throws InvalidXMLException {
        final PlayerClass playerClass = StringUtils.bestFuzzyMatch(el.getTextNormalize(), classModule.get().getPlayerClasses(), 0.9);
        if (playerClass == null) {
            throw new InvalidXMLException("Could not find player-class: " + el.getTextNormalize(), el);
        }

        return new PlayerClassFilter(playerClass);
    }

    @MethodParser("material")
    public MaterialFilter parseMaterial(Element el) throws InvalidXMLException {
        return new MaterialFilter(XMLUtils.parseMaterialPattern(el));
    }

    @MethodParser("void")
    public VoidFilter parseVoid(Element el) throws InvalidXMLException {
        return new VoidFilter();
    }

    @MethodParser("entity")
    public EntityTypeFilter parseEntity(Element el) throws InvalidXMLException {
        return new EntityTypeFilter(XMLUtils.parseEnum(el, EntityType.class, "entity type"));
    }

    @MethodParser("mob")
    public EntityTypeFilter parseMob(Element el) throws InvalidXMLException {
        EntityTypeFilter matcher = this.parseEntity(el);
        if(!LivingEntity.class.isAssignableFrom(matcher.getEntityType())) {
            throw new InvalidXMLException("Unknown mob type: " + el.getTextNormalize(), el);
        }
        return matcher;
    }

    @MethodParser("spawn")
    public SpawnReasonFilter parseSpawnReason(Element el) throws InvalidXMLException {
        return new SpawnReasonFilter(XMLUtils.parseEnum(new Node(el), CreatureSpawnEvent.SpawnReason.class, "spawn reason"));
    }

    @MethodParser("kill-streak")
    public KillStreakFilter parseKillStreak(Element el) throws InvalidXMLException {
        boolean repeat = XMLUtils.parseBoolean(el.getAttribute("repeat"), false);
        boolean persistent = XMLUtils.parseBoolean(el.getAttribute("persistent"), false);
        Integer count = XMLUtils.parseNumber(el.getAttribute("count"), Integer.class, (Integer) null);
        Integer min = XMLUtils.parseNumber(el.getAttribute("min"), Integer.class, (Integer) null);
        Integer max = XMLUtils.parseNumber(el.getAttribute("max"), Integer.class, (Integer) null);
        Range<Integer> range;

        if(count != null) {
            range = Range.singleton(count);
        } else if(min == null) {
            if(max == null) {
                throw new InvalidXMLException("kill-streak filter must have a count, min, or max", el);
            } else {
                range = Range.atMost(max);
            }
        } else {
            if(max == null) {
                range = Range.atLeast(min);
            } else {
                range = Range.closed(min, max);
            }
        }

        if(repeat && !range.hasUpperBound()) {
            throw new InvalidXMLException("repeating kill-streak filter must have a count or max", el);
        }

        return new KillStreakFilter(range, repeat, persistent);
    }

    @MethodParser("random")
    public Filter parseRandom(Element el) throws InvalidXMLException {
        Node node = new Node(el);
        Range<Double> chance;
        try {
            chance = Range.closedOpen(0d, XMLUtils.parseNumber(node, Double.class));
        } catch(InvalidXMLException e) {
            chance = XMLUtils.parseNumericRange(node, Double.class);
        }

        Range<Double> valid = Range.closed(0d, 1d);
        if (valid.encloses(chance)) {
            return proto.isNoOlderThan(ProtoVersions.EVENT_QUERIES) ? new RandomFilter(chance)
                                                                    : new LegacyRandomFilter(chance);
        } else {
            double lower = chance.hasLowerBound() ? chance.lowerEndpoint() : Double.NEGATIVE_INFINITY;
            double upper = chance.hasUpperBound() ? chance.upperEndpoint() : Double.POSITIVE_INFINITY;
            double invalid;
            if(!valid.contains(lower)) {
                invalid = lower;
            } else {
                invalid = upper;
            }

            throw new InvalidXMLException("chance value (" + invalid + ") is not between 0 and 1", el);
        }
    }

    @MethodParser("grounded")
    public Filter parseGrounded(Element el) throws InvalidXMLException {
        return new PoseFilter(PoseFlag.GROUNDED);
    }

    @MethodParser({"crouching", "sneaking"})
    public Filter parseCrouching(Element el) throws InvalidXMLException {
        return new PoseFilter(PoseFlag.SNEAKING);
    }

    @MethodParser("walking")
    public Filter parseWalking(Element el) throws InvalidXMLException {
        return new InverseFilter(new AnyFilter(new PoseFilter(PoseFlag.SNEAKING),
                                               new PoseFilter(PoseFlag.SPRINTING)));
    }

    @MethodParser("sprinting")
    public Filter parseSprinting(Element el) throws InvalidXMLException {
        return new PoseFilter(PoseFlag.SPRINTING);
    }

    @MethodParser("gliding")
    public Filter parseGlidingFilter(Element el) throws InvalidXMLException {
        return new PoseFilter(PoseFlag.GLIDING);
    }

    @MethodParser("flying")
    public Filter parseFlying(Element el) throws InvalidXMLException {
        return new PoseFilter(PoseFlag.FLYING);
    }

    @MethodParser("can-fly")
    public CanFlyFilter parseCanFly(Element el) throws InvalidXMLException {
        return new CanFlyFilter();
    }

    private Filter parseExplicitTeam(Element el, CompetitorFilter filter) throws InvalidXMLException {
        final boolean any = XMLUtils.parseBoolean(el.getAttribute("any"), false);
        final Optional<TeamFactory> team = teamParser.property(el).optional();
        if(any && team.isPresent()) {
            throw new InvalidXMLException("Cannot combine attributes 'team' and 'any'", el);
        }
        return any || team.isPresent() ? new TeamFilterAdapter(team, filter)
                                       : filter;
    }

    private GoalDefinition goalReference(Element el) throws InvalidXMLException {
        return features.reference(new Node(el), GoalDefinition.class);
    }

    private GoalFilter goalFilter(Element el) throws InvalidXMLException {
        return new GoalFilter(goalReference(el));
    }

    @MethodParser("objective")
    public Filter parseGoal(Element el) throws InvalidXMLException {
        return parseExplicitTeam(el, goalFilter(el));
    }

    @MethodParser("completed")
    public Filter parseCompleted(Element el) throws InvalidXMLException {
        return new TeamFilterAdapter(Optional.empty(), goalFilter(el));
    }

    @MethodParser("captured")
    public Filter parseCaptured(Element el) throws InvalidXMLException {
        final GoalFilter goal = goalFilter(el);
        final Optional<TeamFactory> team = teamParser.property(el).optional();
        return team.isPresent() ? new TeamFilterAdapter(team, goal)
                                : goal;
    }

    @MethodParser("rank")
    public Filter parseRankFilter(Element el) throws InvalidXMLException {
        return parseExplicitTeam(el, new RankFilter(XMLUtils.parseNumericRange(new Node(el), Integer.class)));
    }

    @MethodParser("score")
    public Filter parseScoreFilter(Element el) throws InvalidXMLException {
        return parseExplicitTeam(el, new ScoreFilter(XMLUtils.parseNumericRange(new Node(el), Integer.class)));
    }

    protected FlagStateFilter parseFlagState(Element el, Class<? extends State> state) throws InvalidXMLException {
        return new FlagStateFilter(features.reference(new Node(el), FlagDefinition.class),
                                   Node.tryAttr(el, "post").map(rethrowFunction(attr -> features.reference(attr, Post.class))),
                                   state);
    }

    @MethodParser("flag-carried")
    public FlagStateFilter parseFlagCarried(Element el) throws InvalidXMLException {
        return this.parseFlagState(el, Carried.class);
    }

    @MethodParser("flag-dropped")
    public FlagStateFilter parseFlagDropped(Element el) throws InvalidXMLException {
        return this.parseFlagState(el, Dropped.class);
    }

    @MethodParser("flag-returned")
    public FlagStateFilter parseFlagReturned(Element el) throws InvalidXMLException {
        return this.parseFlagState(el, Returned.class);
    }

    @MethodParser("flag-captured")
    public FlagStateFilter parseFlagCaptured(Element el) throws InvalidXMLException {
        return this.parseFlagState(el, Captured.class);
    }

    @MethodParser("carrying-flag")
    public CarryingFlagFilter parseCarryingFlag(Element el) throws InvalidXMLException {
        return new CarryingFlagFilter(features.reference(new Node(el), FlagDefinition.class));
    }

    @MethodParser("cause")
    public CauseFilter parseCause(Element el) throws InvalidXMLException {
        return causeFilters.create(XMLUtils.parseEnum(el, CauseFilter.Cause.class, "cause filter"));
    }

    @MethodParser("relation")
    public RelationFilter parseRelation(Element el) throws InvalidXMLException {
        return new RelationFilter(XMLUtils.parseEnum(el, PlayerRelation.class, "player relation filter"));
    }

    private ImItemStack parseFilterItem(Element el) throws InvalidXMLException {
        return itemModifier.modify(itemParser.parseRequiredItem(el)).immutableCopy();
    }

    @MethodParser("carrying")
    public CarryingItemFilter parseHasItem(Element el) throws InvalidXMLException {
        return new CarryingItemFilter(parseFilterItem(el));
    }

    @MethodParser("holding")
    public HoldingItemFilter parseHolding(Element el) throws InvalidXMLException {
        return new HoldingItemFilter(parseFilterItem(el));
    }

    @MethodParser("wearing")
    public WearingItemFilter parseWearingItem(Element el) throws InvalidXMLException {
        return new WearingItemFilter(parseFilterItem(el));
    }

    @MethodParser("structural-load")
    public StructuralLoadFilter parseStructuralLoad(Element el) throws InvalidXMLException {
        return new StructuralLoadFilter(XMLUtils.parseNumber(el, Integer.class));
    }

    @MethodParser("time")
    public Filter parseTimeFilter(Element el) throws InvalidXMLException {
        final Duration duration = XMLUtils.parseDuration(el, (Duration) null);
        if(Comparables.greaterThan(duration, Duration.ZERO)) {
            return new AllFilter(
                MatchStateFilter.started(),
                new MonostableFilter(
                    duration,
                    MatchStateFilter.running(),
                    Optional.empty()
                ).not()
            );
        } else {
            return new MatchStateFilter(MatchState.Running, MatchState.Finished);
        }
    }

    @MethodParser("countdown")
    public Filter parseCountdownFilter(Element el) throws InvalidXMLException {
        final Duration duration = XMLUtils.parseDuration(el, "duration").required();
        if(Comparables.greaterThan(duration, Duration.ZERO)) {
            return new MonostableFilter(duration,
                                        filterParser.parseReferenceOrChild(el),
                                        messageTemplates.property(el, "message")
                                                        .placeholders(Range.closed(0, 1))
                                                        .optional());
        } else {
            return new StaticFilter(Filter.QueryResponse.DENY);
        }
    }

    @MethodParser("mutation")
    public MatchMutationFilter parseMatchMutation(Element el) throws InvalidXMLException {
        return new MatchMutationFilter(XMLUtils.parseEnum(el, Mutation.class, "match mutation"));
    }

    @MethodParser("participating")
    public Filter parseParticipating(Element el) throws InvalidXMLException {
        return new ParticipatingFilter(true);
    }

    @MethodParser("observing")
    public Filter parseObserving(Element el) throws InvalidXMLException {
        return new ParticipatingFilter(false);
    }

    @MethodParser("match-started")
    public Filter parseMatchStarted(Element el) throws InvalidXMLException {
        return new MatchStateFilter(MatchState.Running, MatchState.Finished);
    }

    @MethodParser("match-running")
    public Filter parseMatchRunning(Element el) throws InvalidXMLException {
        return new MatchStateFilter(MatchState.Running);
    }

    @MethodParser("match-finished")
    public Filter parseMatchFinished(Element el) throws InvalidXMLException {
        return new MatchStateFilter(MatchState.Finished);
    }

    @MethodParser("players")
    public Filter parsePlayerCount(Element el) throws InvalidXMLException {
        return new PlayerCountFilter(filterParser.parseReferenceOrChild(el),
                                     XMLUtils.parseNumericRange(el, Integer.class, Range.atLeast(1)),
                                     XMLUtils.parseBoolean(el, "participants").optional(true),
                                     XMLUtils.parseBoolean(el, "observers").optional(false));
    }

    @MethodParser
    public Filter attribute(Element el) throws InvalidXMLException {
        final Node node = Node.of(el);
        return new AttributeFilter(attributeParser.parse(node),
                                   XMLUtils.parseNumericRange(el, Double.class));
    }
}
