/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtSCriptTools module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qscriptdebuggeragent_p.h"
#include "qscriptdebuggeragent_p_p.h"
#include "qscriptdebuggerbackend_p_p.h"

#include <QtCore/qcoreapplication.h>
#include <QtCore/qset.h>
#include <QtScript/qscriptengine.h>

QT_BEGIN_NAMESPACE

/*!
  \since 4.5
  \class QScriptDebuggerAgent
  \internal

  This class implements a state machine that uses the low-level events
  reported by the QScriptEngineAgent interface to implement debugging-
  specific functionality such as stepping and breakpoints. It is used
  internally by the QScriptDebuggerBackend class.
*/

QScriptDebuggerAgentPrivate::QScriptDebuggerAgentPrivate()
    : state(NoState), stepDepth(0), stepCount(0),
        targetScriptId(-1), targetLineNumber(-1), returnCounter(0),
        nextBreakpointId(1), hitBreakpointId(0),
        nextContextId(0), statementCounter(0)
{
}

QScriptDebuggerAgentPrivate::~QScriptDebuggerAgentPrivate()
{
}

QScriptDebuggerAgentPrivate *QScriptDebuggerAgentPrivate::get(
    QScriptDebuggerAgent *q)
{
    if (!q)
        return 0;
    return q->d_func();
}


/*!
  Constructs a new agent for the given \a engine. The agent will
  report debugging-related events (e.g. step completion) to the given
  \a backend.
*/
QScriptDebuggerAgent::QScriptDebuggerAgent(
    QScriptDebuggerBackendPrivate *backend, QScriptEngine *engine)
    : QScriptEngineAgent(engine), d_ptr(new QScriptDebuggerAgentPrivate())
{
    Q_D(QScriptDebuggerAgent);
    d->backend = backend;

    QScriptContext *ctx = engine->currentContext();
    while (ctx) {
        d->scriptIdStack.append(QList<qint64>());
        d->contextIdStack.append(d->nextContextId);
        ++d->nextContextId;
        ctx = ctx->parentContext();
    }
}

/*!
  Destroys this QScriptDebuggerAgent.
*/
QScriptDebuggerAgent::~QScriptDebuggerAgent()
{
    Q_D(QScriptDebuggerAgent);
    if (d->backend)
        d->backend->agentDestroyed(this);
    delete d;
}

/*!
  Instructs the agent to perform a "step into" operation.  This
  function returns immediately. The agent will report step completion
  at a later time, i.e. when script statements are evaluated.
*/
void QScriptDebuggerAgent::enterStepIntoMode(int count)
{
    Q_D(QScriptDebuggerAgent);
    d->state = QScriptDebuggerAgentPrivate::SteppingIntoState;
    d->stepCount = count;
    d->stepResult = QScriptValue();
}

/*!
  Instructs the agent to perform a "step over" operation.  This
  function returns immediately. The agent will report step completion
  at a later time, i.e. when script statements are evaluated.
*/
void QScriptDebuggerAgent::enterStepOverMode(int count)
{
    Q_D(QScriptDebuggerAgent);
    d->state = QScriptDebuggerAgentPrivate::SteppingOverState;
    if (engine()->isEvaluating())
        d->stepDepth = 0;
    else
        d->stepDepth = -1;
    d->stepCount = count;
    d->stepResult = QScriptValue();
}

/*!
  Instructs the agent to perform a "step out" operation.  This
  function returns immediately. The agent will report step completion
  at a later time, i.e. when script statements are evaluated.
*/
void QScriptDebuggerAgent::enterStepOutMode()
{
    Q_D(QScriptDebuggerAgent);
    d->state = QScriptDebuggerAgentPrivate::SteppingOutState;
    if (engine()->isEvaluating())
        d->stepDepth = 0;
    else
        d->stepDepth = -1;
}

/*!
  Instructs the agent to continue evaluation.
  This function returns immediately.
*/
void QScriptDebuggerAgent::enterContinueMode()
{
    Q_D(QScriptDebuggerAgent);
    d->state = QScriptDebuggerAgentPrivate::NoState;
}

/*!
  Instructs the agent to interrupt evaluation.
  This function returns immediately.
*/
void QScriptDebuggerAgent::enterInterruptMode()
{
    Q_D(QScriptDebuggerAgent);
    d->state = QScriptDebuggerAgentPrivate::InterruptingState;
}

/*!
  Instructs the agent to continue evaluation until the location
  described by \a fileName and \a lineNumber is reached.  This
  function returns immediately.
*/
void QScriptDebuggerAgent::enterRunToLocationMode(const QString &fileName, int lineNumber)
{
    Q_D(QScriptDebuggerAgent);
    d->targetFileName = fileName;
    d->targetLineNumber = lineNumber;
    d->targetScriptId = resolveScript(fileName);
    d->state = QScriptDebuggerAgentPrivate::RunningToLocationState;
}

/*!
  Instructs the agent to continue evaluation until the location
  described by \a scriptId and \a lineNumber is reached.  This
  function returns immediately.
*/
void QScriptDebuggerAgent::enterRunToLocationMode(qint64 scriptId, int lineNumber)
{
    Q_D(QScriptDebuggerAgent);
    d->targetScriptId = scriptId;
    d->targetFileName = QString();
    d->targetLineNumber = lineNumber;
    d->state = QScriptDebuggerAgentPrivate::RunningToLocationState;
}

void QScriptDebuggerAgent::enterReturnByForceMode(int contextIndex, const QScriptValue &value)
{
    Q_D(QScriptDebuggerAgent);
    d->returnCounter = contextIndex + 1;
    d->returnValue = QScriptValue();
    d->state = QScriptDebuggerAgentPrivate::ReturningByForceState;
    // throw an exception; we will catch it when the proper frame is popped
    engine()->currentContext()->throwValue(value);
}

/*!
  Sets a breakpoint defined by the given \a data.
  Returns an integer that uniquely identifies the new breakpoint,
  or -1 if setting the breakpoint failed.
*/
int QScriptDebuggerAgent::setBreakpoint(const QScriptBreakpointData &data)
{
    Q_D(QScriptDebuggerAgent);
    qint64 scriptId = data.scriptId();
    if (scriptId != -1) {
        if (!d->scripts.contains(scriptId)) {
            // that script has been unloaded, so invalidate the ID
            scriptId = -1;
            const_cast<QScriptBreakpointData&>(data).setScriptId(-1);
        } else if (data.fileName().isEmpty()) {
            QString fileName = d->scripts[scriptId].fileName();
            const_cast<QScriptBreakpointData&>(data).setFileName(fileName);
        }
    }

    int id = d->nextBreakpointId;
    ++d->nextBreakpointId;

    if (scriptId != -1) {
        d->resolvedBreakpoints[scriptId].append(id);
    } else {
        QString fileName = data.fileName();
        bool resolved = false;
        QScriptScriptMap::const_iterator it;
        for (it = d->scripts.constBegin(); it != d->scripts.constEnd(); ++it) {
            if (it.value().fileName() == fileName) {
                d->resolvedBreakpoints[it.key()].append(id);
                resolved = true;
                break;
            }
        }
        if (!resolved)
            d->unresolvedBreakpoints[fileName].append(id);
    }

    d->breakpoints.insert(id, data);

    return id;
}

/*!
  Deletes the breakpoint with the given \a id.
  Returns true if the breakpoint was deleted, false if
  no such breakpoint exists.
*/
bool QScriptDebuggerAgent::deleteBreakpoint(int id)
{
    Q_D(QScriptDebuggerAgent);
    if (!d->breakpoints.contains(id))
        return false;
    d->breakpoints.remove(id);
    bool found = false;
    {
        QHash<qint64, QList<int> >::iterator it;
        it = d->resolvedBreakpoints.begin();
        for ( ; !found && (it != d->resolvedBreakpoints.end()); ) {
            QList<int> &lst = it.value();
            Q_ASSERT(!lst.isEmpty());
            for (int i = 0; i < lst.size(); ++i) {
                if (lst.at(i) == id) {
                    lst.removeAt(i);
                    found = true;
                    break;
                }
            }
            if (lst.isEmpty())
                it = d->resolvedBreakpoints.erase(it);
            else
                ++it;
        }
    }
    if (!found) {
        QHash<QString, QList<int> >::iterator it;
        it = d->unresolvedBreakpoints.begin();
        for ( ; !found && (it != d->unresolvedBreakpoints.end()); ) {
            QList<int> &lst = it.value();
            Q_ASSERT(!lst.isEmpty());
            for (int i = 0; i < lst.size(); ++i) {
                if (lst.at(i) == id) {
                    lst.removeAt(i);
                    found = true;
                    break;
                }
            }
            if (lst.isEmpty())
                it = d->unresolvedBreakpoints.erase(it);
            else
                ++it;
        }
    }
    return found;
}

/*!
  Deletes all breakpoints.
*/
void QScriptDebuggerAgent::deleteAllBreakpoints()
{
    Q_D(QScriptDebuggerAgent);
    d->breakpoints.clear();
    d->resolvedBreakpoints.clear();
    d->unresolvedBreakpoints.clear();
}

/*!
  Returns the data associated with the breakpoint with the given \a
  id.
*/
QScriptBreakpointData QScriptDebuggerAgent::breakpointData(int id) const
{
    Q_D(const QScriptDebuggerAgent);
    return d->breakpoints.value(id);
}

/*!
  Sets the data associated with the breakpoint with the given \a
  id.
*/
bool QScriptDebuggerAgent::setBreakpointData(int id,
                                             const QScriptBreakpointData &data)
{
    Q_D(QScriptDebuggerAgent);
    if (!d->breakpoints.contains(id))
        return false;
    d->breakpoints[id] = data;
    return true;
}

/*!
  Returns all breakpoints.
*/
QScriptBreakpointMap QScriptDebuggerAgent::breakpoints() const
{
    Q_D(const QScriptDebuggerAgent);
    return d->breakpoints;
}

/*!
  Returns all scripts.
*/
QScriptScriptMap QScriptDebuggerAgent::scripts() const
{
    Q_D(const QScriptDebuggerAgent);
    return d->scripts;
}

/*!
  Returns the data associated with the script with the given \a id.
*/
QScriptScriptData QScriptDebuggerAgent::scriptData(qint64 id) const
{
    Q_D(const QScriptDebuggerAgent);
    return d->scripts.value(id);
}

/*!
  Checkpoints the current scripts.
*/
void QScriptDebuggerAgent::scriptsCheckpoint()
{
    Q_D(QScriptDebuggerAgent);
    d->previousCheckpointScripts = d->checkpointScripts;
    d->checkpointScripts = d->scripts;
}

/*!
  Returns the difference between the current checkpoint and the
  previous checkpoint. The first item in the pair is a list containing
  the identifiers of the scripts that were added. The second item in
  the pair is a list containing the identifiers of the scripts that
  were removed.
*/
QPair<QList<qint64>, QList<qint64> > QScriptDebuggerAgent::scriptsDelta() const
{
    Q_D(const QScriptDebuggerAgent);
    QSet<qint64> prevSet = d->previousCheckpointScripts.keys().toSet();
    QSet<qint64> currSet = d->checkpointScripts.keys().toSet();
    QSet<qint64> addedScriptIds = currSet - prevSet;
    QSet<qint64> removedScriptIds = prevSet - currSet;
    return qMakePair(addedScriptIds.toList(), removedScriptIds.toList());
}

/*!
  Returns the identifier of the script that has the given \a fileName,
  or -1 if there is no such script.
*/
qint64 QScriptDebuggerAgent::resolveScript(const QString &fileName) const
{
    Q_D(const QScriptDebuggerAgent);
    QScriptScriptMap::const_iterator it;
    for (it = d->scripts.constBegin(); it != d->scripts.constEnd(); ++it) {
        if (it.value().fileName() == fileName)
            return it.key();
    }
    return -1;
}

QList<qint64> QScriptDebuggerAgent::contextIds() const
{
    Q_D(const QScriptDebuggerAgent);
    return d->contextIdStack;
}

QPair<QList<qint64>, QList<qint64> > QScriptDebuggerAgent::contextsCheckpoint()
{
    Q_D(QScriptDebuggerAgent);
    int i = d->checkpointContextIdStack.size() - 1;
    int j = d->contextIdStack.size() - 1;
    for ( ; (i >= 0) && (j >= 0); --i, --j) {
        if (d->checkpointContextIdStack.at(i) != d->contextIdStack.at(j))
            break;
    }
    QList<qint64> removed = d->checkpointContextIdStack.mid(0, i+1);
    QList<qint64> added = d->contextIdStack.mid(0, j+1);
    d->checkpointContextIdStack = d->contextIdStack;
    return qMakePair(removed, added);
}

void QScriptDebuggerAgent::nullifyBackendPointer()
{
    Q_D(QScriptDebuggerAgent);
    d->backend = 0;
}

/*!
  \reimp
*/
void QScriptDebuggerAgent::scriptLoad(qint64 id, const QString &program,
                                      const QString &fileName, int baseLineNumber)
{
    Q_D(QScriptDebuggerAgent);
    QScriptScriptData data = QScriptScriptData(program, fileName, baseLineNumber);
    d->scripts.insert(id, data);

    if ((d->state == QScriptDebuggerAgentPrivate::RunningToLocationState)
        && (d->targetScriptId == -1)
        && ((d->targetFileName == fileName) || d->targetFileName.isEmpty())) {
        d->targetScriptId = id;
    }

    if (!fileName.isEmpty()) {
        QList<int> lst = d->unresolvedBreakpoints.take(fileName);
        if (!lst.isEmpty())
            d->resolvedBreakpoints.insert(id, lst);
    }
}

/*!
  \reimp
*/
void QScriptDebuggerAgent::scriptUnload(qint64 id)
{
    Q_D(QScriptDebuggerAgent);
    QScriptScriptData data = d->scripts.take(id);
    QString fileName = data.fileName();

    if ((d->state == QScriptDebuggerAgentPrivate::RunningToLocationState)
        && (d->targetScriptId == id)) {
        d->targetScriptId = -1;
        d->targetFileName = fileName;
    }

    if (!fileName.isEmpty()) {
        QList<int> lst = d->resolvedBreakpoints.take(id);
        if (!lst.isEmpty())
            d->unresolvedBreakpoints.insert(fileName, lst);
    }
}

/*!
  \reimp
*/
void QScriptDebuggerAgent::contextPush()
{
    Q_D(QScriptDebuggerAgent);
    d->scriptIdStack.append(QList<qint64>());
    d->contextIdStack.prepend(d->nextContextId);
    ++d->nextContextId;
}

/*!
  \reimp
*/
void QScriptDebuggerAgent::contextPop()
{
    Q_D(QScriptDebuggerAgent);
    d->scriptIdStack.removeLast();
    d->contextIdStack.removeFirst();
}

/*!
  \reimp
*/
void QScriptDebuggerAgent::functionEntry(qint64 scriptId)
{
    Q_D(QScriptDebuggerAgent);
    QList<qint64> &ids = d->scriptIdStack.last();
    ids.append(scriptId);
    if ((d->state == QScriptDebuggerAgentPrivate::SteppingOverState)
        || (d->state == QScriptDebuggerAgentPrivate::SteppingOutState)) {
        ++d->stepDepth;
    }
}

/*!
  \reimp
*/
void QScriptDebuggerAgent::functionExit(qint64 scriptId,
                                        const QScriptValue &returnValue)
{
    Q_UNUSED(scriptId);
    Q_D(QScriptDebuggerAgent);
    QList<qint64> &ids = d->scriptIdStack.last();
    ids.removeLast();
    if (d->state == QScriptDebuggerAgentPrivate::SteppingOverState) {
        --d->stepDepth;
    } else if (d->state == QScriptDebuggerAgentPrivate::SteppingOutState) {
        if (--d->stepDepth < 0) {
            d->stepResult = returnValue;
            d->state = QScriptDebuggerAgentPrivate::SteppedOutState;
        }
    } else if (d->state == QScriptDebuggerAgentPrivate::ReturningByForceState) {
        if (--d->returnCounter == 0) {
            d->returnValue = returnValue;
            d->state = QScriptDebuggerAgentPrivate::ReturnedByForceState;
            engine()->clearExceptions();
        }
    }
}

/*!
  \reimp
*/
void QScriptDebuggerAgent::positionChange(qint64 scriptId,
                                          int lineNumber, int columnNumber)
{
    Q_D(QScriptDebuggerAgent);
    if (engine()->processEventsInterval() == -1) {
        // see if it's time to call processEvents()
        if ((++d->statementCounter % 25000) == 0) {
            if (!d->processEventsTimer.isNull()) {
                if (d->processEventsTimer.elapsed() > 30) {
                    QCoreApplication::processEvents();
                    d->processEventsTimer.restart();
                }
            } else {
                d->processEventsTimer.start();
            }
        }
    }

    // check breakpoints
    {
        QList<int> lst = d->resolvedBreakpoints.value(scriptId);
        for (int i = 0; i < lst.size(); ++i) {
            int id = lst.at(i);
            QScriptBreakpointData &data = d->breakpoints[id];
            if (!data.isEnabled())
                continue;
            if (data.lineNumber() != lineNumber)
                continue;
            if (!data.condition().isEmpty()) {
                // ### careful, evaluate() can cause an exception
                // ### disable callbacks in nested evaluate?
                QScriptDebuggerAgentPrivate::State was = d->state;
                d->state = QScriptDebuggerAgentPrivate::NoState;
                QScriptValue ret = engine()->evaluate(
                    data.condition(),
                    QString::fromLatin1("Breakpoint %0 condition checker").arg(id));
                if (!ret.isError())
                    d->state = was;
                if (!ret.toBoolean())
                    continue;
            }
            if (!data.hit())
                continue;
            d->hitBreakpointId = id;
            d->state = QScriptDebuggerAgentPrivate::BreakpointState;
        }
    }

    switch (d->state) {
    case QScriptDebuggerAgentPrivate::NoState:
    case QScriptDebuggerAgentPrivate::SteppingOutState:
    case QScriptDebuggerAgentPrivate::ReturningByForceState:
        // Do nothing
        break;

    case QScriptDebuggerAgentPrivate::SteppingIntoState:
        if (--d->stepCount == 0) {
            d->state = QScriptDebuggerAgentPrivate::NoState;
            if (d->backend)
                d->backend->stepped(scriptId, lineNumber, columnNumber, QScriptValue());
        }
        break;

    case QScriptDebuggerAgentPrivate::SteppingOverState:
        if ((d->stepDepth > 0) || (--d->stepCount != 0))
            break;
        // fallthrough
    case QScriptDebuggerAgentPrivate::SteppedOverState:
        d->state = QScriptDebuggerAgentPrivate::NoState;
        if (d->backend)
            d->backend->stepped(scriptId, lineNumber, columnNumber, d->stepResult);
        break;

    case QScriptDebuggerAgentPrivate::SteppedOutState:
        d->state = QScriptDebuggerAgentPrivate::NoState;
        if (d->backend)
            d->backend->stepped(scriptId, lineNumber, columnNumber, d->stepResult);
        break;

    case QScriptDebuggerAgentPrivate::RunningToLocationState:
        if (((lineNumber == d->targetLineNumber) || (d->targetLineNumber == -1))
            && (scriptId == d->targetScriptId)) {
            d->state = QScriptDebuggerAgentPrivate::NoState;
            if (d->backend)
                d->backend->locationReached(scriptId, lineNumber, columnNumber);
        }
        break;

    case QScriptDebuggerAgentPrivate::InterruptingState:
        d->state = QScriptDebuggerAgentPrivate::NoState;
        if (d->backend)
            d->backend->interrupted(scriptId, lineNumber, columnNumber);
        break;

    case QScriptDebuggerAgentPrivate::BreakpointState:
        d->state = QScriptDebuggerAgentPrivate::NoState;
        if (d->backend)
            d->backend->breakpoint(scriptId, lineNumber, columnNumber, d->hitBreakpointId);
        if (d->breakpoints.value(d->hitBreakpointId).isSingleShot())
            deleteBreakpoint(d->hitBreakpointId);
        break;

    case QScriptDebuggerAgentPrivate::ReturnedByForceState:
        d->state = QScriptDebuggerAgentPrivate::NoState;
        if (d->backend)
            d->backend->forcedReturn(scriptId, lineNumber, columnNumber, d->returnValue);
        break;

    case QScriptDebuggerAgentPrivate::SteppedIntoState:
    case QScriptDebuggerAgentPrivate::ReachedLocationState:
    case QScriptDebuggerAgentPrivate::InterruptedState:
// ### deal with the case when code is evaluated while we're already paused
//        Q_ASSERT(false);
        break;
    }
}

/*!
  \reimp
*/
void QScriptDebuggerAgent::exceptionThrow(qint64 scriptId,
                                          const QScriptValue &exception,
                                          bool hasHandler)
{
    Q_D(QScriptDebuggerAgent);
    if (d->state == QScriptDebuggerAgentPrivate::ReturningByForceState) {
        // we threw this exception ourselves, so ignore it for now
        // (see functionExit()).
        return;
    }
    if (d->backend)
        d->backend->exception(scriptId, exception, hasHandler);
}

/*!
  \reimp
*/
void QScriptDebuggerAgent::exceptionCatch(qint64 scriptId,
                                          const QScriptValue &exception)
{
    Q_UNUSED(scriptId);
    Q_UNUSED(exception);
}

/*!
  \reimp
*/
bool QScriptDebuggerAgent::supportsExtension(Extension extension) const
{
    return (extension == DebuggerInvocationRequest);
}

/*!
  \reimp
*/
QVariant QScriptDebuggerAgent::extension(Extension extension,
                                         const QVariant &argument)
{
    Q_UNUSED(extension);
    Q_D(QScriptDebuggerAgent);
    Q_ASSERT(extension == DebuggerInvocationRequest);
    QVariantList lst = argument.toList();
    qint64 scriptId = lst.at(0).toLongLong();
    int lineNumber = lst.at(1).toInt();
    int columnNumber = lst.at(2).toInt();
    d->state = QScriptDebuggerAgentPrivate::NoState;
    if (d->backend) {
        d->backend->debuggerInvocationRequest(
            scriptId, lineNumber, columnNumber);
    }
    return QVariant();
}

QT_END_NAMESPACE
