package org.python.util;

import java.io.Reader;
import java.io.StringReader;
import java.util.Properties;

import org.python.antlr.base.mod;
import org.python.core.CompileMode;
import org.python.core.CompilerFlags;
import org.python.core.ParserFacade;
import org.python.core.Py;
import org.python.core.PyCode;
import org.python.core.PyException;
import org.python.core.PyFile;
import org.python.core.PyFileWriter;
import org.python.core.PyModule;
import org.python.core.PyObject;
import org.python.core.PyString;
import org.python.core.PyStringMap;
import org.python.core.PySystemState;
import org.python.core.__builtin__;
import org.python.core.PyFileReader;

/**
 * The PythonInterpreter class is a standard wrapper for a Jython interpreter for use embedding in a
 * Java application.
 */
public class PythonInterpreter {

    PyModule module;

    protected PySystemState systemState;

    PyObject locals;

    protected CompilerFlags cflags = new CompilerFlags();

    /**
     * Initializes the jython runtime. This should only be called once, and should be called before
     * any other python objects are created (including a PythonInterpreter).
     *
     * @param preProperties
     *            A set of properties. Typically System.getProperties() is used.
     *            PreProperties override properties from the registry file.
     * @param postProperties
     *            An other set of properties. Values like python.home, python.path and all other
     *            values from the registry files can be added to this property set. PostProperties
     *            will override system properties and registry properties.
     * @param argv
     *            Command line argument. These values will assigned to sys.argv.
     */
    public static void initialize(Properties preProperties, Properties postProperties, String[] argv) {
        PySystemState.initialize(preProperties, postProperties, argv);
    }

    /**
     * Create a new Interpreter with an empty dictionary
     */
    public PythonInterpreter() {
        this(null, null);
    }

    /**
     * Create a new interpreter with the given dictionary to use as its namespace
     */
    public PythonInterpreter(PyObject dict) {
        this(dict, null);
    }

    public PythonInterpreter(PyObject dict, PySystemState systemState) {
        if (dict == null) {
            dict = new PyStringMap();
        }
        if (systemState == null) {
            systemState = Py.getSystemState();
            if (systemState == null) {
                systemState = new PySystemState();
            }
        }
        this.systemState = systemState;
        setState();
        module = new PyModule("__main__", dict);
        systemState.modules.__setitem__("__main__", module);
        locals = dict;
    }

    protected void setState() {
        Py.setSystemState(systemState);
    }

    /**
     * Set the Python object to use for the standard input stream
     *
     * @param inStream
     *            Python file-like object to use as input stream
     */
    public void setIn(PyObject inStream) {
        systemState.stdin = inStream;
    }

    public void setIn(java.io.Reader inStream) {
        setIn(new PyFileReader(inStream));
    }

    /**
     * Set a java.io.InputStream to use for the standard input stream
     *
     * @param inStream
     *            InputStream to use as input stream
     */
    public void setIn(java.io.InputStream inStream) {
        setIn(new PyFile(inStream));
    }

    /**
     * Set the Python object to use for the standard output stream
     *
     * @param outStream
     *            Python file-like object to use as output stream
     */
    public void setOut(PyObject outStream) {
        systemState.stdout = outStream;
    }

    public void setOut(java.io.Writer outStream) {
        setOut(new PyFileWriter(outStream));
    }

    /**
     * Set a java.io.OutputStream to use for the standard output stream
     *
     * @param outStream
     *            OutputStream to use as output stream
     */
    public void setOut(java.io.OutputStream outStream) {
        setOut(new PyFile(outStream));
    }

    public void setErr(PyObject outStream) {
        systemState.stderr = outStream;
    }

    public void setErr(java.io.Writer outStream) {
        setErr(new PyFileWriter(outStream));
    }

    public void setErr(java.io.OutputStream outStream) {
        setErr(new PyFile(outStream));
    }

    /**
     * Evaluate a string as Python source and return the result
     */
    public PyObject eval(String s) {
        setState();
        return __builtin__.eval(new PyString(s), locals);
    }

    /**
     * Evaluate a Python code object and return the result
     */
    public PyObject eval(PyObject code) {
        setState();
        return __builtin__.eval(code, locals, locals);
    }

    /**
     * Execute a string of Python source in the local namespace
     */
    public void exec(String s) {
        setState();
        Py.exec(Py.compile_flags(s, "<string>", CompileMode.exec, cflags), locals, locals);
        Py.flushLine();
    }

    /**
     * Execute a Python code object in the local namespace
     */
    public void exec(PyObject code) {
        setState();
        Py.exec(code, locals, locals);
        Py.flushLine();
    }

    /**
     * Execute a file of Python source in the local namespace
     */
    public void execfile(String filename) {
        setState();
        __builtin__.execfile_flags(filename, locals, locals, cflags);
        Py.flushLine();
    }

    public void execfile(java.io.InputStream s) {
        execfile(s, "<iostream>");
    }

    public void execfile(java.io.InputStream s, String name) {
        setState();
        Py.runCode(Py.compile_flags(s, name, CompileMode.exec, cflags), locals, locals);
        Py.flushLine();
    }

    /**
     * Compile a string of Python source as either an expression (if possible) or module.
     *
     * Designed for use by a JSR 223 implementation: "the Scripting API does not distinguish
     * between scripts which return values and those which do not, nor do they make the
     * corresponding distinction between evaluating or executing objects." (SCR.4.2.1)
     */
    public PyCode compile(String script) {
        return compile(script, "<script>");
    }
    public PyCode compile(Reader reader) {
        return compile(reader, "<script>");
    }
    public PyCode compile(String script, String filename) {
        return compile(new StringReader(script), filename);
    }
    public PyCode compile(Reader reader, String filename) {
        mod node = ParserFacade.parseExpressionOrModule(reader, filename, cflags);
        setState();
        return Py.compile_flags(node, filename, CompileMode.eval, cflags);
    }


    public PyObject getLocals() {
        return locals;
    }

    public void setLocals(PyObject d) {
        locals = d;
    }

    /**
     * Set a variable in the local namespace
     *
     * @param name
     *            the name of the variable
     * @param value
     *            the value to set the variable to. Will be automatically converted to an
     *            appropriate Python object.
     */
    public void set(String name, Object value) {
        locals.__setitem__(name.intern(), Py.java2py(value));
    }

    /**
     * Set a variable in the local namespace
     *
     * @param name
     *            the name of the variable
     * @param value
     *            the value to set the variable to
     */
    public void set(String name, PyObject value) {
        locals.__setitem__(name.intern(), value);
    }

    /**
     * Get the value of a variable in the local namespace
     *
     * @param name
     *            the name of the variable
     * @return the value of the variable, or null if that name isn't assigned
     */
    public PyObject get(String name) {
        return locals.__finditem__(name.intern());
    }

    /**
     * Get the value of a variable in the local namespace Value will be returned as an instance of
     * the given Java class. <code>interp.get("foo", Object.class)</code> will return the most
     * appropriate generic Java object.
     *
     * @param name
     *            the name of the variable
     * @param javaclass
     *            the class of object to return
     * @return the value of the variable as the given class, or null if that name isn't assigned
     */
    public <T> T get(String name, Class<T> javaclass) {
        PyObject val = locals.__finditem__(name.intern());
        if (val == null) {
            return null;
        }
        return Py.tojava(val, javaclass);
    }

    public void cleanup() {
        systemState.callExitFunc();
        try {
            Py.getSystemState().stdout.invoke("flush");
        } catch (PyException pye) {
            // fall through
        }
        try {
            Py.getSystemState().stderr.invoke("flush");
        } catch (PyException pye) {
            // fall through
        }
    }
}
