package net.ranides.assira.reflection.walker.rules;

import lombok.Data;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.walker.ObjectVisitor;
import net.ranides.assira.reflection.walker.WalkerContexts;
import net.ranides.assira.text.ResolveFormat;

import java.util.*;
import java.util.function.*;
import java.util.regex.Pattern;

/**
 * CollectRules is configuration used to construct "collecting" ObjectVisitor.
 * Collectors by default use WalkerEntry, but can use any target type.
 *
 * @param <T> type
 */
public class CollectRules<T> {

    protected final Map<Object, Object> properties;

    protected final List<RuleEntry<T>> rules;

    protected CollectRules(CollectRules<T> source) {
        this(source.properties, source.rules);
    }

    protected CollectRules(Map<Object, Object> properties, List<RuleEntry<T>> rules) {
        this.properties = new HashMap<>(properties);
        this.rules = new ArrayList<>(rules);
    }

    /**
     * Creates new visitor inserting results into target collection.
     *
     * @param target target
     * @return visitor
     */
    public ObjectVisitor visitor(Collection<T> target) {
        return new ConsumeRuleVisitor(target::add);
    }

    /**
     * Creates new visitor passing results into consumer.
     *
     * @param consumer consumer
     * @return visitor
     */
    public ObjectVisitor visitor(Consumer<T> consumer) {
        return new ConsumeRuleVisitor(consumer);
    }

    /**
     * This fluent builder allows to construct new collecting configuration
     */
    public static class Builder<T> {

        private final Map<Object, Object> properties = new HashMap<>();
        private final List<RuleEntry<T>> rules = new ArrayList<>();

        /**
         * Creates immutable CollectRules
         *
         * @return CollectRules
         */
        public CollectRules<T> prepare() {
            return new CollectRules<>(properties, rules);
        }

        /**
         * Defines custom property with "name" which will be accessible from every ObjectContext.
         *
         * Please note, that if you put property of {@link ResolveFormat} type,
         * then it will be evaluated at access time.
         * To read details, lets see {@link ResolveFormat} documentation.
         *
         * @param name name
         * @param value value
         * @return Builder
         */
        public Builder<T> property(String name, Object value) {
            this.properties.put(name, value);
            return this;
        }

        /**
         * Defines custom property with "name" which will be accessible from every ObjectContext.
         * "value" string will be to converted {@link ResolveFormat} object.
         *
         * Please note, that property of {@link ResolveFormat} type is evaluated at access time.
         * To read details, lets see {@link ResolveFormat} documentation.
         *
         * @param name name
         * @param value value
         * @return Builder
         */
        public Builder<T> property(String name, String value) {
            this.properties.put(name, ResolveFormat.compile(value));
            return this;
        }

        /**
         * Starts composition of new rule.
         * Every rule contains some condition and action.
         * It is possible do configure multiple rules matching different objects.
         *
         * @return Rule
         */
        public RootRule rule() {
            return new RootRule();
        }

        /**
         * Copies all rules from provided "parent" into this builder.
         *
         * @param parent parent
         * @return Builder
         */
        public Builder<T> rules(CollectRules<T> parent) {
            this.properties.putAll(parent.properties);
            this.rules.addAll(parent.rules);
            return this;
        }

        /**
         * Rule builder, allowing composition of rule conditions and rule actions.
         *
         * @param <F> F
         */
        public abstract class AbstractRule<F> {

            protected final List<Predicate<WalkerContexts.ObjectContext<?>>> predicates;

            protected BiConsumer<ObjectVisitor.ConsumeVisitor<T>, WalkerContexts.ObjectContext<F>> before = (v, c) -> { };

            protected AbstractRule(List<Predicate<WalkerContexts.ObjectContext<?>>> predicates) {
                this.predicates = predicates;
            }

            /**
             * Lets execute provided handler.
             * Do not stop processing other rules, some of them can match this field.
             *
             * Handler receives description of visited field.
             * Handler's return value is sent to ConsumeVisitor
             *
             * @param handler handler
             * @return Builder
             */
            public Builder<T> exec(Consumer<WalkerContexts.ObjectContext<F>> handler) {
                return exec((target, ctx) -> handler.accept(ctx));
            }

            /**
             * Lets execute provided handler.
             * Do not stop processing other rules, some of them can match this field.
             *
             * Handler receives two arguments:
             *  - collecting consumer, assocciated with visitor
             *  - description of visited field
             *
             * Handler's return value is sent to ConsumeVisitor
             * Please note that handler's body can send much more values to ConsumeVisitor
             *
             * @param handler handler
             * @return Builder
             */
            public Builder<T> exec(BiConsumer<ObjectVisitor.ConsumeVisitor<T>, WalkerContexts.ObjectContext<F>> handler) {
                before = before.andThen(handler);
                return rule(predicates, RuleType.EXEC, asHandler((v, c) -> null));
            }

            /**
             * Lets scan object recursivelly, do not process object itself.
             * Stop processing other rules.
             *
             * @return Builder
             */
            public Builder<T> follow() {
                return follow((target, ctx) -> null);
            }

            /**
             * Lets execute provided handler and scan object recursively.
             * Stop processing other rules.
             *
             * Handler receives description of visited field.
             * Handler's return value is sent to ConsumeVisitor
             *
             * @param handler handler
             * @return Builder
             */
            public Builder<T> follow(Function<WalkerContexts.ObjectContext<F>, T> handler) {
                return follow((target, ctx) -> handler.apply(ctx));
            }

            /**
             * Lets execute provided handler and scan object recursively.
             * Stop processing other rules.
             *
             * Handler receives two arguments:
             *  - collecting consumer, assocciated with visitor
             *  - description of visited field
             *
             * Handler's return value is sent to ConsumeVisitor
             * Please note that handler's body can send much more values to ConsumeVisitor
             *
             * @param handler handler
             * @return Builder
             */
            public Builder<T> follow(BiFunction<ObjectVisitor.ConsumeVisitor<T>, WalkerContexts.ObjectContext<F>, T> handler) {
                return rule(predicates, RuleType.FOLLOW, asHandler(handler));
            }

            /**
             * Lets execute provided handler and do not scan object recursively.
             * Stop processing other rules.
             *
             * Handler receives description of visited field.
             * Handler's return value is sent to ConsumeVisitor
             *
             * @param handler handler
             * @return Builder
             */
            public Builder<T> term(Function<WalkerContexts.ObjectContext<F>, T> handler) {
                return term((target, ctx) -> handler.apply(ctx));
            }

            /**
             * Lets execute provided handler and do not scan object recursively.
             * Stop processing other rules.
             *
             * Handler receives two arguments:
             *  - collecting consumer, assocciated with visitor
             *  - description of visited field
             *
             * Handler's return value is sent to ConsumeVisitor
             * Please note that handler's body can send much more values to ConsumeVisitor
             *
             * @param handler handler
             * @return Builder
             */
            public Builder<T> term(BiFunction<ObjectVisitor.ConsumeVisitor<T>, WalkerContexts.ObjectContext<F>, T> handler) {
                return rule(predicates, RuleType.TERM, asHandler(handler));
            }

            /**
             * Ignore value, do not scan object recursively.
             * Stop processing other rules.
             *
             * @return Builder
             */
            public Builder<T> skip() {
                return rule(predicates, RuleType.TERM, asHandler((v,c) -> null));
            }

            private RuleHandler<ObjectVisitor.ConsumeVisitor<T>, F, T> asHandler(BiFunction<ObjectVisitor.ConsumeVisitor<T>, WalkerContexts.ObjectContext<F>, T> handler) {
                return (target, ctx) -> {
                    before.accept(target, ctx);
                    return handler.apply(target, ctx);
                };
            }

            private <U> Builder<T> rule(List<Predicate<WalkerContexts.ObjectContext<?>>> predicates, RuleType type, RuleHandler<ObjectVisitor.ConsumeVisitor<T>, U, T> handler) {
                predicates.forEach(predicate -> rules.add(new RuleEntry<>(predicate, handler, type)));
                return Builder.this;
            }

        }

        /**
         * Rule builder, allowing composition of rule conditions and rule actions.
         *
         * RootRule is initial builder without any typesafe conditions.
         * It assumes that received field value is of type "Object"
         */
        public class RootRule extends AbstractRule<Object> {

            protected RootRule() {
                super(new ArrayList<>());
            }

            /**
             * Lets execute provided handler before final processing.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule by calling "exec", "follow" or "term".
             *
             * Handler receives description of visited field.
             * Handler can't return value.
             *
             * @param handler handler
             * @return Rule
             */
            public RootRule before(Consumer<WalkerContexts.ObjectContext<Object>> handler) {
                return before((target, ctx) ->  handler.accept(ctx));
            }

            /**
             * Lets execute provided handler before final processing.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule, by calling "exec", "follow" or "term".
             *
             * Handler receives two arguments:
             *  - collecting consumer, assocciated with visitor
             *  - description of visited field
             *
             * Handler can't return value, but handler's body can send values to ConsumeVisitor
             *
             * @param handler handler
             * @return Rule
             */
            public RootRule before(BiConsumer<ObjectVisitor.ConsumeVisitor<T>, WalkerContexts.ObjectContext<Object>> handler) {
                before = before.andThen(handler);
                return this;
            }

            /**
             * Match this rule if field has provided type.
             *
             * Rule with more than one condition, is executed if any condition is met
             *
             * @param type type
             * @param <Q> Q
             * @return Rule
             */
            public <Q> TypeRule<Q> match(Class<Q> type) {
                return match(IClass.typeinfo(type));
            }

            /**
             * Match this rule if field has provided type.
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param type type
             * @param <Q> Q
             * @return Rule
             */
            public <Q> TypeRule<Q> match(IClass<Q> type) {
                predicates.add(ctx -> ctx.isSubclass(type));
                return new TypeRule<>(this);
            }

            /**
             * Match this rule if field has provided name
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param name name
             * @return Rule
             */
            public MoreRule match(String name) {
                return match(ctx -> name.equals(ctx.name()));
            }

            /**
             * Match this rule if field has name matching provided Pattern
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param name name
             * @return Rule
             */
            public MoreRule match(Pattern name) {
                return match(ctx -> name.matcher(ctx.namePath()).matches());
            }

            /**
             * Match this rule if provided predicate returns true
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param predicate predicate
             * @return Rule
             */
            public MoreRule match(Predicate<WalkerContexts.ObjectContext<?>> predicate) {
                predicates.add(predicate);
                return new MoreRule(this);
            }

        }

        /**
         * Rule builder, allowing composition of rule conditions and rule actions.
         *
         * TypeRule is builder with exactly one typesafe condition.
         * It assumes that received field value is of type "F" defined by condition.
         *
         * @param <F> F
         */
        public class TypeRule<F> extends AbstractRule<F> {

            protected TypeRule(AbstractRule<?> source) {
                super(source.predicates);
            }

            /**
             * Lets execute provided handler before final processing.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule, by calling "exec", "follow" or "term".
             *
             * Handler receives description of visited field.
             * Handler can't return value.
             *
             * @param handler handler
             * @return Rule
             */
            public TypeRule<F> before(Consumer<WalkerContexts.ObjectContext<F>> handler) {
                return before((target, ctx) ->  handler.accept(ctx));
            }

            /**
             * Lets execute provided handler before final processing.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule, by calling "exec", "follow" or "term".
             *
             * Handler receives two arguments:
             *  - collecting consumer, assocciated with visitor
             *  - description of visited field
             *
             * Handler can't return value, but handler's body can send values to ConsumeVisitor
             *
             * @param handler handler
             * @return Rule
             */
            public TypeRule<F> before(BiConsumer<ObjectVisitor.ConsumeVisitor<T>, WalkerContexts.ObjectContext<F>> handler) {
                before = before.andThen(handler);
                return this;
            }

            /**
             * Match this rule if field has provided type.
             *
             * Rule with more than one condition, is executed if any condition is met
             *
             * @param type type
             * @return Rule
             */
            public MoreRule match(Class<?> type) {
                return match(IClass.typeinfo(type));
            }

            /**
             * Match this rule if field has provided type.
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param type type
             * @return Rule
             */
            public MoreRule match(IClass<?> type) {
                predicates.add(ctx -> ctx.isSubclass(type));
                return new MoreRule(this);
            }

            /**
             * Match this rule if field has provided name
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param name name
             * @return Rule
             */
            public MoreRule match(String name) {
                return match(ctx -> name.equals(ctx.name()));
            }

            /**
             * Match this rule if field has name matching provided Pattern
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param name name
             * @return Rule
             */
            public MoreRule match(Pattern name) {
                return match(ctx -> name.matcher(ctx.namePath()).matches());
            }

            /**
             * Match this rule if provided predicate returns true
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param predicate predicate
             * @return Rule
             */
            public MoreRule match(Predicate<WalkerContexts.ObjectContext<?>> predicate) {
                predicates.add(predicate);
                return new MoreRule(this);
            }

        }

        /**
         * Rule builder, allowing composition of rule conditions and rule actions.
         *
         * MoreRule is builder with more than one conditions.
         * It assumes that received field value is of type "Object"
         *
         */
        public class MoreRule extends AbstractRule<Object> {

            protected MoreRule(AbstractRule<?> source) {
                super(source.predicates);
            }

            /**
             * Lets execute provided handler before final processing.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule, by calling "exec", "follow" or "term".
             *
             * Handler receives description of visited field.
             * Handler can't return value.
             *
             * @param handler handler
             * @return Rule
             */
            public MoreRule before(Consumer<WalkerContexts.ObjectContext<Object>> handler) {
                return before((target, ctx) ->  handler.accept(ctx));
            }

            /**
             * Lets execute provided handler before final processing.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule, by calling "exec", "follow" or "term".
             *
             * Handler receives two arguments:
             *  - collecting consumer, assocciated with visitor
             *  - description of visited field
             *
             * Handler can't return value, but handler's body can send values to ConsumeVisitor
             *
             * @param handler handler
             * @return Rule
             */
            public MoreRule before(BiConsumer<ObjectVisitor.ConsumeVisitor<T>, WalkerContexts.ObjectContext<Object>> handler) {
                before = before.andThen(handler);
                return this;
            }

            /**
             * Match this rule if field has provided type.
             *
             * Rule with more than one condition, is executed if any condition is met
             *
             * @param type type
             * @return Rule
             */
            public MoreRule match(Class<?> type) {
                return match(IClass.typeinfo(type));
            }

            /**
             * Match this rule if field has provided type.
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param type type
             * @return Rule
             */
            public MoreRule match(IClass<?> type) {
                return match(ctx -> ctx.isSubclass(type));
            }

            /**
             * Match this rule if field has provided name
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param name name
             * @return Rule
             */
            public MoreRule match(String name) {
                return match(ctx -> name.equals(ctx.name()));
            }

            /**
             * Match this rule if field has name matching provided Pattern
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param name name
             * @return Rule
             */
            public MoreRule match(Pattern name) {
                return match(ctx -> name.matcher(ctx.namePath()).matches());
            }

            /**
             * Match this rule if provided predicate returns true
             *
             * Rule with more than one condition is executed if any condition is met
             *
             * @param predicate predicate
             * @return Rule
             */
            public MoreRule match(Predicate<WalkerContexts.ObjectContext<?>> predicate) {
                predicates.add(predicate);
                return this;
            }

        }

    }

    private class ConsumeRuleVisitor extends ObjectVisitor.ConsumeVisitor<T> {

        public ConsumeRuleVisitor(Consumer<T> target) {
            super(target, properties);
        }

        @Override
        public boolean visitObject(WalkerContexts.ObjectContext<?> context) {

            for (RuleEntry<T> rule : rules) {
                if (rule.test(context)) {
                    T item = rule.consume(this, context);
                    if(item != null) {
                        accept(item);
                    }
                    switch (rule.type) {
                        case FOLLOW:
                            return true;
                        case TERM:
                            return false;
                        case EXEC:
                            // continue processing
                    }
                }
            }
            return true;
        }
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    @Data
    private static class RuleEntry<T> {
        private final Predicate<WalkerContexts.ObjectContext<?>> predicate;
        private final RuleHandler handler;
        private final RuleType type;

        public boolean test(WalkerContexts.ObjectContext<?> context) {
            return predicate.test(context);
        }

        public T consume(ObjectVisitor.ConsumeVisitor<T> visitor, WalkerContexts.ObjectContext<?> context) {
            return (T)handler.apply(visitor, context);
        }
    }

    private interface RuleHandler<V, U, T> {
        T apply(V visitor, WalkerContexts.ObjectContext<U> context);
    }

    private enum RuleType {
        FOLLOW, TERM, EXEC
    }

}
