/**
 * Copyright 2004 - 2017 Syncleus, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.syncleus.ferma.typeresolvers;

import com.syncleus.ferma.AbstractEdgeFrame;
import com.syncleus.ferma.AbstractVertexFrame;
import com.syncleus.ferma.EdgeFrame;
import com.syncleus.ferma.ReflectionCache;
import com.syncleus.ferma.VertexFrame;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Property;

import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;

/**
 * This type resolver will use the Java class stored in the 'java_class' on
 * the element.
 */
public class PolymorphicTypeResolver implements TypeResolver {
    public final static String TYPE_RESOLUTION_KEY = "ferma_type";

    private final ReflectionCache reflectionCache;
    private final String typeResolutionKey;

    /**
     * Creates a new SimpleTypeResolver with a typing engine that can recognize the specified types. While these types
     * still need to be included in a separate TypedModule they must be created here as well to ensure proper look-ups
     * occur.
     *
     * @since 2.0.0
     */
    public PolymorphicTypeResolver() {
        this.reflectionCache = new ReflectionCache();
	this.typeResolutionKey = TYPE_RESOLUTION_KEY;
    }

    /**
     * Creates a new SimpleTypeResolver with a typing engine that can recognize the specified types. While these types
     * still need to be included in a separate TypedModule they must be created here as well to ensure proper look-ups
     * occur.
     *
     * @param typeResolutionKey The key used to identfy a element's type.
     * @since 2.0.0
     */
    public PolymorphicTypeResolver(final String typeResolutionKey) {
        this.reflectionCache = new ReflectionCache();
	this.typeResolutionKey = typeResolutionKey;
    }

    /**
     * Creates a new SimpleTypeResolver with a typing engine that can recognize the specified types. While these types
     * still need to be included in a separate TypedModule they must be created here as well to ensure proper look-ups
     * occur.
     *
     * @param reflectionCache the ReflectionCache used to examine the type hierarchy and do general reflection.
     * @since 2.0.0
     */
    public PolymorphicTypeResolver(final ReflectionCache reflectionCache) {
        this.reflectionCache = reflectionCache;
	this.typeResolutionKey = TYPE_RESOLUTION_KEY;
    }

    /**
     * Creates a new SimpleTypeResolver with a typing engine that can recognize the specified types. While these types
     * still need to be included in a separate TypedModule they must be created here as well to ensure proper look-ups
     * occur.
     *
     * @param reflectionCache the ReflectionCache used to examine the type hierarchy and do general reflection.
     * @param typeResolutionKey The key used to identfy a element's type.
     * @since 2.0.0
     */
    public PolymorphicTypeResolver(final ReflectionCache reflectionCache, final String typeResolutionKey) {
        this.reflectionCache = reflectionCache;
	this.typeResolutionKey = typeResolutionKey;
    }

    @Override
    public <T> Class<? extends T> resolve(final Element element, final Class<T> kind) {
        final Property<String> nodeClazzProperty = element.<String>property(this.typeResolutionKey);
        final String nodeClazz;
        if( nodeClazzProperty.isPresent() )
            nodeClazz = nodeClazzProperty.value();
        else
            return kind;

        final Class<T> nodeKind = (Class<T>) this.reflectionCache.forName(nodeClazz);

        if (kind.isAssignableFrom(nodeKind) || kind.equals(VertexFrame.class) || kind.equals(EdgeFrame.class) || kind.equals(AbstractVertexFrame.class) || kind.equals(AbstractEdgeFrame.class) || kind.
              equals(Object.class))
            return nodeKind;
        else
            return kind;
    }
    
    @Override
    public Class<?> resolve(final Element element) {
        final Property<String> typeResolutionName = element.<String>property(this.typeResolutionKey);

        if( typeResolutionName.isPresent() )
            return this.reflectionCache.forName(typeResolutionName.value());
        else
            return null;
    }

    @Override
    public void init(final Element element, final Class<?> kind) {
        element.property(this.typeResolutionKey, kind.getName());
    }
    
    @Override
    public void deinit(final Element element) {
        element.property(this.typeResolutionKey).remove();
    }

    @Override
    public <P extends Element, T extends Element> GraphTraversal<P, T> hasType(final GraphTraversal<P, T> traverser, final Class<?> type) {
        final Set<? extends String> allAllowedValues = this.reflectionCache.getSubTypeNames(type.getName());
        return traverser.has(typeResolutionKey, org.apache.tinkerpop.gremlin.process.traversal.P.within(allAllowedValues));
    }

    @Override
    public <P extends Element, T extends Element> GraphTraversal<P, T> hasNotType(final GraphTraversal<P, T> traverser, final Class<?> type) {
        final Set<? extends String> allAllowedValues = this.reflectionCache.getSubTypeNames(type.getName());
        return traverser.filter(new Predicate<Traverser<T>>() {
            @Override
            public boolean test(final Traverser<T> toCheck) {
                final Property<String> property = toCheck.get().property(typeResolutionKey);
                if( !property.isPresent() )
                    return true;

                final String resolvedType = property.value();
                if( allAllowedValues.contains(resolvedType) )
                    return false;
                else
                    return true;
            }
        });
    }

}