package net.ranides.assira.reflection.walker;

import net.ranides.assira.collection.arrays.NativeArray;
import net.ranides.assira.collection.prototype.PrototypeMap;
import net.ranides.assira.generic.LazyCaller;
import net.ranides.assira.generic.LazyCallers;
import net.ranides.assira.generic.LazySupplier;
import net.ranides.assira.generic.LazySuppliers;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.reflection.*;
import net.ranides.assira.reflection.walker.WalkerContexts.ArrayContext;
import net.ranides.assira.reflection.walker.WalkerContexts.CollectionContext;
import net.ranides.assira.reflection.walker.WalkerContexts.ListContext;
import net.ranides.assira.reflection.walker.WalkerContexts.MapContext;
import net.ranides.assira.reflection.walker.WalkerContexts.ObjectContext;

import java.util.*;
import java.util.function.Predicate;

/**
 * ObjectWalker is a class for deep inspection of objects.
 * It implements listing all fields, optionally recursively.
 *
 * ObjectWalker takes ObjectVisitor as input. This visitor has more than one method
 * and implement separate behaviour for pure objects, arrays, lists and maps.
 *
 * Every visited field is represented as ObjectContext. This contenxt contains information
 * about type, values, enclosing object etc.
 */
public class ObjectWalker {

    private final Map<Object, Long> visited = new IdentityHashMap<>();

    private long counter = 0;

    private final Object root;

    private final ObjectVisitor visitor;

    private ObjectWalker(Object root, ObjectVisitor visitor) {
        this.root = root;
        this.visitor = visitor;
    }

    /**
     * Inspects all fields inside input.
     * Every visited field is passed to "visitor" as ObjectContext.
     *
     * If visitor returns "true", then visited field is inspected recursively.
     *
     * @param input input
     * @param visitor visitor
     */
    public static void walk(Object input, Predicate<ObjectContext<?>> visitor) {
        walk(input, "var", visitor);
    }

    /**
     * Inspects all fields inside input.
     * Every visited field is passed to "visitor" as ObjectContext.
     *
     * If visitor returns "true", then visited field is inspected recursively.
     *
     * Please note that ObjectVisitor must implement ResolveContext interface.
     * This context stores public properties accessible from every ObjectContext (using PrototypeMap).
     *
     * @param input input
     * @param visitor visitor
     */
    public static void walk(Object input, ObjectVisitor visitor) {
        walk(input, "var", visitor);
    }

    /**
     * Inspects all fields inside input.
     * Every visited field is passed to "visitor" as ObjectContext.
     * String "root" is used when asking context for e.g. "namePath"
     *
     * If visitor returns "true", then visited field is inspected recursively.
     *
     * @param input input
     * @param root root
     * @param visitor visitor
     */
    public static void walk(Object input, String root, Predicate<ObjectContext<?>> visitor) {
        walk(input, root, ObjectVisitor.of(visitor));
    }

    /**
     * Inspects all fields inside input.
     * Every visited field is passed to "visitor" as ObjectContext.
     * String "root" is used when asking context for e.g. "namePath"
     *
     * If visitor returns "true", then visited field is inspected recursively.
     *
     * Please note that ObjectVisitor must implement ResolveContext interface.
     * This context stores public properties accessible from every ObjectContext (using PrototypeMap).
     *
     * @param input input
     * @param root root
     * @param visitor visitor
     */
    public static void walk(Object input, String root, ObjectVisitor visitor) {
        new ObjectWalker(input, visitor).start(root);
    }

    private void start(String rootname) {
        CObject<Object> bootstrap = new CObject<>(null, null, IClass.OBJECT, null);
        ObjectContext<Object> context = bootstrap.context(rootname, IClass.typefor(root), root);
        context.accept(visitor);
    }

    private void mark(ObjectContext<?> ctx) {
        if(!ctx.isPrimitive()) {
            visited.put(ctx.value(), ctx.id());
        }
    }

    private abstract class CAbstract<T> implements ObjectContext<T> {
        private final ObjectContext<?> parent;
        private final String name;
        private final IClass<?> declared;
        private final IClass<?> actual;
        private final Object value;
        private final long id;
        private final int depth;

        private final Ref<IField> field;

        private final LazyCaller<String> typePath = LazyCallers.concurrent();
        private final LazyCaller<String> namePath = LazyCallers.concurrent();
        private final LazyCaller<String> idPath = LazyCallers.concurrent();
        private final LazyCaller<List<IClass<?>>> typeList = LazyCallers.concurrent();
        private final LazyCaller<List<String>> nameList = LazyCallers.concurrent();

        private final LazySupplier<PrototypeMap> custom = LazySuppliers.concurrent(() -> {
            return new PrototypeMap(visitor.scope().getResolveMap());
        });

        public CAbstract(ObjectContext<?> parent, IField field, Object value) {
            this.field = Ref.of(field);
            this.parent = parent;
            this.name = field.name();
            this.declared = field.type();
            this.value = value;
            this.actual = IClass.typefor(value);
            this.id = counter++;
            this.depth = (parent == null) ? 0 : parent.depth() + 1;
        }

        public CAbstract(ObjectContext<?> parent, String name, IClass<?> declared, Object value) {
            this.field = Ref.empty();
            this.parent = parent;
            this.name = name;
            this.declared = declared;
            this.value = value;
            this.actual = IClass.typefor(value);
            this.id = counter++;
            this.depth = (parent == null) ? 0 : parent.depth() + 1;
        }

        @Override
        public ObjectContext<?> context(IField field) {
            Object newValue = WalkerUtils.UNDEFINED;
            if(!isNull()) {
                try {
                    newValue = field.accessible().get(value());
                } catch (Exception cause) {
                    // ignore
                }
            }

            IClass<?> declared = field.type();
            IClass<?> actual = IClass.typefor(newValue);

            if(WalkerUtils.isList(declared, actual)) {
                return new CList<>(this, field, newValue);
            }
            if(WalkerUtils.isCollection(declared, actual)) {
                return new CCollection<>(this, field, newValue);
            }
            if(WalkerUtils.isMap(declared, actual)) {
                return new CMap<>(this, field, newValue);
            }
            if(WalkerUtils.isArray(declared, actual)) {
                return new CArray<>(this, field, newValue);
            }
            return new CObject<>(this, field, newValue);
        }


        @SuppressWarnings({"unchecked", "rawtypes"})
        @Override
        public <S> ObjectContext<S> context(String name, IClass<S> declared, Object value) {
            IClass<?> actual = IClass.typefor(value);

            if(WalkerUtils.isList(declared, actual)) {
                return new CList(this, name, WalkerUtils.asList(declared), value);
            }
            if(WalkerUtils.isCollection(declared, actual)) {
                return new CCollection(this, name, WalkerUtils.lower(WalkerUtils.COLLECTION, declared), value);
            }
            if(WalkerUtils.isMap(declared, actual)) {
                return new CMap(this, name, WalkerUtils.lower(WalkerUtils.MAP, declared), value);
            }
            if(WalkerUtils.isArray(declared, actual)) {
                return new CArray<>(this, name, declared, value);
            }
            return new CObject<>(this, name, declared, value);
        }

        @Override
        public int depth() {
            return depth;
        }

        @Override
        public ObjectContext<?> parent() {
            return parent;
        }

        @Override
        public String typeName() {
            return declared.shortName();
        }

        @Override
        public String name() {
            return name;
        }

        @SuppressWarnings("unchecked")
        @Override
        public IClass<T> type() {
            return (IClass<T>)declared;
        }

        @SuppressWarnings("unchecked")
        @Override
        public IClass<T> actual() {
            return (IClass<T>)actual;
        }

        @SuppressWarnings("unchecked")
        @Override
        public T value() {
            return (T)value;
        }


        @Override
        public Map<Object, Object> getResolveMap() {
            return custom.compute();
        }

        @Override
        public void custom(Map<String, ?> values) {
            custom.compute().putAll(values);
        }

        @Override
        public void custom(String key, Object value) {
            custom.compute().put(key, value);
        }

        @Override
        public long id() {
            return id;
        }

        @Override
        public long ref() {
            return ValueUtils.or(visited.get(value), id);
        }

        @Override
        public long hash() {
            return System.identityHashCode(value);
        }

        @Override
        public String typePath() {
            return typePath.computeIfAbsent(() -> WalkerUtils.typePath(this));
        }

        @Override
        public List<IClass<?>> typeList() {
            return typeList.computeIfAbsent(() -> WalkerUtils.typeList(this));
        }

        @Override
        public String namePath() {
            return namePath.computeIfAbsent(() -> WalkerUtils.namePath(this));
        }

        @Override
        public List<String> nameList() {
            return nameList.computeIfAbsent(() -> WalkerUtils.nameList(this));
        }

        @Override
        public String idPath() {
            return idPath.computeIfAbsent(() -> WalkerUtils.idPath(this));
        }

        @Override
        public boolean isRoot() {
            return value == root || parent == null;
        }

        @Override
        public boolean isField() {
            return field.isPresent();
        }

        @Override
        public boolean isTransient() {
            return field.filter(IField::isTransient).isPresent();
        }

        @Override
        public boolean isVisited() {
            return visited.containsKey(value);
        }

        @Override
        public boolean isDefined() {
            return value != WalkerUtils.UNDEFINED;
        }

        @Override
        public boolean isPrimitive() {
            return declared.isPrimitive() || actual.isBoxed();
        }

        @Override
        public boolean isNull() {
            return value == null;
        }

        @Override
        public boolean isSubclass() {
            return declared.isInstance(value) && !declared.equals(actual);
        }

        @Override
        public boolean isSubclass(IClass<?> type) {
            return type().isSubclass(type) || actual().isSubclass(type);
        }

        @Override
        public String toString() {
            return "{" + name() + "=" + value() + "}";
        }
    }

    private class CObject<T> extends CAbstract<T> implements ObjectContext<T> {
        public CObject(ObjectContext<?> parent, IField field, Object value) {
            super(parent, field, value);
        }

        public CObject(ObjectContext<?> parent, String name, IClass<T> declared, Object value) {
            super(parent, name, declared, value);
        }

        @Override
        public IClass<? extends ObjectContext<?>> kind() {
            return ObjectContext.KIND;
        }

        @Override
        public void accept(ObjectVisitor visitor) {
            try {
                if (!visitor.visitObject(this)) {
                    return;
                }

                if (!isNull() && !isPrimitive()) {
                    mark(this);
                    actual()
                        .fields()
                        .require(IAttribute.ANY)
                        .discard(IAttribute.STATIC)
                        .stream()
                        .sort(Comparator.comparing(IElement::name))
                        .forEach(field -> context(field).accept(visitor));
                }
            } finally {
                mark(this);
            }
        }

    }

    private class CArray<T> extends CAbstract<T> implements ArrayContext<T> {

        private final NativeArray array;

        @Override
        public IClass<? extends ObjectContext<?>> kind() {
            return ArrayContext.KIND;
        }

        public CArray(ObjectContext<?> parent, IField field, Object value) {
            super(parent, field, value);
            this.array = wrap(field.type(), value());
        }

        public CArray(ObjectContext<?> parent, String name, IClass<T> type, Object value) {
            super(parent, name, type, value);
            this.array = wrap(type, value());
        }

        @Override
        public IClass<?> component() {
            return type().component();
        }

        @Override
        public int size() {
            return array.size();
        }

        @Override
        public Object at(int index) {
            return array.get(index);
        }

        @Override
        public void accept(ObjectVisitor visitor) {
            try {
                if (!visitor.visitArray(this)) {
                    return;
                }
                if (!isNull()) {
                    mark(this);
                    IClass<?> cmp = component();
                    for (int index = 0, n = size(); index < n; index++) {
                        context("[" + index + "]", cmp, at(index)).accept(visitor);
                    }
                }
            } finally {
                mark(this);
            }
        }

    }

    private static NativeArray wrap(IClass<?> type, Object input) {
        return input == null ? NativeArray.allocate(type.component().raw(), 0) : NativeArray.wrap(input);
    }

    private class CList<T extends List<?>> extends CAbstract<T> implements ListContext<T> {

        private final LazyCaller<IClass<?>> component = LazyCallers.concurrent();

        @Override
        public IClass<? extends ObjectContext<?>> kind() {
            return ListContext.KIND;
        }

        public CList(ObjectContext<?> parent, IField field, Object value) {
            super(parent, field, value);
        }

        public CList(ObjectContext<?> parent, String name, IClass<?> type, Object value) {
            super(parent, name, type, value);
        }

        @Override
        public String typeName() {
            return type().shortName() + "<" + component().shortName() + ">";
        }

        @Override
        public IClass<?> component() {
            return component.computeIfAbsent(() -> {
                if(WalkerUtils.isList(type())) {
                    return type().params().get(0);
                }
                if(WalkerUtils.isList(actual())) {
                    return actual().params().get(0);
                }
                return IClass.OBJECT;
            });
        }

        @Override
        public int size() {
            return value().size();
        }

        @Override
        public Object at(int index) {
            return value().get(index);
        }

        @Override
        public Iterator<?> iterator() {
            return value().iterator();
        }

        @Override
        public void accept(ObjectVisitor visitor) {
            try {
                if (!visitor.visitList(this)) {
                    return;
                }
                if (!isNull()) {
                    mark(this);
                    IClass<?> cmp = component();
                    for (int index = 0, n = size(); index < n; index++) {
                        context("[" + index + "]", cmp, at(index)).accept(visitor);
                    }
                }
            } finally {
                mark(this);
            }
        }
    }

    private class CCollection<T extends Collection<?>> extends CAbstract<T> implements CollectionContext<T> {

        private final LazyCaller<IClass<?>> component = LazyCallers.concurrent();

        public CCollection(ObjectContext<?> parent, IField field, Object value) {
            super(parent, field, value);
        }

        public CCollection(ObjectContext<?> parent, String name, IClass<T> type, Object value) {
            super(parent, name, type, value);
        }

        @Override
        public IClass<? extends ObjectContext<?>> kind() {
            return CollectionContext.KIND;
        }

        @Override
        public String typeName() {
            return type().shortName() + "<" + component().shortName() + ">";
        }

        @Override
        public IClass<?> component() {
            return component.computeIfAbsent(() -> {
                if(WalkerUtils.isCollection(type())) {
                    return type().params().get(0);
                }
                if(WalkerUtils.isCollection(actual())) {
                    return actual().params().get(0);
                }
                return IClass.OBJECT;
            });
        }

        @Override
        public int size() {
            return value().size();
        }

        @Override
        public Iterator<?> iterator() {
            return value().iterator();
        }

        @Override
        public void accept(ObjectVisitor listener) {
            try {
                if (!listener.visitCollection(this)) {
                    return;
                }
                if (!isNull()) {
                    mark(this);
                    IClass<?> cmp = component();
                    for (Object item : value()) {
                        context("[?]", cmp, item).accept(listener);
                    }
                }
            } finally {
                mark(this);
            }
        }
    }

    private class CMap<T extends Map<?,?>> extends CAbstract<T> implements MapContext<T> {

        private final LazyCaller<IClass<?>> keyComponent = LazyCallers.concurrent();
        private final LazyCaller<IClass<?>> valueComponent = LazyCallers.concurrent();

        public CMap(ObjectContext<?> parent, IField field, Object value) {
            super(parent, field, value);
        }

        public CMap(ObjectContext<?> parent, String name, IClass<T> type, Object value) {
            super(parent, name, type, value);
        }

        @Override
        public IClass<? extends ObjectContext<?>> kind() {
            return MapContext.KIND;
        }

        @Override
        public int size() {
            return value().size();
        }

        @Override
        public String typeName() {
            return type().shortName() + "<" + keyComponent().shortName() + "," + valueComponent().shortName() + ">";
        }

        @Override
        public IClass<?> keyComponent() {
            return keyComponent.computeIfAbsent(() -> {
                if(WalkerUtils.isMap(type())) {
                    return type().params().get(0);
                }
                if(WalkerUtils.isMap(actual())) {
                    return actual().params().get(0);
                }
                return IClass.OBJECT;
            });
        }

        @Override
        public IClass<?> valueComponent() {
            return valueComponent.computeIfAbsent(() -> {
                if(WalkerUtils.isMap(type())) {
                    return type().params().get(1);
                }
                if(WalkerUtils.isMap(actual())) {
                    return actual().params().get(1);
                }
                return IClass.OBJECT;
            });
        }

        @Override
        public Set<? extends Map.Entry<?, ?>> entries() {
            return value().entrySet();
        }

        @Override
        public void accept(ObjectVisitor listener) {
            try {
                if (!listener.visitMap(this)) {
                    return;
                }
                if (!isNull()) {
                    mark(this);
                    if(value() instanceof SortedMap || value() instanceof LinkedHashMap) {
                        exactIterate(listener);
                    } else {
                        lexicalIterate(listener);

                    }
                }
            } finally {
                mark(this);
            }
        }

        private void exactIterate(ObjectVisitor listener) {
            IClass<?> cmp = valueComponent();
            for (Map.Entry<?, ?> entry : value().entrySet()) {
                context("[" + entry.getKey() + "]", cmp, entry.getValue()).accept(listener);
            }
        }

        private void lexicalIterate(ObjectVisitor listener) {
            IClass<?> cmp = valueComponent();
            T map = value();
            ArrayList<?> keys = new ArrayList<>(map.keySet());

            keys.sort(Comparator.comparing(String::valueOf));
            for(Object key : keys) {
                context("[" + key + "]", cmp, map.get(key)).accept(listener);
            }
        }
    }

}
