/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy 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 for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
// CodeHacker.java
//

/*
 * ImageJ software for multidimensional image processing and analysis.
 * 
 * Copyright (c) 2010, ImageJDev.org.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * Neither the names of the ImageJDev.org developers nor the
 * names of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package icy.system;

import java.security.ProtectionDomain;
import java.util.ArrayList;

import icy.util.StringUtil;
import javassist.CannotCompileException;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;

/**
 * The code hacker provides a mechanism for altering the behavior of classes
 * before they are loaded, for the purpose of injecting new methods and/or
 * altering existing ones.
 * <p>
 * In ImageJ, this mechanism is used to provide new seams into legacy ImageJ1 code, so that (e.g.)
 * the modern UI is aware of IJ1 events as they occur.
 * </p>
 * 
 * @author Curtis Rueden
 * @author Rick Lentz
 * @author Stephane Dallongeville
 */
public class ClassPatcher
{
    private final static String ARG_RESULT = "result";

    private final ClassPool pool;
    private final String patchPackage;
    private final String patchSuffix;

    public ClassPatcher(ClassLoader classLoader, String patchPackage, String patchSuffix)
    {
        pool = ClassPool.getDefault();
        pool.appendClassPath(new ClassClassPath(getClass()));
        if (classLoader != null)
            pool.appendClassPath(new LoaderClassPath(classLoader));
        this.patchPackage = patchPackage;
        this.patchSuffix = patchSuffix;
    }

    public ClassPatcher(String patchPackage, String patchSuffix)
    {
        this(null, patchPackage, patchSuffix);
    }

    /**
     * Modifies a class by injecting additional code at the end of the specified
     * method's body.
     * <p>
     * The extra code is defined in the imagej.legacy.patches package, as described in the
     * documentation for {@link #insertMethod(String, String)}.
     * </p>
     * 
     * @param fullClass
     *        Fully qualified name of the class to modify.
     * @param methodSig
     *        Method signature of the method to modify; e.g.,
     *        "public void updateAndDraw()"
     */
    public void insertAfterMethod(final String fullClass, final String methodSig)
    {
        insertAfterMethod(fullClass, methodSig, newCode(fullClass, methodSig));
    }

    /**
     * Modifies a class by injecting the provided code string at the end of the
     * specified method's body.
     * 
     * @param fullClass
     *        Fully qualified name of the class to modify.
     * @param methodSig
     *        Method signature of the method to modify; e.g.,
     *        "public void updateAndDraw()"
     * @param newCode
     *        The string of code to add; e.g., System.out.println(\"Hello
     *        World!\");
     */
    public void insertAfterMethod(final String fullClass, final String methodSig, final String newCode)
    {
        try
        {
            getMethod(fullClass, methodSig).insertAfter(newCode);
        }
        catch (final CannotCompileException e)
        {
            throw new IllegalArgumentException("Cannot modify method: " + methodSig, e);
        }
    }

    /**
     * Modifies a class by injecting additional code at the start of the specified
     * method's body.
     * <p>
     * The extra code is defined in the imagej.legacy.patches package, as described in the
     * documentation for {@link #insertMethod(String, String)}.
     * </p>
     * 
     * @param fullClass
     *        Fully qualified name of the class to override.
     * @param methodSig
     *        Method signature of the method to override; e.g.,
     *        "public void updateAndDraw()"
     */
    public void insertBeforeMethod(final String fullClass, final String methodSig)
    {
        insertBeforeMethod(fullClass, methodSig, newCode(fullClass, methodSig));
    }

    /**
     * Modifies a class by injecting the provided code string at the start of the
     * specified method's body.
     * 
     * @param fullClass
     *        Fully qualified name of the class to override.
     * @param methodSig
     *        Method signature of the method to override; e.g.,
     *        "public void updateAndDraw()"
     * @param newCode
     *        The string of code to add; e.g., System.out.println(\"Hello
     *        World!\");
     */
    public void insertBeforeMethod(final String fullClass, final String methodSig, final String newCode)
    {
        try
        {
            getMethod(fullClass, methodSig).insertBefore(newCode);
        }
        catch (final CannotCompileException e)
        {
            throw new IllegalArgumentException("Cannot modify method: " + methodSig, e);
        }
    }

    /**
     * Modifies a class by injecting a new method.
     * <p>
     * The body of the method is defined in the imagej.legacy.patches package, as described in the
     * {@link #insertMethod(String, String)} method documentation.
     * <p>
     * The new method implementation should be declared in the imagej.legacy.patches package, with
     * the same name as the original class plus "Methods"; e.g., overridden ij.gui.ImageWindow
     * methods should be placed in the imagej.legacy.patches.ImageWindowMethods class.
     * </p>
     * <p>
     * New method implementations must be public static, with an additional first parameter: the
     * instance of the class on which to operate.
     * </p>
     * 
     * @param fullClass
     *        Fully qualified name of the class to override.
     * @param methodSig
     *        Method signature of the method to override; e.g.,
     *        "public void setVisible(boolean vis)"
     */
    public void insertMethod(final String fullClass, final String methodSig)
    {
        insertMethod(fullClass, methodSig, newCode(fullClass, methodSig));
    }

    /**
     * Modifies a class by injecting the provided code string as a new method.
     * 
     * @param fullClass
     *        Fully qualified name of the class to override.
     * @param methodSig
     *        Method signature of the method to override; e.g.,
     *        "public void updateAndDraw()"
     * @param newCode
     *        The string of code to add; e.g., System.out.println(\"Hello
     *        World!\");
     */
    public void insertMethod(final String fullClass, final String methodSig, final String newCode)
    {
        final CtClass classRef = getClass(fullClass);
        final String methodBody = methodSig + " { " + newCode + " } ";
        try
        {
            final CtMethod methodRef = CtNewMethod.make(methodBody, classRef);
            classRef.addMethod(methodRef);
        }
        catch (final CannotCompileException e)
        {
            throw new IllegalArgumentException("Cannot add method: " + methodSig, e);
        }
    }

    /**
     * Modifies a class by replacing the specified method.
     * <p>
     * The new code is defined in the imagej.legacy.patches package, as described in the
     * documentation for {@link #insertMethod(String, String)}.
     * </p>
     * 
     * @param fullClass
     *        Fully qualified name of the class to override.
     * @param methodSig
     *        Method signature of the method to replace; e.g.,
     *        "public void setVisible(boolean vis)"
     */
    public void replaceMethod(final String fullClass, final String methodSig)
    {
        replaceMethod(fullClass, methodSig, newCode(fullClass, methodSig));
    }

    /**
     * Modifies a class by replacing the specified method with the provided code
     * string.
     * 
     * @param fullClass
     *        Fully qualified name of the class to override.
     * @param methodSig
     *        Method signature of the method to replace; e.g.,
     *        "public void setVisible(boolean vis)"
     * @param newCode
     *        The string of code to add; e.g., System.out.println(\"Hello
     *        World!\");
     */
    public void replaceMethod(final String fullClass, final String methodSig, final String newCode)
    {
        try
        {
            getMethod(fullClass, methodSig).setBody(newCode);
        }
        catch (final CannotCompileException e)
        {
            throw new IllegalArgumentException("Cannot modify method: " + methodSig, e);
        }
    }

    /**
     * Loads the given, possibly modified, class.
     * <p>
     * This method must be called to confirm any changes made with {@link #insertAfterMethod},
     * {@link #insertBeforeMethod}, {@link #insertMethod} or {@link #replaceMethod}.
     * </p>
     * 
     * @param fullClass
     *        Fully qualified class name to load.
     * @return the loaded class
     */
    public Class<?> loadClass(final String fullClass)
    {
        final CtClass classRef = getClass(fullClass);
        try
        {
            return classRef.toClass();
        }
        catch (final CannotCompileException e)
        {
            IcyExceptionHandler.showErrorMessage(e, false);
            System.err.println("Cannot load class: " + fullClass);
            return null;
        }
    }

    /**
     * Loads the given, possibly modified, class.
     * <p>
     * This method must be called to confirm any changes made with {@link #insertAfterMethod},
     * {@link #insertBeforeMethod}, {@link #insertMethod} or {@link #replaceMethod}.
     * </p>
     * 
     * @param fullClass
     *        Fully qualified class name to load.
     * @return the loaded class
     */
    public Class<?> loadClass(final String fullClass, ClassLoader classLoader, ProtectionDomain protectionDomain)
    {
        final CtClass classRef = getClass(fullClass);
        try
        {
            return classRef.toClass(classLoader, protectionDomain);
        }
        catch (final CannotCompileException e)
        {
            IcyExceptionHandler.showErrorMessage(e, false);
            System.err.println("Cannot load class: " + fullClass);
            return null;
        }
    }

    /** Gets the Javassist class object corresponding to the given class name. */
    private CtClass getClass(final String fullClass)
    {
        try
        {
            return pool.get(fullClass);
        }
        catch (final NotFoundException e)
        {
            throw new IllegalArgumentException("No such class: " + fullClass, e);
        }
    }

    /**
     * Gets the Javassist method object corresponding to the given method
     * signature of the specified class name.
     */
    private CtMethod getMethod(final String fullClass, final String methodSig)
    {
        final CtClass cc = getClass(fullClass);
        final String name = getMethodName(methodSig);
        final String[] argTypes = getMethodArgTypes(methodSig, false);
        final CtClass[] params = new CtClass[argTypes.length];
        for (int i = 0; i < params.length; i++)
        {
            params[i] = getClass(argTypes[i]);
        }
        try
        {
            return cc.getDeclaredMethod(name, params);
        }
        catch (final NotFoundException e)
        {
            throw new IllegalArgumentException("No such method: " + methodSig, e);
        }
    }

    /**
     * Generates a new line of code calling the {@link imagej.legacy.patches} class and method
     * corresponding to the given method signature.
     */
    private String newCode(final String fullClass, final String methodSig)
    {
        final int dotIndex = fullClass.lastIndexOf(".");
        final String className = fullClass.substring(dotIndex + 1);

        final String methodName = getMethodName(methodSig);
        final boolean isStatic = isStatic(methodSig);
        final boolean isVoid = isVoid(methodSig);

        final StringBuilder newCode = new StringBuilder(
                (isVoid ? "" : "return ") + patchPackage + "." + className + patchSuffix + "." + methodName + "(");
        boolean firstArg = true;
        if (!isStatic)
        {
            newCode.append("this");
            firstArg = false;
        }
        int i = 1;
        for (String argName : getMethodArgNames(methodSig, true))
        {
            if (firstArg)
                firstArg = false;
            else
                newCode.append(", ");

            if (StringUtil.equals(argName, ARG_RESULT))
                newCode.append("$_");
            else
            {
                newCode.append("$" + i);
                i++;
            }
        }
        newCode.append(");");

        return newCode.toString();
    }

    /** Extracts the method name from the given method signature. */
    private String getMethodName(final String methodSig)
    {
        final int parenIndex = methodSig.indexOf("(");
        final int spaceIndex = methodSig.lastIndexOf(" ", parenIndex);
        return methodSig.substring(spaceIndex + 1, parenIndex);
    }

    private String[] getMethodArgs(final String methodSig, final boolean wantResult)
    {
        final ArrayList<String> result = new ArrayList<String>();

        final int parenIndex = methodSig.indexOf("(");
        final String methodArgs = methodSig.substring(parenIndex + 1, methodSig.length() - 1);
        final String[] args = methodArgs.equals("") ? new String[0] : methodArgs.split(",");
        for (String arg : args)
        {
            final String a = arg.trim();
            if (!StringUtil.equals(a.split(" ")[1], ARG_RESULT) || wantResult)
                result.add(a);
        }

        return result.toArray(new String[result.size()]);
    }

    private String[] getMethodArgTypes(final String methodSig, final boolean wantResult)
    {
        final String[] args = getMethodArgs(methodSig, wantResult);
        for (int i = 0; i < args.length; i++)
            args[i] = args[i].split(" ")[0];
        return args;
    }

    private String[] getMethodArgNames(final String methodSig, final boolean wantResult)
    {
        final String[] args = getMethodArgs(methodSig, wantResult);
        for (int i = 0; i < args.length; i++)
            args[i] = args[i].split(" ")[1];
        return args;
    }

    /** Returns true if the given method signature is static. */
    private boolean isStatic(final String methodSig)
    {
        final int parenIndex = methodSig.indexOf("(");
        final String methodPrefix = methodSig.substring(0, parenIndex);
        for (final String token : methodPrefix.split(" "))
        {
            if (token.equals("static"))
                return true;
        }
        return false;
    }

    /** Returns true if the given method signature returns void. */
    private boolean isVoid(final String methodSig)
    {
        final int parenIndex = methodSig.indexOf("(");
        final String methodPrefix = methodSig.substring(0, parenIndex);
        return methodPrefix.startsWith("void ") || methodPrefix.indexOf(" void ") > 0;
    }

}