/* This file RenderEngine.cpp is part of the EqualX project https://launchpad.net/equalx
 * Copyright (C) 2013  Mihai Niculescu
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <QDebug>

#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QTextStream>

#include "Util.h"
#include "RenderEngine.h"

namespace EqualX{

bool RenderEngine::mIsRunning = false;
bool RenderEngine::isRunning()
{
    return mIsRunning;
}



/**************************************************************
 * RenderEngine implementation
 **************************************************************/

RenderEngine::RenderEngine(QObject *parent) :
    QObject(parent),
    mPathPDFLatex(),
    mPathGS(),
    mConverters(),
    mConvertToFileTypes(),
    mBuildDir(""),
    mLatexLog(""),
    mLatexErrors(),
    mChildProcess(0),
    mRunningProcesses(0)
{

}

RenderEngine::~RenderEngine()
{


    stop();
}

void RenderEngine::stop()
{
    if(mChildProcess)
    {
        if(mRunningProcesses>0){
            for(int i=0; i<mRunningProcesses;++i){
                QProcess* p = &mChildProcess[i];
                if(p){
                    p->close();
                    delete p;
                    p=0;
                }
            }
            mRunningProcesses=0;
            delete [] mChildProcess;
        }
        else{
            mChildProcess->close();
            //delete mChildProcess;

        }
        mChildProcess=0;
    }


    mConvertToFileTypes.clear();
    mIsRunning = false;
    // restore app current dir
    QDir::setCurrent(mAppDirSave);
    qDebug() << "[RenderEngine] - Engine Stoppped";

}

QString RenderEngine::buildDir() const
{
    return mBuildDir;
}

EqualX::ConvertersList RenderEngine::converters() const
{
    return mConverters;
}

QString RenderEngine::latexLog() const
{
    return mLatexLog;
}

const ErrorsList *RenderEngine::latexErrors() const
{
    return &mLatexErrors;
}

QString RenderEngine::file(const QString &type) const
{
    QString filePath;

    if(!fileTypes().contains(type)){
        qDebug() << "[RenderEngine::file] File type:"<<type<<" is not supported";
        return filePath;
    }

    filePath = buildDir().append(TEMP_FILE_NAME).append(".").append(type);

    if(!QFile::exists(filePath)){
        qDebug() << "[RenderEngine::file] No file "<< filePath <<" was generated for this type:"<<type<<". Did you forget to run first a converter for this type?";
        return QString();
    }


    return filePath;
}

QStringList RenderEngine::fileTypes() const
{
    QStringList ft = mConverters.keys();
    ft << "pdf" << "tex"; // provided by default but they not in the converters list

    ft.sort();

    return ft;
}

bool RenderEngine::parseLatexLog()
{
    mLatexErrors.clear();

    if(!mLatexLog.contains("!")){  // do not try to parse the string for errors when there are none
        return false;
    }

    QString message;
    int lineNumber;

    int pos1 = mLatexLog.indexOf("!", 0);
    int pos2 = -1;
    while(pos1>0){
        pos2 = mLatexLog.indexOf("\n", pos1);

        message = mLatexLog.mid(pos1+1, pos2-pos1-1);

        pos1 = mLatexLog.indexOf("l.", pos2, Qt::CaseSensitive);
        pos2 = mLatexLog.indexOf(" ", pos1);

        lineNumber = (mLatexLog.mid(pos1+2, pos2-pos1-2)).toInt();

        qDebug() << "[RenderEngine] Latex Error - line: " << lineNumber<<" message: ["<<message<<"]";

        mLatexErrors.insertMulti(lineNumber, message);

        pos1 = mLatexLog.indexOf("!", pos1);
    }

    return true;
}

void RenderEngine::setBuildDir(const QString &dir)
{
    mBuildDir = QDir::toNativeSeparators(dir+"/");
}

void RenderEngine::setFileInfo(const FileInfo &info)
{
    mFile.setInfo(info);
}

void RenderEngine::addConverter(const QString &type, const Converter &converter)
{
    mConverters.insert(type, converter);
}

void RenderEngine::registerConverters(const ConvertersList &conv)
{
    mConverters = conv;
}

void RenderEngine::setPDFLatex(const QString &path)
{
    mPathPDFLatex = path;
}

void RenderEngine::setGS(const QString &path)
{
    mPathGS = path;
}

void RenderEngine::run(const QString &filetype)
{
    if(!fileTypes().contains(filetype)){
        qDebug() << "[RenderEngine::run] No converter for file type:"<<filetype<<". Aborting...";
        return;
    }

    if(filetype.contains("pdf", Qt::CaseInsensitive) || filetype.contains("tex", Qt::CaseInsensitive)){ // these are created by default, no need to process again
        return;
    }

    mIsRunning = true;
    mAppDirSave = QDir::currentPath();
    QDir::setCurrent(mBuildDir);

    EqualX::Converter converter = mConverters.value(filetype);

    QString com = QString("%1 %2").arg(converter.path, converter.opts).arg(TEMP_PDF_FILE, QString(TEMP_FILE_NAME));
    qDebug() << "[RenderEngine::run] Starting converter for:"<< filetype <<"  [" << com<<"]";

    QProcess::execute(com);

    mFile.open(TEMP_FILE_NAME"."+filetype);
    mFile.write();
    mFile.close();

    mIsRunning = false;
    QDir::setCurrent(mAppDirSave);

}

void RenderEngine::run(const QStringList& filetypes, bool freshRun)
{
    if(mIsRunning){
        qDebug() << "[RenderEngine] Engine is already running. Aborting Run...";
        return;
    }

    if(mPathGS.isEmpty() || mPathPDFLatex.isEmpty()) {
        qDebug() << "[RenderEngine] pdfLatex or gs are not set. Aborting Run...";
        return;
    }

    // sanity check
    mConvertToFileTypes = filetypes.filter(QRegExp("^((?!pdf|tex).)*$")); // filter file types for which we do not have converters
    mConvertToFileTypes.removeDuplicates();

    //save current app directory, we restore to this after we stop()
    mAppDirSave = QDir::currentPath();

    QDir::setCurrent(mBuildDir);
    mIsRunning = true;
    emit started();

    if(!freshRun) {
        runConvertersOnFileTypes();
        return;
    }

    mFile.writeMetadataFile();
    mFile.writeLatexFile();

    QString com = QString("\"%1\" -interaction=nonstopmode -jobname %2 %3").arg(mPathPDFLatex, TEMP_FILE_NAME, TEMP_LATEX_FILE);
    qDebug() << "[RenderEngine] Starting pdfLatex converter: " << com;

    mChildProcess = new QProcess(this);
    mChildProcess->setProcessChannelMode(QProcess::MergedChannels);

    connect(mChildProcess, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(finishedPdfLatex(int,QProcess::ExitStatus)) );
    connect(mChildProcess, SIGNAL(error(QProcess::ProcessError)), this, SIGNAL(error(QProcess::ProcessError)) );

    mChildProcess->start(com);

    //if(mChildProcess->state()==QProcess::NotRunning) finish(-1, QProcess::CrashExit);
}

void RenderEngine::runConvertersOnFileTypes()
{
    mRunningProcesses = mConvertToFileTypes.size();
    if(mRunningProcesses<=0){
        qDebug() << "[RenderEngine::runConvertersOnFileTypes] No converters available to run. Aborting...";
        finish(-1, QProcess::NormalExit);
        return;
    }

    mChildProcess = new QProcess[mRunningProcesses];

    for(int i=0; i<mRunningProcesses; ++i) {
        EqualX::Converter converter = mConverters.value(mConvertToFileTypes.at(i));

        QString com = QString("\"%1\" %2").arg(converter.path, converter.opts).arg(TEMP_PDF_FILE, QString(TEMP_FILE_NAME));
        qDebug() << "[RenderEngine::runConvertersOnFileTypes] Starting converter:"<< mConvertToFileTypes.at(i) <<"  [" << com<<"]";

        mChildProcess[i].setObjectName(TEMP_FILE_NAME"."+mConvertToFileTypes.at(i));
        mChildProcess[i].setProcessChannelMode(QProcess::MergedChannels);

        connect(&mChildProcess[i], SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(finishedConverter(int,QProcess::ExitStatus)) );

        mChildProcess[i].start(com);

    }
}


void RenderEngine::finishedPdfLatex(int exitCode,QProcess::ExitStatus exitStatus)
{
    qDebug() << "[RenderEngine::finishedPdfLatex] pdfLatex finished with ExitCode:"<<exitCode<<" ExitStatus:"<<exitStatus;
    mChildProcess->disconnect();

    // not interested of latex exitCode since latex exitCode!=0 if there are latex errors
    // and we want to parse and display them
    if(exitStatus != QProcess::NormalExit){
        qDebug() << "[RenderEngine::finishedPdfLatex] pdfLatex crashed with error:"<<mChildProcess->errorString();
        finish(0, exitStatus);
        return;
    }

    mLatexLog = mChildProcess->readAll();
    if(parseLatexLog()) { // if latex has errors
        qDebug() << "[RenderEngine::finishedPdfLatex] latex has errors.Aborting...";
        finish(0,QProcess::NormalExit);
        return;
    }
    mChildProcess->close(); delete mChildProcess; mChildProcess=0;

    qDebug() << "[RenderEngine::finishedPdfLatex] Running ghostscript to compute the bounding box of the generated pdf";

    QString com = QString("\"%1\" -DBATCH -DNOPAUSE -sDEVICE=bbox %2").arg(mPathGS,TEMP_PDF_FILE);

    qDebug() << "[RenderEngine::finishedPdfLatex] Starting command:["<<com<<"]";

    mChildProcess = new QProcess(this);
    mChildProcess->setProcessChannelMode(QProcess::MergedChannels);
    connect(mChildProcess, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(finishedGS(int,QProcess::ExitStatus)) );

    mChildProcess->start(com);
}

void RenderEngine::finishedGS(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/)
{
    mChildProcess->disconnect();

    QString gsoutput(mChildProcess->readAll()); // gs output

    gsoutput = gsoutput.simplified();

    int idxS = gsoutput.indexOf("%%BoundingBox:");
    int idxE = gsoutput.indexOf("%%HiResBoundingBox:");
    int len = QString("%%BoundingBox:").length();

    QString bboxParamsStr = gsoutput.mid(idxS+len, idxE-idxS-len);

    qDebug() << "[RenderEngine::finishedGS] gs returned boundingbox: "<<bboxParamsStr;

    QStringList bboxParamsStrList = bboxParamsStr.split(" ", QString::SkipEmptyParts);

    if(bboxParamsStrList.size()!=4){
        qDebug() << "[RenderEngine::finishedGS] invalid boundingbox. Aborting...";
        finish(-1,QProcess::NormalExit);
        return;
    }

    mChildProcess->close(); delete mChildProcess; mChildProcess=0;

    //parse the output from gs
    float llx, lly, urx, ury; // will hold the boundingbox params
    llx = bboxParamsStrList.at(0).toFloat()-5;
    lly = bboxParamsStrList.at(1).toFloat()-5;
    urx = bboxParamsStrList.at(2).toFloat()+5;
    ury = bboxParamsStrList.at(3).toFloat()+5;

    // now include the background color in the .tex file
    QString com;

    // if we have a background with color, we need to regenerate the PDF including pagecolor and then we apply the boundingbox
    if(mFile.info().bgColor().alpha()!=0){
        mFile.writeLatexFile(true);

        com = QString("\"%1\" -interaction=nonstopmode -jobname %2 %3").arg(mPathPDFLatex, TEMP_FILE_NAME, TEMP_LATEX_FILE);
        qDebug() << "[RenderEngine::finishedGS] Starting pdfLatex converter on the latex with background: " << com;

        QProcess::execute(com);
    }

    mFile.writeLatexFileCropped(llx,lly,urx,ury);
    com = QString("\"%1\" -interaction=nonstopmode -jobname %2 %3").arg(mPathPDFLatex, TEMP_FILE_NAME"-cropped", TEMP_LATEX_CROP_FILE);
    qDebug() << "[RenderEngine::finishedGS] Starting pdfLatex converter on the latex-cropped: " << com;

    mChildProcess = new QProcess(this);
    mChildProcess->setProcessChannelMode(QProcess::MergedChannels);

    connect(mChildProcess, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(finishedPdfCrop(int,QProcess::ExitStatus)) );

    mChildProcess->start(com);
}

void RenderEngine::finishedPdfCrop(int /*exitCode*/,QProcess::ExitStatus /*exitStatus*/)
{
    mChildProcess->disconnect();

    // leave only one PDF file in the build dir - this file will be retrieved by generatedFile("pdf")
    QFile::remove(TEMP_PDF_FILE);

    qDebug() << "[RenderEngine::finishedPdfCrop] Rename ["<< TEMP_PDFCROP_FILE<<"] to ["<<  TEMP_PDF_FILE << "]";

    if(!QFile::rename(TEMP_PDFCROP_FILE, TEMP_PDF_FILE) )
        qDebug() << "[RenderEngine::finishedPdfCrop] couldn't rename ["<< TEMP_PDFCROP_FILE<<"] to ["<< TEMP_PDF_FILE << "]";

    mChildProcess->close(); delete mChildProcess; mChildProcess=0;

    runConvertersOnFileTypes();
}

void RenderEngine::finishedConverter(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/)
{
    // release allocated memory
    QProcess* proc   = static_cast<QProcess*>(QObject::sender());
    QString filename = proc->objectName();
    proc->close();

    mRunningProcesses--;

    mFile.open(filename);
    mFile.write();
    mFile.close();

    //check if we have other converters running
    if(mRunningProcesses==0){
        finish(0, QProcess::NormalExit);
        return;
    }

}

void RenderEngine::finish(int exitCode,QProcess::ExitStatus exitStatus)
{
    qDebug() << "[RenderEngine] - Finish Rendering: cleaning up resources...";
    stop();
    qDebug() << "[RenderEngine] - emitting signals...";

    emit finished();
    emit finished(exitCode, exitStatus);
}


} // end namespace EqualX
