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

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
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.Charsets;
import net.ranides.assira.text.ResolveFormat;

import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.function.*;
import java.util.regex.Pattern;

/**
 * PrintRules is configuration used to construct "printing" ObjectVisitor.
 */
public class PrintRules {

    private final CollectRules<String> rules;

    protected PrintRules(CollectRules<String> rules) {
        this.rules = new CollectRules<>(rules);
    }

    /**
     * Creates new visitor printing results into target OutputStream.
     * It uses UTF8 charset.
     *
     * @param target target
     * @return visitor
     */
    public ObjectVisitor visitor(OutputStream target) {
        return visitor(new OutputStreamWriter(target, Charsets.UTF8));
    }

    /**
     * Creates new visitor printing results into target Writer
     *
     * @param target target
     * @return visitor
     */
    public ObjectVisitor visitor(Writer target) {
        return rules.visitor(new PrintWriter(target)::println);
    }

    /**
     * This fluent builder allows to construct new printing configuration
     */
    public static class Builder {

        private final CollectRules.Builder<String> builder = new CollectRules.Builder<>();

        /**
         * Creates immutable PrintRules
         *
         * @return PrintRules
         */
        public PrintRules prepare() {
            return new PrintRules(builder.prepare());
        }

        /**
         * 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 this
         */
        public Builder property(String name, Object value) {
            builder.property(name, value);
            return this;
        }

        /**
         * Defines custom property with "name" which will be accessible from every ObjectContext.
         * "value" string will be converted to {@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 this
         */
        public Builder property(String name, String value) {
            builder.property(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(builder.new RootRule());
        }

        /**
         * Copies all rules from provided "parent" into this builder.
         *
         * @param parent parent
         * @return Builder
         */
        public Builder rules(PrintRules parent) {
            this.builder.rules(parent.rules);
            return this;
        }

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

            protected abstract CollectRules.Builder<String>.AbstractRule<F> getDelegate();

            /**
             * Lets format field value using provided pattern (ResolveFormat).
             * Do not stop processing other rules, some of them can match this field.
             *
             * Formatted value is stored inside field context as "value" property.
             * It overrides previously stored property.
             *
             * @param text text
             * @return Builder
             */
            public Builder text(String text) {
                ResolveFormat fmt = ResolveFormat.compile(text);
                return text((target, ctx) -> fmt.format(ctx));
            }

            /**
             * Lets format field value using provided handler
             * Do not stop processing other rules, some of them can match this field.
             *
             * Handler receives description of visited field.
             *
             * Formatted value is stored inside field context as "value" property.
             * It overrides previously stored property.
             *
             * @param handler handler
             * @return Builder
             */
            public Builder text(Function<WalkerContexts.ObjectContext<F>, String> handler) {
                return text((target, ctx) -> handler.apply(ctx));
            }

            /**
             * Lets format field value using provided handler
             * Do not stop processing other rules, some of them can match this field.
             *
             * Handler receives two arguments:
             *  - printing consumer, assocciated with visitor
             *  - description of visited field
             *
             * Formatted value is stored inside field context as "value" property.
             * It overrides previously stored property.
             *
             * Please note that handler's body can send formatted lines to printing consumer
             *
             * @param handler handler
             * @return Builder
             */
            public Builder text(BiFunction<ObjectVisitor.ConsumeVisitor<String>, WalkerContexts.ObjectContext<F>, String> handler) {
                getDelegate().exec((v, c) -> c.custom("value", handler.apply(v, c)));
                return Builder.this;
            }

            /**
             * Lets format field value using provided pattern (ResolveFormat).
             * Scan object recursively, stop processing other rules.
             *
             * Formatted value is sent to printing consumer
             *
             * @param text text
             * @return Builder
             */
            public Builder hint(String text) {
                ResolveFormat fmt = ResolveFormat.compile(text);
                return hint((ctx) -> fmt.format(ctx));
            }

            /**
             * Lets format field value using provided handler.
             * Scan object recursively, stop processing other rules.
             *
             * Handler receives description of visited field.
             *
             * Formatted value is sent to printing consumer.
             *
             * @param handler handler
             * @return Builder
             */
            public Builder hint(Function<WalkerContexts.ObjectContext<F>, String> handler) {
                return hint((target, ctx) -> handler.apply(ctx));
            }

            /**
             * Lets format field value using provided handler
             * Scan object recursively, stop processing other rules.
             *
             * Handler receives two arguments:
             *  - printing consumer, assocciated with visitor
             *  - description of visited field
             *
             * Formatted value is sent to printing visitor
             * Please note that handler's body can send much more lines to printing consumer
             *
             * @param handler handler
             * @return Builder
             */
            public Builder hint(BiFunction<ObjectVisitor.ConsumeVisitor<String>, WalkerContexts.ObjectContext<F>, String> handler) {
                getDelegate().follow(handler);
                return Builder.this;
            }

            /**
             * Lets format field value using provided pattern (ResolveFormat).
             * Do not scan object recursively, stop processing other rules.
             *
             * Formatted value is sent to printing consumer
             *
             * @param text text
             * @return Builder
             */
            public Builder term(String text) {
                ResolveFormat fmt = ResolveFormat.compile(text);
                return term((target, ctx) -> fmt.format(ctx));
            }

            /**
             * Lets format field value using provided handler.
             * Do not scan object recursively, stop processing other rules.
             *
             * Handler receives description of visited field.
             *
             * Formatted value is sent to printing consumer.
             *
             * @param handler handler
             * @return Builder
             */
            public Builder term(Function<WalkerContexts.ObjectContext<F>, String> handler) {
                return term((target, ctx) -> handler.apply(ctx));
            }

            /**
             * Lets format field value using provided handler
             * Do not scan object recursively, stop processing other rules.
             *
             * Handler receives two arguments:
             *  - printing consumer, assocciated with visitor
             *  - description of visited field
             *
             * Formatted value is sent to printing visitor
             * Please note that handler's body can send much more lines to printing consumer
             *
             * @param handler handler
             * @return Builder
             */
            public Builder term(BiFunction<ObjectVisitor.ConsumeVisitor<String>, WalkerContexts.ObjectContext<F>, String> handler) {
                getDelegate().term(handler);
                return Builder.this;
            }

            /**
             * Ignore value, do not scan object recursively.
             * Stop processing other rules.
             *
             * @return Builder
             */
            public Builder skip() {
                getDelegate().skip();
                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"
         */
        @RequiredArgsConstructor
        public class RootRule extends AbstractRule<Object> {

            @Getter(AccessLevel.PROTECTED)
            private final CollectRules.Builder<String>.RootRule delegate;

            /**
             * Lets format field value using provided pattern (ResolveFormat).
             *
             * Formatted value is stored inside field context as "value" property.
             * It overrides previously stored property.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule by calling "text", "hint" or "term".
             *
             * @param text text
             * @return Rule
             */
            public RootRule format(String text) {
                return format(ResolveFormat.compile(text)::format);
            }

            /**
             * Lets format field value using provided handler.
             *
             * Formatted value is stored inside field context as "value" property.
             * It overrides previously stored property.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule by calling "text", "hint" or "term".
             *
             * @param formatter formatter
             * @return Rule
             */
            public RootRule format(Function<WalkerContexts.ObjectContext<Object>, String> formatter) {
                text(formatter);
                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 <T> T
             * @return Rule
             */
            public <T> TypeRule<T> match(Class<T> 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 <T> T
             * @return Rule
             */
            public <T> TypeRule<T> match(IClass<T> type) {
                return new TypeRule<>(delegate.match(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) {
                return new MoreRule(delegate.match(predicate));
            }

        }

        /**
         * 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
         */
        @RequiredArgsConstructor
        public class TypeRule<F> extends AbstractRule<F> {

            @Getter(AccessLevel.PROTECTED)
            private final CollectRules.Builder<String>.TypeRule<F> delegate;

            /**
             * Lets format field value using provided pattern (ResolveFormat).
             *
             * Formatted value is stored inside field context as "value" property.
             * It overrides previously stored property.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule by calling "text", "hint" or "term".
             *
             * @param text text
             * @return Rule
             */
            public TypeRule<F> format(String text) {
                return format(ResolveFormat.compile(text)::format);
            }

            /**
             * Lets format field value using provided handler.
             *
             * Formatted value is stored inside field context as "value" property.
             * It overrides previously stored property.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule by calling "text", "hint" or "term".
             *
             * @param formatter formatter
             * @return Rule
             */
            public TypeRule<F> format(Function<WalkerContexts.ObjectContext<F>, String> formatter) {
                text(formatter);
                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 new MoreRule(delegate.match(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 new MoreRule(delegate.match(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) {
                return new MoreRule(delegate.match(predicate));
            }

        }

        /**
         * 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"
         *
         */
        @RequiredArgsConstructor
        public class MoreRule extends AbstractRule<Object> {

            @Getter(AccessLevel.PROTECTED)
            private final CollectRules.Builder<String>.MoreRule delegate;

            /**
             * Lets format field value using provided pattern (ResolveFormat).
             *
             * Formatted value is stored inside field context as "value" property.
             * It overrides previously stored property.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule by calling "text", "hint" or "term".
             *
             * @param text text
             * @return Rule
             */
            public MoreRule format(String text) {
                return format(ResolveFormat.compile(text)::format);
            }

            /**
             * Lets format field value using provided handler.
             *
             * Formatted value is stored inside field context as "value" property.
             * It overrides previously stored property.
             *
             * This method does not finish configuration of rule:
             * You must define final behaviour of rule by calling "text", "hint" or "term".
             *
             * @param formatter formatter
             * @return Rule
             */
            public MoreRule format(Function<WalkerContexts.ObjectContext<Object>, String> formatter) {
                text(formatter);
                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) {
                delegate.match(type);
                return 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) {
                delegate.match(predicate);
                return this;
            }

        }

    }

}
