/***************************************************************************
 * pythonscript.cpp
 * This file is part of the KDE project
 * copyright (C)2004-2005 by Sebastian Sauer (mail@dipe.org)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 * This program 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
 * Library General Public License for more details.
 * You should have received a copy of the GNU Library General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 ***************************************************************************/

#include "pythonscript.h"
#include "pythonmodule.h"
#include "pythoninterpreter.h"
#include "pythonsecurity.h"
#include "../main/scriptcontainer.h"

//#include <kapplication.h>

using namespace Kross::Python;

namespace Kross { namespace Python {

    /// @internal
    class PythonScriptPrivate
    {
        public:

            /**
            * The \a Py::Module instance this \a PythonScript
            * has as local context.
            */
            Py::Module* m_module;

            /**
            * The PyCodeObject object representing the
            * compiled python code. Internaly we first
            * compile the python code and later execute
            * it.
            */
            Py::Object* m_code;

            /**
            * A list of functionnames.
            */
            QStringList m_functions;

            /**
            * A list of classnames.
            */
            QStringList m_classes;
    };

}}

PythonScript::PythonScript(Kross::Api::Interpreter* interpreter, Kross::Api::ScriptContainer* scriptcontainer)
    : Kross::Api::Script(interpreter, scriptcontainer)
    , d(new PythonScriptPrivate())
{
#ifdef KROSS_PYTHON_SCRIPT_CTOR_DEBUG
    krossdebug("PythonScript::PythonScript() Constructor.");
#endif
    d->m_module = 0;
    d->m_code = 0;
}

PythonScript::~PythonScript()
{
#ifdef KROSS_PYTHON_SCRIPT_DTOR_DEBUG
    krossdebug("PythonScript::~PythonScript() Destructor.");
#endif
    finalize();
    delete d;
}

void PythonScript::initialize()
{
    finalize();
    clearException(); // clear previously thrown exceptions.

    try {
        if(m_scriptcontainer->getCode().isNull())
            throw Kross::Api::Exception::Ptr( new Kross::Api::Exception(QString("Invalid scripting code for script '%1'").arg( m_scriptcontainer->getName() )) );

        if(m_scriptcontainer->getName().isNull())
            throw Kross::Api::Exception::Ptr( new Kross::Api::Exception(QString("Name for the script is invalid!")) );

        PyObject* pymod = PyModule_New( (char*) m_scriptcontainer->getName().latin1() );
        d->m_module = new Py::Module(pymod, true);
        if(! d->m_module)
            throw Kross::Api::Exception::Ptr( new Kross::Api::Exception(QString("Failed to initialize local module context for script '%1'").arg( m_scriptcontainer->getName() )) );

#ifdef KROSS_PYTHON_SCRIPT_INIT_DEBUG
        krossdebug( QString("PythonScript::initialize() module='%1' refcount='%2'").arg(d->m_module->as_string().c_str()).arg(d->m_module->reference_count()) );
#endif

        // Set the "self" variable to point to the ScriptContainer
        // we are using for the script. That way we are able to
        // simply access the ScriptContainer itself from within
        // python scripting code.
        Py::Dict moduledict = d->m_module->getDict();
        moduledict["self"] = PythonExtension::toPyObject( m_scriptcontainer );
        //moduledict["parent"] = PythonExtension::toPyObject( m_manager );

/*
        // Prepare the local context.
        QString s =
            //"import sys\n"
            "if self.has(\"stdout\"):\n"
            "  self.stdout = Redirect( self.get(\"stdout\") )\n"
            "if self.has(\"stderr\"):\n"
            "  self.stderr = Redirect( self.get(\"stderr\") )\n"
            ;
        Py::Dict mainmoduledict = ((PythonInterpreter*)m_interpreter)->mainModule()->getDict();
        PyObject* pyrun = PyRun_StringFlags((char*)s.latin1(), Py_file_input, mainmoduledict.ptr(), moduledict.ptr());
        if(! pyrun)
            throw Py::Exception(); // throw exception
        Py_XDECREF(pyrun); // free the reference.
*/

        // Compile the python script code. It will be later on request
        // executed. That way we cache the compiled code.
        PyObject* code = 0;
        bool restricted = m_scriptcontainer->getOption("restricted", QVariant(false,0), true).toBool();

        krossdebug( QString("PythonScript::initialize() name=%1 restricted=%2").arg(m_scriptcontainer->getName()).arg(restricted) );
        if(restricted) {

            // Use the RestrictedPython module wrapped by the PythonSecurity class.
            code = dynamic_cast<PythonInterpreter*>(m_interpreter)->securityModule()->compile_restricted(
                m_scriptcontainer->getCode(),
                m_scriptcontainer->getName(),
                "exec"
            );

        }
        else {
            //PyCompilerFlags* cf = new PyCompilerFlags;
            //cf->cf_flags |= PyCF_SOURCE_IS_UTF8;

            // Just compile the code without any restrictions.
            code = Py_CompileString(
                (char*) m_scriptcontainer->getCode().latin1(),
                (char*) m_scriptcontainer->getName().latin1(),
                Py_file_input
            );
        }

        if(! code)
            throw Py::Exception();
        d->m_code = new Py::Object(code, true);
    }
    catch(Py::Exception& e) {
        QString err = Py::value(e).as_string().c_str();
        Kross::Api::Exception::Ptr exception = toException( QString("Failed to compile python code: %1").arg(err) );
        e.clear(); // exception is handled. clear it now.
        throw exception;
    }
}

void PythonScript::finalize()
{
#ifdef KROSS_PYTHON_SCRIPT_FINALIZE_DEBUG
    if(d->m_module)
        krossdebug( QString("PythonScript::finalize() module='%1' refcount='%2'").arg(d->m_module->as_string().c_str()).arg(d->m_module->reference_count()) );
#endif

    delete d->m_module; d->m_module = 0;
    delete d->m_code; d->m_code = 0;
    d->m_functions.clear();
    d->m_classes.clear();
}

Kross::Api::Exception::Ptr PythonScript::toException(const QString& error)
{
    long lineno = -1;
    QStringList errorlist;

    PyObject *type, *value, *traceback;
    PyErr_Fetch(&type, &value, &traceback);
    Py_FlushLine();
    PyErr_NormalizeException(&type, &value, &traceback);

    if(traceback) {
        Py::List tblist;
        try {
            Py::Module tbmodule( PyImport_Import(Py::String("traceback").ptr()), true );
            Py::Dict tbdict = tbmodule.getDict();
            Py::Callable tbfunc(tbdict.getItem("format_tb"));
            Py::Tuple args(1);
            args.setItem(0, Py::Object(traceback));
            tblist = tbfunc.apply(args);
            uint length = tblist.length();
            for(Py::List::size_type i = 0; i < length; ++i)
                errorlist.append( Py::Object(tblist[i]).as_string().c_str() );
        }
        catch(Py::Exception& e) {
            QString err = Py::value(e).as_string().c_str();
            e.clear(); // exception is handled. clear it now.
            krosswarning( QString("Kross::Python::PythonScript::toException() Failed to fetch a traceback: %1").arg(err) );
        }

        PyObject *next;
        while (traceback && traceback != Py_None) {
            PyFrameObject *frame = (PyFrameObject*)PyObject_GetAttrString(traceback, "tb_frame");
            Py_DECREF(frame);
            {
                PyObject *getobj = PyObject_GetAttrString(traceback, "tb_lineno");
                lineno = PyInt_AsLong(getobj);
                Py_DECREF(getobj);
            }
            if(Py_OptimizeFlag) {
                PyObject *getobj = PyObject_GetAttrString(traceback, "tb_lasti");
                int lasti = PyInt_AsLong(getobj);
                Py_DECREF(getobj);
                lineno = PyCode_Addr2Line(frame->f_code, lasti);
            }

            //const char* filename = PyString_AsString(frame->f_code->co_filename);
            //const char* name = PyString_AsString(frame->f_code->co_name);
            //errorlist.append( QString("%1#%2: \"%3\"").arg(filename).arg(lineno).arg(name) );

            next = PyObject_GetAttrString(traceback, "tb_next");
            Py_DECREF(traceback);
            traceback = next;
        }
    }

    if(lineno < 0) {
        if(value) {
            PyObject *getobj = PyObject_GetAttrString(value, "lineno");
            if(getobj) {
                lineno = PyInt_AsLong(getobj);
                Py_DECREF(getobj);
            }
        }
        if(lineno < 0)
            lineno = 0;
    }

    //PyErr_Restore(type, value, traceback);

    Kross::Api::Exception::Ptr exception = new Kross::Api::Exception(error, lineno - 1);
    if(errorlist.count() > 0)
        exception->setTrace( errorlist.join("\n") );
    return exception;
}

const QStringList& PythonScript::getFunctionNames()
{
    if(! d->m_module)
        initialize(); //TODO catch exception
    return d->m_functions;
    /*
    QStringList list;
    Py::List l = d->m_module->getDict().keys();
    int length = l.length();
    for(Py::List::size_type i = 0; i < length; ++i)
        list.append( l[i].str().as_string().c_str() );
    return list;
    */
}

Kross::Api::Object::Ptr PythonScript::execute()
{
#ifdef KROSS_PYTHON_SCRIPT_EXEC_DEBUG
    krossdebug( QString("PythonScript::execute()") );
#endif

    try {
        if(! d->m_module)
            initialize();

        // the main module dictonary.
        Py::Dict mainmoduledict = ((PythonInterpreter*)m_interpreter)->mainModule()->getDict();
        // the local context dictonary.
        Py::Dict moduledict( d->m_module->getDict().ptr() );

        // Initialize context before execution.
        QString s =
            "import sys\n"
            //"if self.has(\"stdout\"):\n"
            //"  sys.stdout = Redirect( self.get(\"stdout\") )\n"
            //"if self.has(\"stderr\"):\n"
            //"  sys.stderr = Redirect( self.get(\"stderr\") )\n"
            ;

        PyObject* pyrun = PyRun_String(s.latin1(), Py_file_input, mainmoduledict.ptr(), moduledict.ptr());
        if(! pyrun)
            throw Py::Exception(); // throw exception
        Py_XDECREF(pyrun); // free the reference.

        // Acquire interpreter lock*/
        PyGILState_STATE gilstate = PyGILState_Ensure();

        // Evaluate the already compiled code.
        PyObject* pyresult = PyEval_EvalCode(
            (PyCodeObject*)d->m_code->ptr(),
            mainmoduledict.ptr(),
            moduledict.ptr()
        );

        // Free interpreter lock
        PyGILState_Release(gilstate);

        if(! pyresult || PyErr_Occurred()) {
            krosswarning("Kross::Python::PythonScript::execute(): Failed to PyEval_EvalCode");
            throw Py::Exception();
        }
        Py::Object result(pyresult, true);

#ifdef KROSS_PYTHON_SCRIPT_EXEC_DEBUG
        krossdebug( QString("PythonScript::execute() result=%1").arg(result.as_string().c_str()) );
#endif

        for(Py::Dict::iterator it = moduledict.begin(); it != moduledict.end(); ++it) {
            Py::Dict::value_type vt(*it);
            if(PyClass_Check( vt.second.ptr() )) {
#ifdef KROSS_PYTHON_SCRIPT_EXEC_DEBUG
                krossdebug( QString("PythonScript::execute() class '%1' added.").arg(vt.first.as_string().c_str()) );
#endif
                d->m_classes.append( vt.first.as_string().c_str() );
            }
            else if(vt.second.isCallable()) {
#ifdef KROSS_PYTHON_SCRIPT_EXEC_DEBUG
                krossdebug( QString("PythonScript::execute() function '%1' added.").arg(vt.first.as_string().c_str()) );
#endif
                d->m_functions.append( vt.first.as_string().c_str() );
            }
        }

        Kross::Api::Object::Ptr r = PythonExtension::toObject(result);
        return r;
    }
    catch(Py::Exception& e) {
        try {
            Py::Object errobj = Py::value(e);
            if(errobj.ptr() == Py_None) // at least string-exceptions have there errormessage in the type-object
                errobj = Py::type(e);
            QString err = errobj.as_string().c_str();

            Kross::Api::Exception::Ptr exception = toException( QString("Failed to execute python code: %1").arg(err) );
            e.clear(); // exception is handled. clear it now.
            setException( exception );
        }
        catch(Py::Exception& e) {
            QString err = Py::value(e).as_string().c_str();
            Kross::Api::Exception::Ptr exception = toException( QString("Failed to execute python code: %1").arg(err) );
            e.clear(); // exception is handled. clear it now.
            setException( exception );
        }
    }
    catch(Kross::Api::Exception::Ptr e) {
        setException(e);
    }

    return 0; // return nothing if exception got thrown.
}

Kross::Api::Object::Ptr PythonScript::callFunction(const QString& name, Kross::Api::List::Ptr args)
{
#ifdef KROSS_PYTHON_SCRIPT_CALLFUNC_DEBUG
    krossdebug( QString("PythonScript::callFunction(%1, %2)")
                 .arg(name)
                 .arg(args ? QString::number(args->count()) : QString("NULL")) );
#endif

    if(hadException()) return 0; // abort if we had an unresolved exception.

    if(! d->m_module) {
        setException( new Kross::Api::Exception(QString("Script not initialized.")) );
        return 0;
    }

    try {
        Py::Dict moduledict = d->m_module->getDict();

        // Try to determinate the function we like to execute.
        PyObject* func = PyDict_GetItemString(moduledict.ptr(), name.latin1());

        if( (! d->m_functions.contains(name)) || (! func) )
            throw Kross::Api::Exception::Ptr( new Kross::Api::Exception(QString("No such function '%1'.").arg(name)) );

        Py::Callable funcobject(func, true); // the funcobject takes care of freeing our func pyobject.

        // Check if the object is really a function and therefore callable.
        if(! funcobject.isCallable())
            throw Kross::Api::Exception::Ptr( new Kross::Api::Exception(QString("Function is not callable.")) );

        // Call the function.
        Py::Object result = funcobject.apply(PythonExtension::toPyTuple(args));
        return PythonExtension::toObject(result);
    }
    catch(Py::Exception& e) {
        QString err = Py::value(e).as_string().c_str();
        e.clear(); // exception is handled. clear it now.
        setException( new Kross::Api::Exception(QString("Python Exception: %1").arg(err)) );
    }
    catch(Kross::Api::Exception::Ptr e) {
        setException(e);
    }

    return 0; // return nothing if exception got thrown.
}

const QStringList& PythonScript::getClassNames()
{
    if(! d->m_module)
        initialize(); //TODO catch exception
    return d->m_classes;
}

Kross::Api::Object::Ptr PythonScript::classInstance(const QString& name)
{
    if(hadException()) return 0; // abort if we had an unresolved exception.

    if(! d->m_module) {
        setException( new Kross::Api::Exception(QString("Script not initialized.")) );
        return 0;
    }

    try {
        Py::Dict moduledict = d->m_module->getDict();

        // Try to determinate the class.
        PyObject* pyclass = PyDict_GetItemString(moduledict.ptr(), name.latin1());
        if( (! d->m_classes.contains(name)) || (! pyclass) )
            throw Kross::Api::Exception::Ptr( new Kross::Api::Exception(QString("No such class '%1'.").arg(name)) );

        PyObject *pyobj = PyInstance_New(pyclass, 0, 0);//aclarg, 0);
        if(! pyobj)
            throw Kross::Api::Exception::Ptr( new Kross::Api::Exception(QString("Failed to create instance of class '%1'.").arg(name)) );

        Py::Object classobject(pyobj, true);

#ifdef KROSS_PYTHON_SCRIPT_CLASSINSTANCE_DEBUG
        krossdebug( QString("PythonScript::classInstance() inst='%1'").arg(classobject.as_string().c_str()) );
#endif
        return PythonExtension::toObject(classobject);
    }
    catch(Py::Exception& e) {
        QString err = Py::value(e).as_string().c_str();
        e.clear(); // exception is handled. clear it now.
        setException( Kross::Api::Exception::Ptr( new Kross::Api::Exception(err) ) );
    }
    catch(Kross::Api::Exception::Ptr e) {
        setException(e);
    }

    return 0; // return nothing if exception got thrown.
}

