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

import groovy.ui.GroovyMain;
import groovy.security.GroovyCodeSourcePermission;

import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.plugin.GroovyRunner;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.InvokerInvocationException;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.List;
import java.util.Map;

/**
 * Represents a groovy shell capable of running arbitrary groovy scripts
 *
 * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
 * @author Guillaume Laforge
 * @author Paul King
 */
public class GroovyShell extends GroovyObjectSupport {

    public static final String DEFAULT_CODE_BASE = "/groovy/shell";

    private Binding context;
    private int counter;
    private CompilerConfiguration config;
    private GroovyClassLoader loader;

    public static void main(String[] args) {
        GroovyMain.main(args);
    }

    public GroovyShell() {
        this(null, new Binding());
    }

    public GroovyShell(Binding binding) {
        this(null, binding);
    }

    public GroovyShell(ClassLoader parent, CompilerConfiguration config) {
        this(parent, new Binding(), config);
    }

    public GroovyShell(CompilerConfiguration config) {
        this(new Binding(), config);
    }

    public GroovyShell(Binding binding, CompilerConfiguration config) {
        this(null, binding, config);
    }

    public GroovyShell(ClassLoader parent, Binding binding) {
        this(parent, binding, CompilerConfiguration.DEFAULT);
    }

    public GroovyShell(ClassLoader parent) {
        this(parent, new Binding(), CompilerConfiguration.DEFAULT);
    }
    
    public GroovyShell(ClassLoader parent, Binding binding, final CompilerConfiguration config) {
        if (binding == null) {
            throw new IllegalArgumentException("Binding must not be null.");
        }
        if (config == null) {
            throw new IllegalArgumentException("Compiler configuration must not be null.");
        }
        final ClassLoader parentLoader = (parent!=null)?parent:GroovyShell.class.getClassLoader();
        this.loader = AccessController.doPrivileged(new PrivilegedAction<GroovyClassLoader>() {
            public GroovyClassLoader run() {
                return new GroovyClassLoader(parentLoader,config);
            }
        });
        this.context = binding;        
        this.config = config;
    }
    
    public void resetLoadedClasses() {
        loader.clearCache();
    }

    /**
     * Creates a child shell using a new ClassLoader which uses the parent shell's
     * class loader as its parent
     *
     * @param shell is the parent shell used for the variable bindings and the parent class loader
     */
    public GroovyShell(GroovyShell shell) {
        this(shell.loader, shell.context);
    }

    public Binding getContext() {
        return context;
    }

    public GroovyClassLoader getClassLoader() {
        return loader;
    }

    public Object getProperty(String property) {
        Object answer = getVariable(property);
        if (answer == null) {
            answer = super.getProperty(property);
        }
        return answer;
    }

    public void setProperty(String property, Object newValue) {
        setVariable(property, newValue);
        try {
            super.setProperty(property, newValue);
        } catch (GroovyRuntimeException e) {
            // ignore, was probably a dynamic property
        }
    }

    //
    // FIXME: Use List<String> here, current version is not safe
    //

    /**
     * A helper method which runs the given script file with the given command line arguments
     *
     * @param scriptFile the file of the script to run
     * @param list       the command line arguments to pass in
     */
    public Object run(File scriptFile, List list) throws CompilationFailedException, IOException {
        String[] args = new String[list.size()];
        return run(scriptFile, (String[]) list.toArray(args));
    }

    /**
     * A helper method which runs the given cl script with the given command line arguments
     *
     * @param scriptText is the text content of the script
     * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
     * @param list       the command line arguments to pass in
     */
    public Object run(String scriptText, String fileName, List list) throws CompilationFailedException {
        String[] args = new String[list.size()];
        list.toArray(args);
        return run(scriptText, fileName, args);
    }

    /**
     * Runs the given script file name with the given command line arguments
     *
     * @param scriptFile the file name of the script to run
     * @param args       the command line arguments to pass in
     */
    public Object run(final File scriptFile, String[] args) throws CompilationFailedException, IOException {
        String scriptName = scriptFile.getName();
        int p = scriptName.lastIndexOf(".");
        if (p++ >= 0) {
            if (scriptName.substring(p).equals("java")) {
                throw new CompilationFailedException(0, null);
            }
        }

        // Get the current context classloader and save it on the stack
        final Thread thread = Thread.currentThread();
        //ClassLoader currentClassLoader = thread.getContextClassLoader();

        class DoSetContext implements PrivilegedAction {
            ClassLoader classLoader;

            public DoSetContext(ClassLoader loader) {
                classLoader = loader;
            }

            public Object run() {
                thread.setContextClassLoader(classLoader);
                return null;
            }
        }

        AccessController.doPrivileged(new DoSetContext(loader));

        // Parse the script, generate the class, and invoke the main method.  This is a little looser than
        // if you are compiling the script because the JVM isn't executing the main method.
        Class scriptClass;
        try {
            scriptClass = AccessController.doPrivileged(new PrivilegedExceptionAction<Class>() {
                public Class run() throws CompilationFailedException, IOException {
                    return loader.parseClass(scriptFile);
                }
            });
        } catch (PrivilegedActionException pae) {
            Exception e = pae.getException();
            if (e instanceof CompilationFailedException) {
                throw (CompilationFailedException) e;
            } else if (e instanceof IOException) {
                throw (IOException) e;
            } else {
                throw (RuntimeException) pae.getException();
            }
        }

        return runScriptOrMainOrTestOrRunnable(scriptClass, args);

        // Set the context classloader back to what it was.
        //AccessController.doPrivileged(new DoSetContext(currentClassLoader));
    }

    /**
     * if (theClass is a Script) {
     * run it like a script
     * } else if (theClass has a main method) {
     * run the main method
     * } else if (theClass instanceof GroovyTestCase) {
     * use the test runner to run it
     * } else if (theClass implements Runnable) {
     * if (theClass has a constructor with String[] params)
     * instantiate theClass with this constructor and run
     * else if (theClass has a no-args constructor)
     * instantiate theClass with the no-args constructor and run
     * }
     */
    private Object runScriptOrMainOrTestOrRunnable(Class scriptClass, String[] args) {
        // Always set the "args" property, regardless of what path we take in the code.
        // Bad enough to have side effects but worse if their behavior is wonky.
        context.setProperty("args", args);

        if (scriptClass == null) {
            return null;
        }

        //TODO: This logic mostly duplicates InvokerHelper.createScript.  They should probably be unified.

        if (Script.class.isAssignableFrom(scriptClass)) {
            // treat it just like a script if it is one
            try {
                Constructor constructor = scriptClass.getConstructor(Binding.class);
                Script script = (Script) constructor.newInstance(context);
                return script.run();
            } catch (InstantiationException e) {
                // ignore instantiation errors,, try to do main
            } catch (IllegalAccessException e) {
               // ignore instantiation errors, try to do main
            } catch (NoSuchMethodException e) {
                try {
                    // Fallback for non-standard "Scripts" that don't have contextual constructor.
                    Script script = (Script) scriptClass.newInstance();
                    script.setBinding(context);
                    return script.run();
                } catch (InstantiationException e1) {
                    // ignore instantiation errors, try to do main
                } catch (IllegalAccessException e1) {
                    // ignore instantiation errors, try to do main
                }
            } catch (InvocationTargetException e) {
                // ignore instantiation errors, try to do main
            }
        }
        try {
            // let's find a main method
            scriptClass.getMethod("main", new Class[]{String[].class});
            // if that main method exist, invoke it
            return InvokerHelper.invokeMethod(scriptClass, "main", new Object[]{args});
        } catch (NoSuchMethodException e) {
            // if it implements Runnable, try to instantiate it
            if (Runnable.class.isAssignableFrom(scriptClass)) {
                return runRunnable(scriptClass, args);
            }
            // if it's a JUnit 3.8.x test, run it with an appropriate runner
            if (isJUnit3Test(scriptClass)) {
                return runJUnit3Test(scriptClass);
            }
            // if it's a JUnit 3.8.x test suite, run it with an appropriate runner
            if (isJUnit3TestSuite(scriptClass)) {
                return runJUnit3TestSuite(scriptClass);
            }
            // if it's a JUnit 4.x test, run it with an appropriate runner
            if (isJUnit4Test(scriptClass)) {
                return runJUnit4Test(scriptClass);
            }
            for (Map.Entry<String, GroovyRunner> entry : GroovySystem.RUNNER_REGISTRY.entrySet()) {
                GroovyRunner runner = entry.getValue();
                if (runner != null && runner.canRun(scriptClass, this.loader)) {
                    return runner.run(scriptClass, this.loader);
                }
            }
            String message = "This script or class could not be run.\n" +
                    "It should either:\n" +
                    "- have a main method,\n" +
                    "- be a JUnit test or extend GroovyTestCase,\n" +
                    "- implement the Runnable interface,\n" +
                    "- or be compatible with a registered script runner. Known runners:\n";
            if (GroovySystem.RUNNER_REGISTRY.isEmpty()) {
                message += "  * <none>";
            }
            for (Map.Entry<String, GroovyRunner> entry : GroovySystem.RUNNER_REGISTRY.entrySet()) {
                message += "  * " + entry.getKey() + "\n";
            }
            throw new GroovyRuntimeException(message);
        }
    }

    private Object runRunnable(Class scriptClass, String[] args) {
        Constructor constructor = null;
        Runnable runnable = null;
        Throwable reason = null;
        try {
            // first, fetch the constructor taking String[] as parameter
            constructor = scriptClass.getConstructor(new Class[]{(new String[]{}).getClass()});
            try {
                // instantiate a runnable and run it
                runnable = (Runnable) constructor.newInstance(new Object[]{args});
            } catch (Throwable t) {
                reason = t;
            }
        } catch (NoSuchMethodException e1) {
            try {
                // otherwise, find the default constructor
                constructor = scriptClass.getConstructor(new Class[]{});
                try {
                    // instantiate a runnable and run it
                    runnable = (Runnable) constructor.newInstance();
                } catch (InvocationTargetException ite) {
                    throw new InvokerInvocationException(ite.getTargetException());
                } catch (Throwable t) {
                    reason = t;
                }
            } catch (NoSuchMethodException nsme) {
                reason = nsme;
            }
        }
        if (constructor != null && runnable != null) {
            runnable.run();
        } else {
            throw new GroovyRuntimeException("This script or class was runnable but could not be run. ", reason);
        }
        return null;
    }

    /**
     * Run the specified class extending TestCase as a unit test.
     * This is done through reflection, to avoid adding a dependency to the JUnit framework.
     * Otherwise, developers embedding Groovy and using GroovyShell to load/parse/compile
     * groovy scripts and classes would have to add another dependency on their classpath.
     *
     * @param scriptClass the class to be run as a unit test
     */
    private Object runJUnit3Test(Class scriptClass) {
        try {
            Object testSuite = InvokerHelper.invokeConstructorOf("junit.framework.TestSuite", new Object[]{scriptClass});
            return InvokerHelper.invokeStaticMethod("junit.textui.TestRunner", "run", new Object[]{testSuite});
        } catch (ClassNotFoundException e) {
            throw new GroovyRuntimeException("Failed to run the unit test. JUnit is not on the Classpath.", e);
        }
    }

    /**
     * Run the specified class extending TestSuite as a unit test.
     * This is done through reflection, to avoid adding a dependency to the JUnit framework.
     * Otherwise, developers embedding Groovy and using GroovyShell to load/parse/compile
     * groovy scripts and classes would have to add another dependency on their classpath.
     *
     * @param scriptClass the class to be run as a unit test
     */
    private Object runJUnit3TestSuite(Class scriptClass) {
        try {
            Object testSuite = InvokerHelper.invokeStaticMethod(scriptClass, "suite", new Object[]{});
            return InvokerHelper.invokeStaticMethod("junit.textui.TestRunner", "run", new Object[]{testSuite});
        } catch (ClassNotFoundException e) {
            throw new GroovyRuntimeException("Failed to run the unit test. JUnit is not on the Classpath.", e);
        }
    }

    private Object runJUnit4Test(Class scriptClass) {
        try {
            return InvokerHelper.invokeStaticMethod("org.codehaus.groovy.vmplugin.v5.JUnit4Utils",
                    "realRunJUnit4Test", new Object[]{scriptClass, this.loader});
        } catch (ClassNotFoundException e) {
            throw new GroovyRuntimeException("Failed to run the JUnit 4 test.", e);
        }
    }

    /**
     * Utility method to check through reflection if the class appears to be a
     * JUnit 3.8.x test, i.e. checks if it extends JUnit 3.8.x's TestCase.
     *
     * @param scriptClass the class we want to check
     * @return true if the class appears to be a test
     */
    private boolean isJUnit3Test(Class scriptClass) {
        // check if the parsed class is a GroovyTestCase,
        // so that it is possible to run it as a JUnit test
        boolean isUnitTestCase = false;
        try {
            try {
                Class testCaseClass = this.loader.loadClass("junit.framework.TestCase");
                // if scriptClass extends testCaseClass
                if (testCaseClass.isAssignableFrom(scriptClass)) {
                    isUnitTestCase = true;
                }
            } catch (ClassNotFoundException e) {
                // fall through
            }
        } catch (Throwable e) {
            // fall through
        }
        return isUnitTestCase;
    }

     /**
     * Utility method to check through reflection if the class appears to be a
     * JUnit 3.8.x test suite, i.e. checks if it extends JUnit 3.8.x's TestSuite.
     *
     * @param scriptClass the class we want to check
     * @return true if the class appears to be a test
     */
    private boolean isJUnit3TestSuite(Class scriptClass) {
        // check if the parsed class is a TestSuite,
        // so that it is possible to run it as a JUnit test
        boolean isUnitTestSuite = false;
        try {
            try {
                Class testSuiteClass = this.loader.loadClass("junit.framework.TestSuite");
                // if scriptClass extends TestSuiteClass
                if (testSuiteClass.isAssignableFrom(scriptClass)) {
                    isUnitTestSuite = true;
                }
            } catch (ClassNotFoundException e) {
                // fall through
            }
        } catch (Throwable e) {
            // fall through
        }
        return isUnitTestSuite;
    }

    /**
     * Utility method to check via reflection if the parsed class appears to be a JUnit4
     * test, i.e. checks whether it appears to be using the relevant JUnit 4 annotations.
     *
     * @param scriptClass the class we want to check
     * @return true if the class appears to be a test
     */
    private boolean isJUnit4Test(Class scriptClass) {
        // check if there are appropriate class or method annotations
        // that suggest we have a JUnit 4 test
        boolean isTest = false;

        try {
            if (InvokerHelper.invokeStaticMethod("org.codehaus.groovy.vmplugin.v5.JUnit4Utils",
                    "realIsJUnit4Test", new Object[]{scriptClass, this.loader}) == Boolean.TRUE) {
                isTest = true;
            }
        } catch (ClassNotFoundException e) {
            throw new GroovyRuntimeException("Failed to invoke the JUnit 4 helper class.", e);
        }
        return isTest;
    }

    /**
     * Runs the given script text with command line arguments
     *
     * @param scriptText is the text content of the script
     * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
     * @param args       the command line arguments to pass in
     */
    public Object run(final String scriptText, final String fileName, String[] args) throws CompilationFailedException {
        GroovyCodeSource gcs = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() {
            public GroovyCodeSource run() {
                return new GroovyCodeSource(scriptText, fileName, DEFAULT_CODE_BASE);
            }
        });
        return run(gcs, args);
    }

    /**
     * Runs the given script source with command line arguments
     *
     * @param source    is the source content of the script
     * @param args      the command line arguments to pass in
     */
    public Object run(GroovyCodeSource source, List args) throws CompilationFailedException {
        return run(source, ((String[]) args.toArray(new String[args.size()])));
    }

    /**
     * Runs the given script source with command line arguments
     *
     * @param source    is the source content of the script
     * @param args      the command line arguments to pass in
     */
    public Object run(GroovyCodeSource source, String[] args) throws CompilationFailedException {
        Class scriptClass = parseClass(source);
        return runScriptOrMainOrTestOrRunnable(scriptClass, args);
    }

    /**
     * Runs the given script source with command line arguments
     *
     * @param source    is the source content of the script
     * @param args      the command line arguments to pass in
     */
    public Object run(URI source, List args) throws CompilationFailedException, IOException {
        return run(new GroovyCodeSource(source), ((String[]) args.toArray(new String[args.size()])));
    }

    /**
     * Runs the given script source with command line arguments
     *
     * @param source    is the source content of the script
     * @param args      the command line arguments to pass in
     */
    public Object run(URI source, String[] args) throws CompilationFailedException, IOException {
        return run(new GroovyCodeSource(source), args);
    }

    /**
     * Runs the given script with command line arguments
     *
     * @param in       the stream reading the script
     * @param fileName is the logical file name of the script (which is used to create the class name of the script)
     * @param list     the command line arguments to pass in
     */
    public Object run(final Reader in, final String fileName, List list) throws CompilationFailedException {
        return run(in, fileName, (String[]) list.toArray(new String[list.size()]));
    }

    /**
     * Runs the given script with command line arguments
     *
     * @param in       the stream reading the script
     * @param fileName is the logical file name of the script (which is used to create the class name of the script)
     * @param args     the command line arguments to pass in
     */
    public Object run(final Reader in, final String fileName, String[] args) throws CompilationFailedException {
        GroovyCodeSource gcs = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() {
                    public GroovyCodeSource run() {
                        return new GroovyCodeSource(in, fileName, DEFAULT_CODE_BASE);
                    }
        });
        Class scriptClass = parseClass(gcs);
        return runScriptOrMainOrTestOrRunnable(scriptClass, args);
    }

    public Object getVariable(String name) {
        return context.getVariables().get(name);
    }

    public void setVariable(String name, Object value) {
        context.setVariable(name, value);
    }

    /**
     * Evaluates some script against the current Binding and returns the result
     *
     * @param codeSource
     * @throws CompilationFailedException
     */
    public Object evaluate(GroovyCodeSource codeSource) throws CompilationFailedException {
        Script script = parse(codeSource);
        return script.run();
    }

    /**
     * Evaluates some script against the current Binding and returns the result
     *
     * @param scriptText the text of the script
     */
    public Object evaluate(final String scriptText) throws CompilationFailedException {
        return evaluate(scriptText, generateScriptName(), DEFAULT_CODE_BASE);
    }

    /**
     * Evaluates some script against the current Binding and returns the result
     *
     * @param scriptText the text of the script
     * @param fileName   is the logical file name of the script (which is used to create the class name of the script)
     */
    public Object evaluate(String scriptText, String fileName) throws CompilationFailedException {
        return evaluate(scriptText, fileName, DEFAULT_CODE_BASE);
    }

    /**
     * Evaluates some script against the current Binding and returns the result.
     * The .class file created from the script is given the supplied codeBase
     */
    public Object evaluate(final String scriptText, final String fileName, final String codeBase) throws CompilationFailedException {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new GroovyCodeSourcePermission(codeBase));
        }

        GroovyCodeSource gcs = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() {
            public GroovyCodeSource run() {
                return new GroovyCodeSource(scriptText, fileName, codeBase);
            }
        });

        return evaluate(gcs);
    }

    /**
     * Evaluates some script against the current Binding and returns the result
     *
     * @param file is the file of the script (which is used to create the class name of the script)
     */
    public Object evaluate(File file) throws CompilationFailedException, IOException {
        return evaluate(new GroovyCodeSource(file, config.getSourceEncoding()));
    }

    /**
     * Evaluates some script against the current Binding and returns the result
     *
     * @param uri is the URI of the script (which is used to create the class name of the script)
     */
    public Object evaluate(URI uri) throws CompilationFailedException, IOException {
        return evaluate(new GroovyCodeSource(uri));
    }

    /**
     * Evaluates some script against the current Binding and returns the result
     *
     * @param in the stream reading the script
     */
    public Object evaluate(Reader in) throws CompilationFailedException {
        return evaluate(in, generateScriptName());
    }

    /**
     * Evaluates some script against the current Binding and returns the result
     *
     * @param in       the stream reading the script
     * @param fileName is the logical file name of the script (which is used to create the class name of the script)
     */
    public Object evaluate(Reader in, String fileName) throws CompilationFailedException {
        Script script = null;
        try {
            script = parse(in, fileName);
            return script.run();
        } finally {
            if (script != null) {
                InvokerHelper.removeClass(script.getClass());
            }
        }
    }


    /**
     * Parses the given script and returns it ready to be run
     *
     * @param reader    the stream reading the script
     * @param fileName  is the logical file name of the script (which is used to create the class name of the script)
     * @return the parsed script which is ready to be run via {@link Script#run()}
     */
    public Script parse(final Reader reader, final String fileName) throws CompilationFailedException {
        return parse(new GroovyCodeSource(reader, fileName, DEFAULT_CODE_BASE));
    }

    /**
     * Parses the groovy code contained in codeSource and returns a java class.
     */
    private Class parseClass(final GroovyCodeSource codeSource) throws CompilationFailedException {
        // Don't cache scripts
        return loader.parseClass(codeSource, false);
    }

    /**
     * Parses the given script and returns it ready to be run.  When running in a secure environment
     * (-Djava.security.manager) codeSource.getCodeSource() determines what policy grants should be
     * given to the script.
     *
     * @param codeSource
     * @return ready to run script
     */
    public Script parse(final GroovyCodeSource codeSource) throws CompilationFailedException {
        return InvokerHelper.createScript(parseClass(codeSource), context);
    }

    /**
     * Parses the given script and returns it ready to be run
     *
     * @param file is the file of the script (which is used to create the class name of the script)
     */
    public Script parse(File file) throws CompilationFailedException, IOException {
        return parse(new GroovyCodeSource(file, config.getSourceEncoding()));
    }

    /**
     * Parses the given script and returns it ready to be run
     *
     * @param uri is the URI of the script (which is used to create the class name of the script)
     */
    public Script parse(URI uri) throws CompilationFailedException, IOException {
        return parse(new GroovyCodeSource(uri));
    }

    /**
     * Parses the given script and returns it ready to be run
     *
     * @param scriptText the text of the script
     */
    public Script parse(String scriptText) throws CompilationFailedException {
        return parse(scriptText, generateScriptName());
    }

    public Script parse(final String scriptText, final String fileName) throws CompilationFailedException {
        GroovyCodeSource gcs = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() {
            public GroovyCodeSource run() {
                return new GroovyCodeSource(scriptText, fileName, DEFAULT_CODE_BASE);
            }
        });
        return parse(gcs);
    }

    /**
     * Parses the given script and returns it ready to be run
     *
     * @param in the stream reading the script
     */
    public Script parse(Reader in) throws CompilationFailedException {
        return parse(in, generateScriptName());
    }

    protected synchronized String generateScriptName() {
        return "Script" + (++counter) + ".groovy";
    }
}
