/*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.nashorn.internal.runtime;

import static jdk.nashorn.internal.codegen.CompilerConstants.staticCall;
import static jdk.nashorn.internal.codegen.CompilerConstants.staticCallNoLookup;
import static jdk.nashorn.internal.codegen.CompilerConstants.virtualCallNoLookup;

import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
import java.util.Arrays;
import jdk.nashorn.internal.codegen.CompilerConstants;
import jdk.nashorn.internal.codegen.CompilerConstants.Call;
import jdk.nashorn.internal.codegen.types.Type;
import jdk.nashorn.internal.lookup.MethodHandleFactory;
import jdk.nashorn.internal.lookup.MethodHandleFunctionality;
import jdk.nashorn.internal.objects.Global;
import jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor;

/**
 * Used to signal to the linker to relink the callee
 */
@SuppressWarnings("serial")
public final class RewriteException extends Exception {
    private static final MethodHandleFunctionality MH = MethodHandleFactory.getFunctionality();

    // Runtime scope in effect at the time of the compilation. Used to evaluate types of expressions and prevent overly
    // optimistic assumptions (which will lead to unnecessary deoptimizing recompilations).
    private ScriptObject runtimeScope;

    // Contents of bytecode slots
    private Object[] byteCodeSlots;

    private final int[] previousContinuationEntryPoints;

    /** Call for getting the contents of the bytecode slots in the exception */
    public static final Call GET_BYTECODE_SLOTS       = virtualCallNoLookup(RewriteException.class, "getByteCodeSlots", Object[].class);
    /** Call for getting the program point in the exception */
    public static final Call GET_PROGRAM_POINT        = virtualCallNoLookup(RewriteException.class, "getProgramPoint", int.class);
    /** Call for getting the return value for the exception */
    public static final Call GET_RETURN_VALUE         = virtualCallNoLookup(RewriteException.class, "getReturnValueDestructive", Object.class);
    /** Call for the populate array bootstrap */
    public static final Call BOOTSTRAP                = staticCallNoLookup(RewriteException.class, "populateArrayBootstrap", CallSite.class, Lookup.class, String.class, MethodType.class, int.class);

    /** Call for populating an array with local variable state */
    private static final Call POPULATE_ARRAY           = staticCall(MethodHandles.lookup(), RewriteException.class, "populateArray", Object[].class, Object[].class, int.class, Object[].class);

    /** Call for converting an array to a long array. */
    public static final Call TO_LONG_ARRAY   = staticCallNoLookup(RewriteException.class, "toLongArray",   long[].class, Object.class, RewriteException.class);
    /** Call for converting an array to a double array. */
    public static final Call TO_DOUBLE_ARRAY = staticCallNoLookup(RewriteException.class, "toDoubleArray", double[].class, Object.class, RewriteException.class);
    /** Call for converting an array to an object array. */
    public static final Call TO_OBJECT_ARRAY = staticCallNoLookup(RewriteException.class, "toObjectArray", Object[].class, Object.class, RewriteException.class);
    /** Call for converting an object to null if it can't be represented as an instance of a class. */
    public static final Call INSTANCE_OR_NULL = staticCallNoLookup(RewriteException.class, "instanceOrNull", Object.class, Object.class, Class.class);
    /** Call for asserting the length of an array. */
    public static final Call ASSERT_ARRAY_LENGTH = staticCallNoLookup(RewriteException.class, "assertArrayLength", void.class, Object[].class, int.class);

    private RewriteException(
            final UnwarrantedOptimismException e,
            final Object[] byteCodeSlots,
            final String[] byteCodeSymbolNames,
            final int[] previousContinuationEntryPoints) {
        super("", e, false, Context.DEBUG);
        this.byteCodeSlots = byteCodeSlots;
        this.runtimeScope = mergeSlotsWithScope(byteCodeSlots, byteCodeSymbolNames);
        this.previousContinuationEntryPoints = previousContinuationEntryPoints;
    }

    /**
     * Constructor for a rewrite exception thrown from an optimistic function.
     * @param e the {@link UnwarrantedOptimismException} that triggered this exception.
     * @param byteCodeSlots contents of local variable slots at the time of rewrite at the program point
     * @param byteCodeSymbolNames the names of the variables in the {@code byteCodeSlots} parameter. The array might
     * have less elements, and some elements might be unnamed (the name can be null). The information is provided in an
     * effort to assist evaluation of expressions for their types by the compiler doing the deoptimizing recompilation,
     * and can thus be incomplete - the more complete it is, the more expressions can be evaluated by the compiler, and
     * the more unnecessary deoptimizing compilations can be avoided.
     * @return a new rewrite exception
     */
    public static RewriteException create(final UnwarrantedOptimismException e,
            final Object[] byteCodeSlots,
            final String[] byteCodeSymbolNames) {
        return create(e, byteCodeSlots, byteCodeSymbolNames, null);
    }

    /**
     * Constructor for a rewrite exception thrown from a rest-of method.
     * @param e the {@link UnwarrantedOptimismException} that triggered this exception.
     * @param byteCodeSlots contents of local variable slots at the time of rewrite at the program point
     * @param byteCodeSymbolNames the names of the variables in the {@code byteCodeSlots} parameter. The array might
     * have less elements, and some elements might be unnamed (the name can be null). The information is provided in an
     * effort to assist evaluation of expressions for their types by the compiler doing the deoptimizing recompilation,
     * and can thus be incomplete - the more complete it is, the more expressions can be evaluated by the compiler, and
     * the more unnecessary deoptimizing compilations can be avoided.
     * @param previousContinuationEntryPoints an array of continuation entry points that were already executed during
     * one logical invocation of the function (a rest-of triggering a rest-of triggering a...)
     * @return a new rewrite exception
     */
    public static RewriteException create(final UnwarrantedOptimismException e,
            final Object[] byteCodeSlots,
            final String[] byteCodeSymbolNames,
            final int[] previousContinuationEntryPoints) {
        return new RewriteException(e, byteCodeSlots, byteCodeSymbolNames, previousContinuationEntryPoints);
    }

    /**
     * Bootstrap method for populate array
     * @param lookup     lookup
     * @param name       name (ignored)
     * @param type       method type for signature
     * @param startIndex start index to start writing to
     * @return callsite to array populator (constant)
     */
    public static CallSite populateArrayBootstrap(final MethodHandles.Lookup lookup, final String name, final MethodType type, final int startIndex) {
        MethodHandle mh = POPULATE_ARRAY.methodHandle();
        mh = MH.insertArguments(mh, 1, startIndex);
        mh = MH.asCollector(mh, Object[].class, type.parameterCount() - 1);
        mh = MH.asType(mh, type);
        return new ConstantCallSite(mh);
    }

    private static ScriptObject mergeSlotsWithScope(final Object[] byteCodeSlots, final String[] byteCodeSymbolNames) {
        final ScriptObject locals = Global.newEmptyInstance();
        final int l = Math.min(byteCodeSlots.length, byteCodeSymbolNames.length);
        ScriptObject runtimeScope = null;
        final String scopeName = CompilerConstants.SCOPE.symbolName();
        for(int i = 0; i < l; ++i) {
            final String name = byteCodeSymbolNames[i];
            final Object value = byteCodeSlots[i];
            if(scopeName.equals(name)) {
                assert runtimeScope == null;
                runtimeScope = (ScriptObject)value;
            } else if(name != null) {
                locals.set(name, value, NashornCallSiteDescriptor.CALLSITE_STRICT);
            }
        }
        locals.setProto(runtimeScope);
        return locals;
    }

    /**
     * Array populator used for saving the local variable state into the array contained in the
     * RewriteException
     * @param arrayToBePopluated array to be populated
     * @param startIndex start index to write to
     * @param items items with which to populate the array
     * @return the populated array - same array object
     */
    public static Object[] populateArray(final Object[] arrayToBePopluated, final int startIndex, final Object[] items) {
        System.arraycopy(items, 0, arrayToBePopluated, startIndex, items.length);
        return arrayToBePopluated;
    }

    /**
     * Continuation handler calls this method when a local variable carried over into the continuation is expected to be
     * a long array in the continued method. Normally, it will also be a long array in the original (interrupted by
     * deoptimization) method, but it can actually be an int array that underwent widening in the new code version.
     * @param obj the object that has to be converted into a long array
     * @param e the exception being processed
     * @return a long array
     */
    public static long[] toLongArray(final Object obj, final RewriteException e) {
        if(obj instanceof long[]) {
            return (long[])obj;
        }

        assert obj instanceof int[];

        final int[] in = (int[])obj;
        final long[] out = new long[in.length];
        for(int i = 0; i < in.length; ++i) {
            out[i] = in[i];
        }
        return e.replaceByteCodeValue(in, out);
    }

    /**
     * Continuation handler calls this method when a local variable carried over into the continuation is expected to be
     * a double array in the continued method. Normally, it will also be a double array in the original (interrupted by
     * deoptimization) method, but it can actually be an int or long array that underwent widening in the new code version.
     * @param obj the object that has to be converted into a double array
     * @param e the exception being processed
     * @return a double array
     */
    public static double[] toDoubleArray(final Object obj, final RewriteException e) {
        if(obj instanceof double[]) {
            return (double[])obj;
        }

        assert obj instanceof int[] || obj instanceof long[];

        final int l = Array.getLength(obj);
        final double[] out = new double[l];
        for(int i = 0; i < l; ++i) {
            out[i] = Array.getDouble(obj, i);
        }
        return e.replaceByteCodeValue(obj, out);
    }

    /**
     * Continuation handler calls this method when a local variable carried over into the continuation is expected to be
     * an Object array in the continued method. Normally, it will also be an Object array in the original (interrupted by
     * deoptimization) method, but it can actually be an int, long, or double array that underwent widening in the new
     * code version.
     * @param obj the object that has to be converted into an Object array
     * @param e the exception being processed
     * @return an Object array
     */
    public static Object[] toObjectArray(final Object obj, final RewriteException e) {
        if(obj instanceof Object[]) {
            return (Object[])obj;
        }

        assert obj instanceof int[] || obj instanceof long[] || obj instanceof double[] : obj + " is " + obj.getClass().getName();

        final int l = Array.getLength(obj);
        final Object[] out = new Object[l];
        for(int i = 0; i < l; ++i) {
            out[i] = Array.get(obj, i);
        }
        return e.replaceByteCodeValue(obj, out);
    }

    /**
     * Continuation handler calls this method when a local variable carried over into the continuation is expected to
     * have a certain type, but the value can have a different type coming from the deoptimized method as it was a dead
     * store. If we had precise liveness analysis, we wouldn't need this.
     * @param obj the object inspected for being of a particular type
     * @param clazz the type the object must belong to
     * @return the object if it belongs to the type, or null otherwise
     */
    public static Object instanceOrNull(final Object obj, final Class<?> clazz) {
        return clazz.isInstance(obj) ? obj : null;
    }

    /**
     * Asserts the length of an array. Invoked from continuation handler only when running with assertions enabled.
     * The array can, in fact, have more elements than asserted, but they must all have Undefined as their value. The
     * method does not test for the array having less elements than asserted, as those would already have caused an
     * {@code ArrayIndexOutOfBoundsException} to be thrown as the continuation handler attempts to access the missing
     * elements.
     * @param arr the array
     * @param length the asserted length
     */
    public static void assertArrayLength(final Object[] arr, final int length) {
        for(int i = arr.length; i-- > length;) {
            if(arr[i] != ScriptRuntime.UNDEFINED) {
                throw new AssertionError(String.format("Expected array length %d, but it is %d", length, i + 1));
            }
        }
    }

    private <T> T replaceByteCodeValue(final Object in, final T out) {
        for(int i = 0; i < byteCodeSlots.length; ++i) {
            if(byteCodeSlots[i] == in) {
                byteCodeSlots[i] = out;
            }
        }
        return out;
    }

    private UnwarrantedOptimismException getUOE() {
        return (UnwarrantedOptimismException)getCause();
    }
    /**
     * Get return value. This method is destructive, after it is invoked subsequent invocation of either
     * {@link #getByteCodeSlots()} or this method will return null. This method is invoked from the generated
     * continuation code as the last step before continuing the execution, and we need to make sure we don't hang on to
     * either the entry bytecode slot values or the return value and prevent them from being garbage collected.
     * @return return value
     */
    public Object getReturnValueDestructive() {
        assert byteCodeSlots != null;
        byteCodeSlots = null;
        runtimeScope = null;
        return getUOE().getReturnValueDestructive();
    }

    Object getReturnValueNonDestructive() {
        return getUOE().getReturnValueNonDestructive();
    }

    /**
     * Get return type
     * @return return type
     */
    public Type getReturnType() {
        return getUOE().getReturnType();
    }

    /**
     * Get the program point.
     * @return program point.
     */
    public int getProgramPoint() {
        return getUOE().getProgramPoint();
    }

    /**
     * Get the bytecode slot contents.
     * @return bytecode slot contents.
     */
    public Object[] getByteCodeSlots() {
        return byteCodeSlots == null ? null : byteCodeSlots.clone();
    }

    /**
     * @return an array of continuation entry points that were already executed during one logical invocation of the
     * function (a rest-of triggering a rest-of triggering a...)
     */
    public int[] getPreviousContinuationEntryPoints() {
        return previousContinuationEntryPoints == null ? null : previousContinuationEntryPoints.clone();
    }

    /**
     * Returns the runtime scope that was in effect when the exception was thrown.
     * @return the runtime scope.
     */
    public ScriptObject getRuntimeScope() {
        return runtimeScope;
    }

    private static String stringify(final Object returnValue) {
        if (returnValue == null) {
            return "null";
        }
        String str = returnValue.toString();
        if (returnValue instanceof String) {
            str = '\'' + str + '\'';
        } else if (returnValue instanceof Double) {
            str += 'd';
        } else if (returnValue instanceof Long) {
            str += 'l';
        }
        return str;
    }

    @Override
    public String getMessage() {
        return getMessage(false);
    }

    /**
     * Short toString function for message
     * @return short message
     */
    public String getMessageShort() {
        return getMessage(true);
    }

    private String getMessage(final boolean isShort) {
        final StringBuilder sb = new StringBuilder();

        //program point
        sb.append("[pp=").
            append(getProgramPoint()).
            append(", ");

        //slot contents
        if (!isShort) {
            final Object[] slots = byteCodeSlots;
            if (slots != null) {
                sb.append("slots=").
                    append(Arrays.asList(slots)).
                    append(", ");
            }
        }

        //return type
        sb.append("type=").
            append(getReturnType()).
            append(", ");

        //return value
        sb.append("value=").
            append(stringify(getReturnValueNonDestructive())).
            append(")]");

        return sb.toString();
    }

    private void writeObject(final ObjectOutputStream out) throws NotSerializableException {
        throw new NotSerializableException(getClass().getName());
    }

    private void readObject(final ObjectInputStream in) throws NotSerializableException {
        throw new NotSerializableException(getClass().getName());
    }
}
