/*
    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor,
    Boston, MA  02110-1301, USA.

    ---
    Copyright (C) 2009 Alexander Rieder <alexanderrieder@gmail.com>
 */

#include "maximasession.h"
#include "maximaexpression.h"
#include "maximatabcompletionobject.h"
#include "maximasyntaxhelpobject.h"
#include "maximahighlighter.h"

#include <QTimer>
#include <QTcpSocket>
#include <QTcpServer>
#include <kdebug.h>
#include <kprocess.h>
#include <kmessagebox.h>
#include <klocale.h>
#include <signal.h>
#include "settings.h"

#include "result.h"

//NOTE: the \\s in the expressions is needed, because Maxima seems to sometimes insert newlines/spaces between the letters
//maybe this is caused by some behaviour if the Prompt is split into multiple "readStdout" calls
//the Expressions are encapsulated in () to allow capturing for the text
const QRegExp MaximaSession::MaximaPrompt=QRegExp("(\\(\\s*%\\s*I\\s*[0-9\\s]*\\))"); //Text, maxima outputs, if it's taking new input
const QRegExp MaximaSession::MaximaOutputPrompt=QRegExp("(\\(\\s*%\\s*O\\s*[0-9\\s]*\\))"); //Text, maxima outputs, before any output


static QByteArray initCmd="display2d:false$                     \n"\
                          "inchar:%I$                           \n"\
                          "outchar:%O$                          \n"\
                          "print(____END_OF_INIT____);          \n";
static QByteArray helperInitCmd="simp: false$ \n";

MaximaSession::MaximaSession( Cantor::Backend* backend) : Session(backend)
{
    kDebug();
    m_isInitialized=false;
    m_isHelperReady=false;
    m_server=0;
    m_maxima=0;
    m_process=0;
    m_helperProcess=0;
    m_helperMaxima=0;
    m_justRestarted=false;
    m_useLegacy=false;
}

MaximaSession::~MaximaSession()
{
    kDebug();
}

void MaximaSession::login()
{
    kDebug()<<"login";
    if (m_process)
        m_process->deleteLater();
    if(!m_server||!m_server->isListening())
        startServer();

    m_maxima=0;
    m_process=new KProcess(this);
    QStringList args;
    //TODO: these parameters may need tweaking to run on windows (see wxmaxima for hints)
    if(m_useLegacy)
        args<<"-r"<<QString(":lisp (setup-server %1)").arg(m_server->serverPort());
    else
        args<<"-r"<<QString(":lisp (setup-client %1)").arg(m_server->serverPort());

    m_process->setProgram(MaximaSettings::self()->path().toLocalFile(),args);

    m_process->start();

    connect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(restartMaxima()));
    connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(reportProcessError(QProcess::ProcessError)));

    if(!m_helperQueue.isEmpty())
       runNextHelperCommand();
}

void MaximaSession::startServer()
{
    kDebug()<<"starting up maxima server";
    const int defaultPort=4060;
    int port=defaultPort;
    m_server=new QTcpServer(this);
    connect(m_server, SIGNAL(newConnection()), this, SLOT(newConnection()));

    while(! m_server->listen(QHostAddress::LocalHost, port) )
    {
        kDebug()<<"Could not listen to "<<port;
        port++;
        kDebug()<<"Now trying "<<port;

        if(port>defaultPort+50)
        {
            KMessageBox::error(0, i18n("Could not start the server."), i18n("Error - Cantor"));
            return;
        }
    }

    kDebug()<<"got a server on "<<port;
}

void MaximaSession::newMaximaClient(QTcpSocket* socket)
{
    kDebug()<<"got new maxima client";
    m_maxima=socket;
    connect(m_maxima, SIGNAL(readyRead()), this, SLOT(readStdOut()));
    m_maxima->write(initCmd);
}

void MaximaSession::newHelperClient(QTcpSocket* socket)
{
    kDebug()<<"got new helper client";
    m_helperMaxima=socket;

    connect(m_helperMaxima, SIGNAL(readyRead()), this, SLOT(readHelperOut()));

    m_helperMaxima->write(helperInitCmd);
    m_helperMaxima->write(initCmd);
}

void MaximaSession::logout()
{
    kDebug()<<"logout";

    if(!m_process||!m_maxima)
        return;

    disconnect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(restartMaxima()));

    if(m_expressionQueue.isEmpty())
    {
        m_maxima->write("quit();\n");
        m_maxima->flush();
        //evaluateExpression("quit();", Cantor::Expression::DeleteOnFinish);
    }
    else
    {
        m_expressionQueue.clear();
    }

    //Give maxima time to clean up
    kDebug()<<"waiting for maxima to finish";

    if(m_process->state()!=QProcess::NotRunning)
    {
        if(!m_maxima->waitForDisconnected(3000))
        {
            m_process->kill();
            m_maxima->waitForDisconnected(3000);
        }
    }

    m_maxima->close();

    kDebug()<<"done logging out";

    delete m_process;
    m_process=0;
    delete m_helperProcess;
    m_helperProcess=0;
    delete m_helperMaxima;
    m_helperMaxima=0;
    delete m_maxima;
    m_maxima=0;

    kDebug()<<"destroyed maxima";

    m_expressionQueue.clear();
}

void MaximaSession::newConnection()
{
    kDebug()<<"new connection";
    QTcpSocket* const socket=m_server->nextPendingConnection();
    if(m_maxima==0)
    {
        newMaximaClient(socket);
    }else if (m_helperMaxima==0)
    {
        newHelperClient(socket);
    }else
    {
        kDebug()<<"got another client, without needing one";
    }
}

Cantor::Expression* MaximaSession::evaluateExpression(const QString& cmd, Cantor::Expression::FinishingBehavior behave)
{
    kDebug()<<"evaluating: "<<cmd;
    MaximaExpression* expr=new MaximaExpression(this);
    expr->setFinishingBehavior(behave);
    expr->setCommand(cmd);
    expr->evaluate();

    return expr;
}

MaximaExpression* MaximaSession::evaluateHelperExpression(const QString& cmd)
{
    if(!m_helperMaxima)
        startHelperProcess();
    MaximaExpression* expr=new MaximaExpression(this, MaximaExpression::HelpExpression);
    expr->setFinishingBehavior(Cantor::Expression::DoNotDelete);
    expr->setCommand(cmd);
    expr->evaluate();

    return expr;
}

void MaximaSession::appendExpressionToQueue(MaximaExpression* expr)
{
    m_expressionQueue.append(expr);

    kDebug()<<"queue: "<<m_expressionQueue.size();
    if(m_expressionQueue.size()==1)
    {
        changeStatus(Cantor::Session::Running);
        runFirstExpression();
    }
}

void MaximaSession::appendExpressionToHelperQueue(MaximaExpression* expr)
{
   m_helperQueue.append(expr);

   kDebug()<<"helper queue: "<<m_helperQueue.size();
   if(m_helperQueue.size()==1)
   {
       runNextHelperCommand();
   }
}

void MaximaSession::readStdOut()
{
    kDebug()<<"reading stdOut";
    QString out=m_maxima->readAll();
    kDebug()<<"out: "<<out;


    m_cache+=out;

    if(m_cache.contains(QRegExp(QString("%1 %2").arg(MaximaOutputPrompt.pattern()).arg("____END_OF_INIT____"))))
    {
        kDebug()<<"initialized";
        out.remove("____END_OF_INIT____");

        m_isInitialized=true;
        m_cache.clear();
        runFirstExpression();

        QTimer::singleShot(0, this, SLOT(killLabels()));

        return;
    }

    if(!m_isInitialized)
        return;

    if(m_cache.contains('\n')||m_cache.contains(MaximaPrompt))
    {
        kDebug()<<"letting parse"<<m_cache;
        letExpressionParseOutput();
    }
}

void MaximaSession::letExpressionParseOutput()
{
    kDebug()<<"queuesize: "<<m_expressionQueue.size();
    if(m_isInitialized&&!m_expressionQueue.isEmpty())
    {
        MaximaExpression* expr=m_expressionQueue.first();

        //send over the part of the cache to the last newline or last InputPrompt, whatever comes last
        const int index=m_cache.lastIndexOf('\n')+1;
        const int index2=MaximaPrompt.lastIndexIn(m_cache)+MaximaPrompt.matchedLength();
        const int max=qMax(index, index2);
        QString txt=m_cache.left(max);
        expr->parseOutput(txt);
        m_cache.remove(0, max);
    }
}

void MaximaSession::killLabels()
{
    Cantor::Expression* e=evaluateExpression("kill(labels);", Cantor::Expression::DeleteOnFinish);
    connect(e, SIGNAL(statusChanged(Cantor::Expression::Status)), this, SIGNAL(ready()));
}

void MaximaSession::reportProcessError(QProcess::ProcessError e)
{
    kDebug()<<"process error";
    if(e==QProcess::FailedToStart)
    {
        changeStatus(Cantor::Session::Done);
        emit error(i18n("Failed to start Maxima"));
    }
}

void MaximaSession::readHelperOut()
{
    kDebug()<<"reading stdOut from helper process";
    QString out=m_helperMaxima->readAll();
    kDebug()<<"out: "<<out;

    if(out.contains(QRegExp(QString("%1 %2").arg(MaximaOutputPrompt.pattern()).arg("____END_OF_INIT____"))))
    {
        kDebug()<<"helper initialized";

        m_isHelperReady=true;
        runNextHelperCommand();

        return;
    }

    if(!m_isHelperReady)
        return;

    kDebug()<<"queuesize: "<<m_helperQueue.size();
    if(!m_helperQueue.isEmpty())
    {
        MaximaExpression* expr=m_helperQueue.first();
        kDebug()<<"needs latex?: "<<expr->needsLatexResult();

        expr->parseOutput(out);

        if(expr->type()==MaximaExpression::TexExpression&&!expr->needsLatexResult())
        {
            kDebug()<<"expression doesn't need latex anymore";
            m_helperQueue.removeFirst();
            runNextHelperCommand();
        }
    }
}

void MaximaSession::currentExpressionChangedStatus(Cantor::Expression::Status status)
{
    if(status!=Cantor::Expression::Computing) //The session is ready for the next command
    {
        kDebug()<<"expression finished";
        MaximaExpression* expression=m_expressionQueue.first();
        disconnect(expression, SIGNAL(statusChanged(Cantor::Expression::Status)),
                   this, SLOT(currentExpressionChangedStatus(Cantor::Expression::Status)));

        if(expression->needsLatexResult())
        {
            kDebug()<<"asking for tex version";
            expression->setType(MaximaExpression::TexExpression);

            m_helperQueue<<expression;
            if(m_helperQueue.size()==1) //It only contains the actual item. start processing it
                runNextHelperCommand();
        }

        kDebug()<<"running next command";
        m_expressionQueue.removeFirst();
        if(m_expressionQueue.isEmpty())
            changeStatus(Cantor::Session::Done);
        runFirstExpression();

    }

}

void MaximaSession::currentHelperExpressionChangedStatus(Cantor::Expression::Status status)
{
    if(status!=Cantor::Expression::Computing) //The session is ready for the next command
    {
        kDebug()<<"expression finished";
        MaximaExpression* expression=m_helperQueue.first();
        disconnect(expression, SIGNAL(statusChanged(Cantor::Expression::Status)),
                   this, SLOT(currentHelperExpressionChangedStatus(Cantor::Expression::Status)));

        kDebug()<<"running next command";
        m_helperQueue.removeFirst();
        if(m_helperQueue.isEmpty())
            runNextHelperCommand();

    }

}

void MaximaSession::runFirstExpression()
{
    kDebug()<<"running next expression";

    if(m_isInitialized&&!m_expressionQueue.isEmpty())
    {
        MaximaExpression* expr=m_expressionQueue.first();
        QString command=expr->internalCommand();
        connect(expr, SIGNAL(statusChanged(Cantor::Expression::Status)), this, SLOT(currentExpressionChangedStatus(Cantor::Expression::Status)));

        if(command.isEmpty())
        {
            kDebug()<<"empty command";
            expr->forceDone();
        }else
        {
            kDebug()<<"writing "<<command+'\n'<<" to the process";
            m_cache.clear();
            m_maxima->write((command+'\n').toLatin1());
        }
    }
}

void MaximaSession::runNextHelperCommand()
{
    kDebug()<<"helperQueue: "<<m_helperQueue.size();
    if(m_isHelperReady&&!m_helperQueue.isEmpty())
    {
        kDebug()<<"running next helper command";
        MaximaExpression* expr=m_helperQueue.first();

        if(expr->type()==MaximaExpression::TexExpression)
        {
            QStringList out=expr->output();

            if(!out.isEmpty())
            {
                QString texCmd;
                foreach(const QString& part, out)
                {
                    if(part.isEmpty())
                        continue;
                    kDebug()<<"running "<<QString("tex(%1);").arg(part);
                    texCmd+=QString("tex(%1);").arg(part);
                }
                texCmd+='\n';
                m_helperMaxima->write(texCmd.toUtf8());
            }else
            {
                kDebug()<<"current tex request is empty, so drop it";
                m_helperQueue.removeFirst();
            }
        }else
        {
            QString command=expr->internalCommand();
            connect(expr, SIGNAL(statusChanged(Cantor::Expression::Status)), this, SLOT(currentHelperExpressionChangedStatus(Cantor::Expression::Status)));

            if(command.isEmpty())
            {
                kDebug()<<"empty command";
                expr->forceDone();
            }else
            {
                kDebug()<<"writing "<<command+'\n'<<" to the process";
                m_cache.clear();
                m_helperMaxima->write((command+'\n').toLatin1());
            }
        }
    }
}

void MaximaSession::interrupt()
{
    if(!m_expressionQueue.isEmpty())
        m_expressionQueue.first()->interrupt();

    m_expressionQueue.clear();
    changeStatus(Cantor::Session::Done);
}

void MaximaSession::interrupt(MaximaExpression* expr)
{
    Q_ASSERT(!m_expressionQueue.isEmpty());

    if(expr==m_expressionQueue.first())
    {
        disconnect(m_maxima, 0);
        disconnect(expr, 0, this, 0);
        restartMaxima();
        kDebug()<<"done interrupting";
    }else
    {
        m_expressionQueue.removeAll(expr);
    }
}

void MaximaSession::sendInputToProcess(const QString& input)
{
    kDebug()<<"WARNING: use this method only if you know what you're doing. Use evaluateExpression to run commands";
    kDebug()<<"running "<<input;
    m_maxima->write(input.toLatin1());
}

void MaximaSession::restartMaxima()
{
    kDebug()<<"restarting maxima cooldown: "<<m_justRestarted;

    if(!m_justRestarted)
    {
        //If maxima finished, before the session was initialized
        //We try to use Legacy commands for startups (Maxima <5.18)
        //In this case, don't require the cooldown
        if(!m_isInitialized)
        {
            m_useLegacy=!m_useLegacy;
            kDebug()<<"Initializing maxima failed now trying legacy support: "<<m_useLegacy;
        }
        else
        {
             emit error(i18n("Maxima crashed. restarting..."));
             //remove the command that caused maxima to crash (to avoid infinite loops)
             if(!m_expressionQueue.isEmpty())
                 m_expressionQueue.removeFirst();

            m_justRestarted=true;
            QTimer::singleShot(1000, this, SLOT(restartsCooledDown()));
        }

        disconnect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(restartMaxima()));
        login();
    }else
    {
        KMessageBox::error(0, i18n("Maxima crashed twice within a short time. Stopping to try starting"), i18n("Error - Cantor"));
    }
}

void MaximaSession::restartsCooledDown()
{
    kDebug()<<"maxima restart cooldown";
    m_justRestarted=false;
}

void MaximaSession::startHelperProcess()
{
    kDebug()<<"starting helper";
    m_helperMaxima=0;
    m_isHelperReady=false;
    if(!m_server)
    {
        kDebug()<<"WARNING: you tried to create a helper process, without running server";
        return;
    }
    //Start the process that is used to convert to LaTeX
    m_helperProcess=new KProcess(this);
    QStringList args;
    if(m_useLegacy)
        args<<"-r"<<QString(":lisp (setup-server %1)").arg(m_server->serverPort());
    else
        args<<"-r"<<QString(":lisp (setup-client %1)").arg(m_server->serverPort());

    m_helperProcess->setProgram(MaximaSettings::self()->path().toLocalFile(),args);

    connect(m_helperProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(startHelperProcess()));
    m_helperProcess->start();
}

void MaximaSession::setTypesettingEnabled(bool enable)
{
    if(enable)
    {
        if(!m_isHelperReady)
            startHelperProcess();
        //LaTeX and Display2d don't go together and even deliver wrong results
        evaluateExpression("display2d:false", Cantor::Expression::DeleteOnFinish);
    }
    else if(m_helperProcess)
    {
        disconnect(m_helperProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(startHelperProcess()));
        m_helperProcess->deleteLater();
        m_helperProcess=0;
        m_helperMaxima=0;
        m_isHelperReady=false;
    }
    Cantor::Session::setTypesettingEnabled(enable);
}

Cantor::TabCompletionObject* MaximaSession::tabCompletionFor(const QString& command)
{
    return new MaximaTabCompletionObject(command, this);
}

Cantor::SyntaxHelpObject* MaximaSession::syntaxHelpFor(const QString& command)
{
    return new MaximaSyntaxHelpObject(command, this);
}

QSyntaxHighlighter* MaximaSession::syntaxHighlighter(QTextEdit* parent)
{
    return new MaximaHighlighter(parent);
}

#include "maximasession.moc"
