/*
 * Copyright (c) 2015, Technische Universität Kaiserslautern
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. 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.
 *
 * 3. Neither the name of the copyright holder 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 HOLDER
 * 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.
 *
 * Authors:
 *    Janik Schlemminger
 *    Robert Gernhardt
 *    Matthias Jung
 *    Felipe S. Prado
 *    Derek Christ
 */

#include "pythoncaller.h"
#include <exception>
#include <string>
#include <iostream>
#include <QFileInfo>
#include <QDebug>
#include <QApplication>

PythonCaller &PythonCaller::instance()
{
    static PythonCaller instance;
    return instance;
}

PythonCaller::PythonCaller() :
    metricModuleName("metrics"),
    metricFunctionName("calculateMetrics"),
    getMetricFunctionName("getMetrics"),

    pathToScripts(QApplication::applicationDirPath().toStdString() +
                  "/../../extensions/apps/traceAnalyzer/scripts/"),
    plotsModuleName("plots"),
    plotsFunctionName("generatePlots"),
    checkDependenciesModuleName("checkDependencies"),
    vcdExportModuleName("vcdExport"),
    vcdExportFunctionName("dumpVcd"),
    vcdExportDependenciesFunctionName("checkVcdExport")
{
    Py_Initialize();
    PyObject *sysPath = PySys_GetObject((char *)"path");
    PyObject *path = PyUnicode_FromString(this->pathToScripts.c_str());
    PyList_Insert(sysPath, 0, path);
    Py_DECREF(path);

    qDebug() << "Metric:" << metricModuleName.c_str() << metricFunctionName.c_str();
    qDebug() << "Plot:" << plotsModuleName.c_str() << plotsFunctionName.c_str();
    qDebug() << "VcdExport:" << vcdExportModuleName.c_str() << vcdExportFunctionName.c_str();
    qDebug() << "Script: " << pathToScripts.c_str();

    pCalculateMetricsFunction = loadFunctionFromModule(metricModuleName,
                                                       metricFunctionName);
    pGenPlotsFunction = loadFunctionFromModule(plotsModuleName, plotsFunctionName);
    pGetMetricsFunction = loadFunctionFromModule(metricModuleName,
                                                 getMetricFunctionName);

    pVcdExportDependenciesFunction = loadFunctionFromModule(checkDependenciesModuleName, vcdExportDependenciesFunctionName);

    if (vcdExportDependenciesAvailable())
        pVcdExportFunction = loadFunctionFromModule(vcdExportModuleName, vcdExportFunctionName);
    else
        std::cerr << "Warning: Python module pyvcd or tqdm not installed! Exporting as VCD not possible." << std::endl;
}


//returns new reference to the function (see: http://edcjones.tripod.com/refcount.html for the difference between "new reference" and "borrowed reference")
PyObject *PythonCaller::loadFunctionFromModule(std::string moduleName, std::string functionName)
{
    PyObject *pModuleName = PyUnicode_FromString(moduleName.c_str());
    PyObject *pModule = PyImport_Import(pModuleName);

    if (!pModule) {
        throw std::runtime_error(std::string("Could not load module " + moduleName));
    }

    PyObject *pFunction = PyObject_GetAttrString(pModule, functionName.c_str());

    if (!pFunction || !PyCallable_Check(pFunction)) {
        throw std::runtime_error(
            std::string("Could not load test function " + functionName + "in module " + moduleName));
    }

    Py_DECREF(pModuleName);
    Py_DECREF(pModule);
    return pFunction;
}

PythonCaller::~PythonCaller()
{
    Py_DECREF(pCalculateMetricsFunction);
    Py_DECREF(pGenPlotsFunction);
    Py_DECREF(pGetMetricsFunction);

    if (pVcdExportFunction)
        Py_DECREF(pVcdExportFunction);

    Py_DECREF(pVcdExportDependenciesFunction);
    Py_Finalize();
}

PyObject *PythonCaller::callMetricsFunction(PyObject *function, QString argument, std::vector<long> list)
{
    assert(PyCallable_Check(function));

    PyObject *pArgs = PyTuple_New(2);
    PyObject *pArgumentString = PyUnicode_FromString(
                                    argument.toStdString().c_str());
    PyObject *pArgumentList = PyList_New(list.size());
    for (size_t i = 0; i < list.size(); i++) {
        PyList_SetItem(pArgumentList, i, PyBool_FromLong(list[i]));
    }
    PyTuple_SetItem(pArgs, 0, pArgumentString);
    PyTuple_SetItem(pArgs, 1, pArgumentList);
    PyObject *pResult = PyObject_CallObject(function, pArgs);
    Py_DECREF(pArgs);

    if (!pResult) {
        PyErr_Print();
        throw std::runtime_error(
            std::string("Error in calling " + metricFunctionName + " in module " + metricModuleName));
    }

    return pResult;
}

//returns a new reference to result of function call
PyObject *PythonCaller::callFunctionWithStringArgument(PyObject *function,
                                                       QString argument)
{
    assert(PyCallable_Check(function));

    PyObject *pArgs = PyTuple_New(1);
    PyObject *pArgument = PyUnicode_FromString(argument.toStdString().c_str());
    PyTuple_SetItem(pArgs, 0, pArgument);
    PyObject *pResult = PyObject_CallObject(function, pArgs);
    Py_DECREF(pArgument);

    if (!pResult) {
        PyErr_Print();
        throw std::runtime_error(std::string("Error in calling function with string argument"));
    }

    return pResult;
}

PyObject *PythonCaller::callFunctionWithoutArguments(PyObject *function)
{
    assert(PyCallable_Check(function));
    PyObject *pResult = PyObject_CallObject(function, NULL);

    if (!pResult) {
        PyErr_Print();
        throw std::runtime_error(std::string("Error in calling python function"));
    }

    return pResult;
}

TraceCalculatedMetrics PythonCaller::calculateMetricsOnTrace(QString pathToTrace, std::vector<long> list)
{
    TraceCalculatedMetrics result(QFileInfo(pathToTrace).baseName());
    PyObject *pResult = callMetricsFunction(pCalculateMetricsFunction, pathToTrace,
                                            list);

    for (Py_ssize_t i = 0; i < PyList_Size(pResult); ++i) {
        PyObject *calculatedMetric = PyList_GetItem(pResult, i);
        QString metricName(PyUnicode_AsUTF8(PyTuple_GetItem(calculatedMetric, 0)));
        double  value = PyFloat_AsDouble(PyTuple_GetItem(calculatedMetric, 1));
        result.addCalculatedMetric(CalculatedMetric(metricName, value));
    }

    Py_DECREF(pResult);
    return result;
}

std::vector<std::string> PythonCaller::getMetrics(QString pathToTrace)
{
    std::vector<std::string> result;
    PyObject *pResult = callFunctionWithStringArgument(pGetMetricsFunction,
                                                       pathToTrace);

    for (Py_ssize_t i = 0; i < PyList_Size(pResult); ++i) {
        PyObject *metric = PyList_GetItem(pResult, i);
        QString metricName(PyUnicode_AsUTF8(metric));
        result.push_back(metricName.toStdString().c_str());
    }

    Py_DECREF(pResult);
    return result;
}

QString PythonCaller::generatePlotsOnTrace(QString pathToTrace)
{
    assert(PyCallable_Check(pGenPlotsFunction));

    PyObject *pResult = callFunctionWithStringArgument(pGenPlotsFunction,
                                                       pathToTrace);
    QString outputFiles (PyUnicode_AsUTF8(pResult));
    Py_DECREF(pResult);
    return outputFiles;

}

QString PythonCaller::exportAsVcd(QString pathToTrace)
{
    if (!pVcdExportFunction)
        return QString();

    PyObject *pResult = callFunctionWithStringArgument(pVcdExportFunction, pathToTrace);

    QString dump(PyUnicode_AsUTF8(pResult));
    Py_DECREF(pResult);
    return dump;
}

bool PythonCaller::vcdExportDependenciesAvailable()
{
    PyObject *result = callFunctionWithoutArguments(pVcdExportDependenciesFunction);
    bool available = PyObject_IsTrue(result);
    Py_DECREF(result);

    return available;
}
