/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 org.apache.tinkerpop.gremlin.process.traversal;

import org.apache.tinkerpop.gremlin.process.traversal.lambda.CardinalityValueTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.TraversalStrategyProxy;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.util.function.Lambda;

import java.sql.Timestamp;
import java.time.OffsetDateTime;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiFunction;

/**
 * A Translator will translate {@link Bytecode} into another representation. That representation may be a
 * Java instance via {@link StepTranslator} or a String script in some language via {@link ScriptTranslator}.
 * The parameterization of Translator is S (traversal source) and T (full translation).
 *
 * @author Marko A. Rodriguez (http://markorodriguez.com)
 * @author Stark Arya (sandszhou.zj@alibaba-inc.com)
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
public interface Translator<S, T> {

    /**
     * Get the {@link TraversalSource} representation rooting this translator.
     * For string-based translators ({@link ScriptTranslator}), this is typically a "g".
     * For java-based translators ({@link StepTranslator}), this is typically the {@link TraversalSource} instance
     * which the {@link Traversal} will be built from.
     *
     * @return the traversal source representation
     */
    public S getTraversalSource();

    /**
     * Translate {@link Bytecode} into a new representation. Typically, for language translations, the translation is
     * to a string representing the traversal in the respective scripting language.
     *
     * @param bytecode the bytecode representing traversal source and traversal manipulations.
     * @return the translated object
     */
    public T translate(final Bytecode bytecode);

    /**
     * Translates a {@link Traversal} into the specified form
     */
    public default T translate(final Traversal<?,?> t) {
        return translate(t.asAdmin().getBytecode());
    }

    /**
     * Get the language that the translator is converting the traversal byte code to.
     *
     * @return the language of the translation
     */
    public String getTargetLanguage();

    ///

    /**
     * Translates bytecode to a Script representation.
     */
    public interface ScriptTranslator extends Translator<String, Script> {

        /**
         * Provides a way for the {@link ScriptTranslator} to convert various data types to their string
         * representations in their target language.
         */
        public interface TypeTranslator extends BiFunction<String, Object, Script> { }

        public abstract class AbstractTypeTranslator implements ScriptTranslator.TypeTranslator {
            protected static final String ANONYMOUS_TRAVERSAL_PREFIX = "__";
            protected final boolean withParameters;
            protected final Script script;

            protected AbstractTypeTranslator(final boolean withParameters) {
                this.withParameters = withParameters;
                this.script = new Script();
            }

            @Override
            public Script apply(final String traversalSource, final Object o) {
                this.script.init();
                if (o instanceof Bytecode) {
                    return produceScript(traversalSource, (Bytecode) o);
                } else {
                    return convertToScript(o);
                }
            }

            /**
             * Gets the syntax for the spawn of an anonymous traversal which is traditionally the double underscore.
             */
            protected String getAnonymousTraversalPrefix() {
                return ANONYMOUS_TRAVERSAL_PREFIX;
            }

            /**
             * Gets the syntax for a {@code null} value as a string representation.
             */
            protected abstract String getNullSyntax();

            /**
             * Take the string argument and convert it to a string representation in the target language (i.e. escape,
             * enclose in appropriate quotes, etc.)
             */
            protected abstract String getSyntax(final String o);

            /**
             * Take the boolean argument and convert it to a string representation in the target language.
             */
            protected abstract String getSyntax(final Boolean o);

            /**
             * Take the {@code Date} argument and convert it to a string representation in the target language.
             */
            protected abstract String getSyntax(final Date o);

            /**
             * Take the {@code OffsetDateTime} argument and convert it to a string representation in the target language.
             */
            protected abstract String getSyntax(final OffsetDateTime o);

            /**
             * Take the {@code Timestamp} argument and convert it to a string representation in the target language.
             */
            protected abstract String getSyntax(final Timestamp o);

            /**
             * Take the {@code UUID} argument and convert it to a string representation in the target language.
             */
            protected abstract String getSyntax(final UUID o);

            /**
             * Take the {@link Lambda} argument and convert it to a string representation in the target language.
             */
            protected abstract String getSyntax(final Lambda o);

            /**
             * Take the {@link SackFunctions.Barrier} argument and convert it to a string representation in the target language.
             */
            protected abstract String getSyntax(final SackFunctions.Barrier o);

            /**
             * Take the {@link VertexProperty.Cardinality} argument and convert it to a string representation in the target language.
             */
            protected abstract String getSyntax(final VertexProperty.Cardinality o);

            /**
             * Take the {@link Pick} argument and convert it to a string representation in the target language.
             */
            protected abstract String getSyntax(final Pick o);

            /**
             * Take the numeric argument and convert it to a string representation in the target language. Languages
             * that can discern differences in types of numbers will wish to further check the type of the
             * {@code Number} instance itself (i.e. {@code Double}, {@code Integer}, etc.)
             */
            protected abstract String getSyntax(final Number o);

            /**
             * Take the {@code Set} and writes the syntax directly to the member {@link #script} variable.
             */
            protected abstract Script produceScript(final Set<?> o);

            /**
             * Take the {@code List} and writes the syntax directly to the member {@link #script} variable.
             */
            protected abstract Script produceScript(final List<?> o);

            /**
             * Take the {@code Map} and writes the syntax directly to the member {@link #script} variable.
             */
            protected abstract Script produceScript(final Map<?,?> o);

            /**
             * Take the {@code Class} and writes the syntax directly to the member {@link #script} variable.
             */
            protected abstract Script produceScript(final Class<?> o);

            /**
             * Take the {@code Enum} and writes the syntax directly to the member {@link #script} variable.
             */
            protected abstract Script produceScript(final Enum<?> o);

            /**
             * Take the {@link Vertex} and writes the syntax directly to the member {@link #script} variable.
             */
            protected abstract Script produceScript(final Vertex o);

            /**
             * Take the {@link Edge} and writes the syntax directly to the member {@link #script} variable.
             */
            protected abstract Script produceScript(final Edge o);

            /**
             * Take the {@link VertexProperty} and writes the syntax directly to the member {@link #script} variable.
             */
            protected abstract Script produceScript(final VertexProperty<?> o);

            /**
             * Take the {@link TraversalStrategyProxy} and writes the syntax directly to the member {@link #script} variable.
             */
            protected abstract Script produceScript(final TraversalStrategyProxy<?> o);

            /**
             * Take the {@link Bytecode} and writes the syntax directly to the member {@link #script} variable.
             */
            protected abstract Script produceScript(final String traversalSource, final Bytecode o);

            /**
             * Take the {@link P} and writes the syntax directly to the member {@link #script} variable. This
             * implementation should also consider {@link TextP}.
             */
            protected abstract Script produceScript(final P<?> p);

            /**
             * Take the {@link Bytecode} and write the syntax for it directly to the member {@link #script} variable.
             */
            protected abstract Script produceCardinalityValue(final Bytecode o);

            /**
             *  For each operator argument, if withParameters set true, try parametrization as follows:
             *
             *  -----------------------------------------------
             *  if unpack, why ?      ObjectType
             *  -----------------------------------------------
             *  (Yes)                 Bytecode.Binding
             *  (Recursion, No)       Bytecode
             *  (Recursion, No)       Traversal
             *  (Yes)                 String
             *  (Recursion, No)       Set
             *  (Recursion, No)       List
             *  (Recursion, No)       Map
             *  (Yes)                 Long
             *  (Yes)                 Double
             *  (Yes)                 Float
             *  (Yes)                 Integer
             *  (Yes)                 Timestamp
             *  (Yes)                 Date
             *  (Yes)                 Uuid
             *  (Recursion, No)       P
             *  (Enumeration, No)     SackFunctions.Barrier
             *  (Enumeration, No)     VertexProperty.Cardinality
             *  (Enumeration, No)     TraversalOptionParent.Pick
             *  (Enumeration, No)     Enum
             *  (Recursion, No)       Vertex
             *  (Recursion, No)       Edge
             *  (Recursion, No)       VertexProperty
             *  (Yes)                 Lambda
             *  (Recursion, No)       TraversalStrategyProxy
             *  (Enumeration, No)     TraversalStrategy
             *  (Yes)                 Other
             *  -------------------------------------------------
             *
             * @param object
             * @return String Repres
             */
            protected Script convertToScript(final Object object) {
                if (object instanceof Bytecode.Binding) {
                    return script.getBoundKeyOrAssign(withParameters, ((Bytecode.Binding) object).variable());
                } else if (object instanceof Bytecode) {
                    final Bytecode bc = (Bytecode) object;
                    if (bc.getSourceInstructions().size() == 1 &&
                            bc.getSourceInstructions().get(0).getOperator().equals(CardinalityValueTraversal.class.getSimpleName())) {
                        return produceCardinalityValue(bc);
                    } else {
                        return produceScript(getAnonymousTraversalPrefix(), bc);
                    }
                } else if (object instanceof Traversal) {
                    return convertToScript(((Traversal) object).asAdmin().getBytecode());
                } else if (object instanceof String) {
                    final Object objectOrWrapper = withParameters ? object : getSyntax((String) object);
                    return script.getBoundKeyOrAssign(withParameters, objectOrWrapper);
                } else if (object instanceof Boolean) {
                    final Object objectOrWrapper = withParameters ? object : getSyntax((Boolean) object);
                    return script.getBoundKeyOrAssign(withParameters, objectOrWrapper);
                } else if (object instanceof Set) {
                    return produceScript((Set<?>) object);
                } else if (object instanceof List) {
                    return produceScript((List<?>) object);
                } else if (object instanceof Map) {
                    return produceScript((Map<?, ?>) object);
                } else if (object instanceof Number) {
                    final Object objectOrWrapper = withParameters ? object : getSyntax((Number) object);
                    return script.getBoundKeyOrAssign(withParameters, objectOrWrapper);
                } else if (object instanceof Class) {
                    return produceScript((Class<?>) object);
                } else if (object instanceof Timestamp) {
                    final Object objectOrWrapper = withParameters ? object : getSyntax((Timestamp) object);
                    return script.getBoundKeyOrAssign(withParameters, objectOrWrapper);
                } else if (object instanceof Date) {
                    final Object objectOrWrapper = withParameters ? object : getSyntax((Date) object);
                    return script.getBoundKeyOrAssign(withParameters, objectOrWrapper);
                } else if (object instanceof OffsetDateTime) {
                    final Object objectOrWrapper = withParameters ? object : getSyntax((OffsetDateTime) object);
                    return script.getBoundKeyOrAssign(withParameters, objectOrWrapper);
                } else if (object instanceof UUID) {
                    final Object objectOrWrapper = withParameters ? object : getSyntax((UUID) object);
                    return script.getBoundKeyOrAssign(withParameters, objectOrWrapper);
                } else if (object instanceof P) {
                    return produceScript((P<?>) object);
                } else if (object instanceof SackFunctions.Barrier) {
                    return script.append(getSyntax((SackFunctions.Barrier) object));
                } else if (object instanceof VertexProperty.Cardinality) {
                    return script.append(getSyntax((VertexProperty.Cardinality) object));
                } else if (object instanceof Pick) {
                    return script.append(getSyntax((Pick) object));
                } else if (object instanceof Enum) {
                    return produceScript((Enum<?>) object);
                } else if (object instanceof Vertex) {
                    return produceScript((Vertex) object);
                } else if (object instanceof Edge) {
                    return produceScript((Edge) object);
                } else if (object instanceof VertexProperty) {
                    return produceScript((VertexProperty<?>) object);
                } else if (object instanceof Lambda) {
                    final Object objectOrWrapper = withParameters ? object : getSyntax((Lambda) object);
                    return script.getBoundKeyOrAssign(withParameters,objectOrWrapper);
                } else if (object instanceof TraversalStrategyProxy) {
                    return produceScript((TraversalStrategyProxy<?>) object);
                } else if (object instanceof TraversalStrategy) {
                    return convertToScript(new TraversalStrategyProxy(((TraversalStrategy) object)));
                } else {
                    return null == object ? script.append(getNullSyntax()) : script.getBoundKeyOrAssign(withParameters, object);
                }
            }
        }
    }

    /**
     * Translates bytecode to actual steps.
     */
    public interface StepTranslator<S extends TraversalSource, T extends Traversal.Admin<?, ?>> extends Translator<S, T> {

    }
}
