/*
 *  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.groovy.jsr223;

import groovy.json.StringEscapeUtils;
import org.apache.commons.configuration2.ConfigurationConverter;
import org.apache.tinkerpop.gremlin.process.traversal.Bytecode;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.SackFunctions;
import org.apache.tinkerpop.gremlin.process.traversal.Script;
import org.apache.tinkerpop.gremlin.process.traversal.TextP;
import org.apache.tinkerpop.gremlin.process.traversal.Translator;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalOptionParent;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.TraversalStrategyProxy;
import org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP;
import org.apache.tinkerpop.gremlin.process.traversal.util.OrP;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import org.apache.tinkerpop.gremlin.util.function.Lambda;

import java.sql.Timestamp;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.function.BinaryOperator;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

/**
 * Converts bytecode to a Groovy string of Gremlin.
 *
 * @author Marko A. Rodriguez (http://markorodriguez.com)
 * @author Stephen Mallette (http://stephen.genoprime.com)
 * @author Stark Arya (sandszhou.zj@alibaba-inc.com)
 * @deprecated As of release 3.4.9, moved to {@link org.apache.tinkerpop.gremlin.process.traversal.translator.GroovyTranslator} in {@code gremlin-core}.
 */
@Deprecated
public final class GroovyTranslator implements Translator.ScriptTranslator {

    private final String traversalSource;
    private final TypeTranslator typeTranslator;

    private GroovyTranslator(final String traversalSource, final TypeTranslator typeTranslator) {
        this.traversalSource = traversalSource;
        this.typeTranslator = typeTranslator;
    }

    /**
     * Creates the translator with a {@code false} argument to {@code withParameters} using
     * {@link #of(String, boolean)}.
     */
    public static final GroovyTranslator of(final String traversalSource) {
        return of(traversalSource, false);
    }

    /**
     * Creates the translator with the {@link DefaultTypeTranslator} passing the {@code withParameters} option to it
     * which will handle type translation in a fashion that should typically increase cache hits and reduce
     * compilation times if enabled at the sacrifice to rewriting of the script that could reduce readability.
     */
    public static final GroovyTranslator of(final String traversalSource, final boolean withParameters) {
        return of(traversalSource, new DefaultTypeTranslator(withParameters));
    }

    public static final GroovyTranslator of(final String traversalSource, final TypeTranslator typeTranslator) {
        return new GroovyTranslator(traversalSource, typeTranslator);
    }

    ///////

    @Override
    public Script translate(final Bytecode bytecode) {
        return typeTranslator.apply(traversalSource, bytecode);
    }

    @Override
    public String getTargetLanguage() {
        return "gremlin-groovy";
    }

    @Override
    public String toString() {
        return StringFactory.translatorString(this);
    }

    @Override
    public String getTraversalSource() {
        return this.traversalSource;
    }

    /**
     * Performs standard type translation for the TinkerPop types to Groovy.
     */
    public static class DefaultTypeTranslator implements TypeTranslator {
        protected final boolean withParameters;
        protected final Script script;

        public DefaultTypeTranslator(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 internalTranslate(traversalSource, (Bytecode) o);
            } else {
                return convertToScript(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) {
                return internalTranslate("__", (Bytecode) object);
            } else if (object instanceof Traversal) {
                return convertToScript(((Traversal) object).asAdmin().getBytecode());
            } else if (object instanceof String) {
                final String wrapper = (((String) object).contains("\"") ? "\"\"\"" + StringEscapeUtils.escapeJava((String) object) + "\"\"\"" : "\"" + StringEscapeUtils.escapeJava((String) object) + "\"")
                        .replace("$", "\\$");
                return script.getBoundKeyOrAssign(withParameters, withParameters ? object : wrapper);
            } else if (object instanceof Set) {
                convertToScript(((Set)object).stream().collect(Collectors.toList()));
                return script.append(" as Set");
            } else if (object instanceof List) {
                Iterator<?> iterator = ((List)object).iterator();
                if (! iterator.hasNext()) {
                    return script.append("[]");
                } else {
                    script.append("[");
                    for (;;)  {
                        Object e =  iterator.next();
                        convertToScript(e);
                        if (! iterator.hasNext()) {
                            return script.append("]");
                        } else {
                            script.append(",").append(" ");
                        }
                    }
                }
            } else if (object instanceof Map) {
                script.append("[");
                for (final Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
                    script.append("(");
                    convertToScript(entry.getKey());
                    script.append("):(");
                    convertToScript(entry.getValue());
                    script.append("),");
                }

                // only need to remove this last bit if entries were added
                if (!((Map<?, ?>) object).isEmpty()) {
                    return script.setCharAtEnd(']');
                } else {
                    return script.append("]");
                }
            } else if (object instanceof Long) {
                return script.getBoundKeyOrAssign(withParameters, withParameters ? object : object + "L");
            } else if (object instanceof Double) {
                return script.getBoundKeyOrAssign(withParameters, withParameters ? object : object + "d");
            } else if (object instanceof Float) {
                return script.getBoundKeyOrAssign(withParameters, withParameters ? object : object + "f");
            } else if (object instanceof Integer) {
                return script.getBoundKeyOrAssign(withParameters, withParameters ? object : "(int) " + object);
            } else if (object instanceof Class) {
                return script.append(((Class) object).getCanonicalName());
            } else if (object instanceof Timestamp) {
                return script.getBoundKeyOrAssign(withParameters, withParameters ? object : "new java.sql.Timestamp(" + ((Timestamp) object).getTime() + ")");
            } else if (object instanceof Date) {
                return script.getBoundKeyOrAssign(withParameters, withParameters ? object : "new java.util.Date(" + ((Date) object).getTime() + ")");
            } else if (object instanceof UUID) {
                return script.getBoundKeyOrAssign(withParameters, withParameters ? object : "java.util.UUID.fromString('" + object.toString() + "')");
            } else if (object instanceof P) {
                return convertPToScript((P) object);
            } else if (object instanceof SackFunctions.Barrier) {
                return script.append("SackFunctions.Barrier." + object.toString());
            } else if (object instanceof VertexProperty.Cardinality) {
                return script.append("VertexProperty.Cardinality." + object.toString());
            } else if (object instanceof TraversalOptionParent.Pick) {
                return script.append("TraversalOptionParent.Pick." + object.toString());
            } else if (object instanceof Enum) {
                return script.append(((Enum) object).getDeclaringClass().getSimpleName() + "." + object.toString());
            } else if (object instanceof Element) {
                if (object instanceof Vertex) {
                    final Vertex vertex = (Vertex) object;
                    script.append("new org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex(");
                    convertToScript(vertex.id());
                    script.append(",");
                    convertToScript(vertex.label());
                    return script.append(", Collections.emptyMap())");
                } else if (object instanceof Edge) {
                    final Edge edge = (Edge) object;
                    script.append("new org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge(");
                    convertToScript(edge.id());
                    script.append(",");
                    convertToScript(edge.label());
                    script.append(",");
                    script.append("Collections.emptyMap(),");
                    convertToScript(edge.outVertex().id());
                    script.append(",");
                    convertToScript(edge.outVertex().label());
                    script.append(",");
                    convertToScript(edge.inVertex().id());
                    script.append(",");
                    convertToScript(edge.inVertex().label());
                    return script.append(")");
                } else {// VertexProperty
                    final VertexProperty vertexProperty = (VertexProperty) object;
                    script.append("new org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertexProperty(");
                    convertToScript(vertexProperty.id());
                    script.append(",");
                    convertToScript(vertexProperty.label());
                    script.append(",");
                    convertToScript(vertexProperty.value());
                    script.append(",");
                    script.append("Collections.emptyMap(),");
                    convertToScript(vertexProperty.element());
                    return script.append(")");
                }
            } else if (object instanceof Lambda) {
                final String lambdaString = ((Lambda) object).getLambdaScript().trim();
                final String wrapper = lambdaString.startsWith("{") ? lambdaString : "{" + lambdaString + "}";
                return script.getBoundKeyOrAssign(withParameters, withParameters ? object : wrapper);
            } else if (object instanceof TraversalStrategyProxy) {
                final TraversalStrategyProxy proxy = (TraversalStrategyProxy) object;
                if (proxy.getConfiguration().isEmpty()) {
                    return script.append(proxy.getStrategyClass().getCanonicalName() + ".instance()");
                } else {
                    script.append(proxy.getStrategyClass().getCanonicalName() + ".create(new org.apache.commons.configuration2.MapConfiguration(");
                    convertToScript(ConfigurationConverter.getMap(proxy.getConfiguration()));
                    return script.append("))");
                }
            } else if (object instanceof TraversalStrategy) {
                return convertToScript(new TraversalStrategyProxy(((TraversalStrategy) object)));
            } else {
                return null == object ? script.append("null") : script.getBoundKeyOrAssign(withParameters, object);
            }
        }

        protected Script internalTranslate(final String start, final Bytecode bytecode) {
            script.append(start);
            for (final Bytecode.Instruction instruction : bytecode.getInstructions()) {
                final String methodName = instruction.getOperator();
                if (0 == instruction.getArguments().length) {
                    script.append(".").append(methodName).append("()");
                } else {
                    script.append(".").append(methodName).append("(");

                    // have to special case withSack() for Groovy because UnaryOperator and BinaryOperator signatures
                    // make it impossible for the interpreter to figure out which function to call. specifically we need
                    // to discern between:
                    //     withSack(A initialValue, UnaryOperator<A> splitOperator)
                    //     withSack(A initialValue, BinaryOperator<A> splitOperator)
                    // and:
                    //     withSack(Supplier<A> initialValue, UnaryOperator<A> mergeOperator)
                    //     withSack(Supplier<A> initialValue, BinaryOperator<A> mergeOperator)
                    if (methodName.equals(TraversalSource.Symbols.withSack) &&
                            instruction.getArguments().length == 2 && instruction.getArguments()[1] instanceof Lambda) {
                        final String castFirstArgTo = instruction.getArguments()[0] instanceof Lambda ?
                                Supplier.class.getName() : "";
                        final Lambda secondArg = (Lambda) instruction.getArguments()[1];
                        final String castSecondArgTo = secondArg.getLambdaArguments() == 1 ? UnaryOperator.class.getName() :
                                BinaryOperator.class.getName();
                        if (!castFirstArgTo.isEmpty())
                            script.append(String.format("(%s) ", castFirstArgTo));
                        convertToScript(instruction.getArguments()[0]);
                        script.append(", (").append(castSecondArgTo).append(") ");
                        convertToScript(instruction.getArguments()[1]);
                        script.append(",");
                    } else {
                        for (final Object object : instruction.getArguments()) {
                            convertToScript(object);
                            script.append(",");
                        }
                    }
                    script.setCharAtEnd(')');
                }
            }
            return script;
        }

        protected Script convertPToScript(final P p) {
            if (p instanceof TextP) {
                return convertTextPToScript((TextP) p);
            }
            if (p instanceof ConnectiveP) {
                final List<P<?>> list = ((ConnectiveP) p).getPredicates();
                for (int i = 0; i < list.size(); i++) {
                    convertPToScript(list.get(i));
                    if (i < list.size() - 1) {
                        script.append(p instanceof OrP ? ".or(" : ".and(");
                    }
                }
                script.append(")");
            } else {
                script.append("P.").append(p.getBiPredicate().toString()).append("(");
                convertToScript(p.getValue());
                script.append(")");
            }
            return script;
        }

        protected Script convertTextPToScript(final TextP p) {
            script.append("TextP.").append(p.getBiPredicate().toString()).append("(");
            convertToScript(p.getValue());
            script.append(")");
            return script;
        }
    }
}
