package net.ranides.assira.reflection.walker;

import lombok.experimental.UtilityClass;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.walker.WalkerContexts.ArrayContext;
import net.ranides.assira.reflection.walker.WalkerContexts.ObjectContext;
import net.ranides.assira.reflection.walker.rules.CollectRules;
import net.ranides.assira.reflection.walker.rules.PrintRules;
import net.ranides.assira.text.FormatHex;

import javax.xml.datatype.XMLGregorianCalendar;
import java.net.URL;
import java.time.temporal.Temporal;
import java.util.*;

/**
 * This utility class contains default rules for ObjectWalker.
 *
 * WalkerRules represent configuration which can be used to create specific ObjectVisitor
 * just before inspection.
 */
@UtilityClass
public class WalkerRules {

    /**
     * Default set of rules, which can be used to create printing ObjectVisitor.
     */
    public static final PrintRules PRINTER = new PrintRules.Builder()
        .property("PATH", "{id,text,4}.{namePath,text,-50}")
        .rule()
            .match(ObjectContext::isVisited)
            .term("{PATH} <ref> = #{ref}")
        .rule()
            .match(ctx -> ctx.depth() > 128)
            .term("{PATH} ...")
        .rule()
            .match(ObjectContext::isNull)
            .term("{PATH} {typeName} = <null>")
        .rule()
            .match(CharSequence.class)
            .term("{PATH} {actual.shortName} = '{value}'")
        .rule()
            .match(byte[].class)
            .format(ctx -> FormatHex.format(ctx.value()))
            .term("{PATH} {actual.shortName} = '{value}'")
        .rule()
            .match(char[].class)
            .format(ctx -> String.valueOf(ctx.value()))
            .term("{PATH} {actual.shortName} = '{value}'")
        .rule()
            .match(ArrayContext.class::isInstance)
            .hint("{PATH} {actual.shortName}.size = {size}")
        .rule()
            .match(List.class)
            .match(Collection.class)
            .match(Map.class)
            .hint("{PATH} {typeName}.size = {size}")
        .rule()
            .match(java.util.Date.class)
            .match(java.sql.Date.class)
            .match(XMLGregorianCalendar.class)
            .match(Temporal.class)
            .match(Number.class)
            .match(Enum.class)
            .match(BitSet.class)
            .match(URL.class)
            .match(StackTraceElement.class)
            .match(ObjectContext::isPrimitive)
            .term("{PATH} {actual.shortName} = {value}")
        .rule()
            .match(Object.class)
            .hint("{PATH} {actual.shortName} = {value}")
        .prepare();

    /**
     * Default set of rules, which can be used to create collecting ObjectVisitor.
     */
    public static final CollectRules<WalkerEntry> COLLECT = new CollectRules.Builder<WalkerEntry>()
            .rule()
                .match(ObjectContext::isVisited)
                .skip()
            .rule()
                .match(ctx -> ctx.depth() > 128)
                .skip()
            .rule()
                .match(ObjectContext::isNull)
                .term(WalkerEntry::new)
            .rule()
                .match(byte[].class)
                .term(WalkerEntry::new)
            .rule()
                .match(char[].class)
                .term(WalkerEntry::new)
            .rule()
                .match(CharSequence.class)
                .match(java.util.Date.class)
                .match(java.sql.Date.class)
                .match(XMLGregorianCalendar.class)
                .match(Temporal.class)
                .match(Number.class)
                .match(Enum.class)
                .match(BitSet.class)
                .match(URL.class)
                .match(StackTraceElement.class)
                .match(ObjectContext::isPrimitive)
                .term(WalkerEntry::new)
            .rule()
                .match(Object.class)
                .follow(WalkerEntry::new)
            .prepare();

    /**
     * Returns new builder for custom PrintRules
     *
     * @return builder
     */
    public static PrintRules.Builder printer() {
        return new PrintRules.Builder();
    }

    /**
     * Returns new builder for custom CollectRules
     *
     * @return builder
     */
    public static CollectRules.Builder<WalkerEntry> collectEntries() {
        return new CollectRules.Builder<>();
    }

    /**
     * Returns new builder for custom CollectRules
     *
     * @return builder
     */
    public static CollectRules.Builder<String> collectLines() {
        return new CollectRules.Builder<>();
    }

    /**
     * Returns new builder for custom CollectRules
     *
     * @param type type
     * @param <T> T
     * @return builder
     */
    public static <T> CollectRules.Builder<T> collect(Class<T> type) {
        return new CollectRules.Builder<>();
    }

    /**
     * Returns new builder for custom CollectRules
     *
     * @param type type
     * @param <T> T
     * @return builder
     */
    public static <T> CollectRules.Builder<T> collect(IClass<T> type) {
        return new CollectRules.Builder<>();
    }

    /**
     * It is simplified representation of ObjectContext.
     * It is just key-value pair, which could be stored inside map.
     *
     * Visitor generated by CollectRules stores entries of this class by default.
     */
    public static class WalkerEntry implements Map.Entry<String, Object> {

        private final long ref;
        private final String path;
        private final Object value;

        /**
         * Creates new entry from provided context.
         * Effectively it is simplified snapshot of the context.
         *
         * @param ctx ctx
         */
        public WalkerEntry(ObjectContext<?> ctx) {
            this.ref = ctx.ref();
            this.path = ctx.namePath();
            this.value = ctx.value();
        }

        @Override
        public String getKey() {
            return path;
        }

        @Override
        public Object getValue() {
            return value;
        }

        @Override
        public Object setValue(Object value) {
            throw new UnsupportedOperationException("Immutable entry");
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            WalkerEntry that = (WalkerEntry) o;
            return this.ref == that.ref || Objects.equals(this.value, that.value);
        }

        @Override
        public int hashCode() {
            return value==null ? Objects.hash(ref) : Objects.hash(value);
        }

        @Override
        public String toString() {
            if(value instanceof char[]) {
                return "{" + path + "=" + String.valueOf((char[])value) + "}";
            }
            if(value!=null && value.getClass().isArray()) {
                return "{" + path + "=<array>}";
            }
            return "{" + path + "=" + value + "}";
        }
    }

}
