/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** 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.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**   * Redistributions of source code must retain the above copyright
**     notice, this list of conditions and the following disclaimer.
**   * 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.
**   * Neither the name of The Qt Company Ltd 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
** OWNER 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."
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "scriptdebugger.h"
#include "scriptbreakpointmanager.h"

#include <QtScript/QScriptEngine>
#include <QtScript/QScriptEngineAgent>
#include <QtScript/QScriptContextInfo>
#include <QtScript/QScriptValueIterator>
#include <QtCore/QTextStream>
#include <QtCore/QStack>

static QString safeValueToString(const QScriptValue &value)
{
    if (value.isObject())
        return QLatin1String("[object Object]");
    else
        return value.toString();
}

class ScriptInfo;
class ScriptBreakpointManager;

class ScriptDebuggerPrivate
    : public QScriptEngineAgent
{
    Q_DECLARE_PUBLIC(ScriptDebugger)
public:
    enum Mode {
        Run,
        StepInto,
        StepOver
    };

    ScriptDebuggerPrivate(QScriptEngine *engine);
    ~ScriptDebuggerPrivate();

    // QScriptEngineAgent interface
    void scriptLoad(qint64 id, const QString &program,
                    const QString &fileName, int lineNumber);
    void scriptUnload(qint64 id);

    void positionChange(qint64 scriptId,
                        int lineNumber, int columnNumber);

    void functionEntry(qint64 scriptId);
    void functionExit(qint64 scriptId,
                      const QScriptValue &returnValue);

    void exceptionThrow(qint64 scriptId,
                        const QScriptValue &exception, bool hasHandler);


    void interactive();
    bool executeCommand(const QString &command, const QStringList &args);

    void setMode(Mode mode);
    Mode mode() const;

    int frameCount() const;
    void setCurrentFrameIndex(int index);
    int currentFrameIndex() const;

    QScriptContext *frameContext(int index) const;
    QScriptContext *currentFrameContext() const;

    ScriptInfo *scriptInfo(QScriptContext *context) const;

    int listLineNumber() const;
    void setListLineNumber(int lineNumber);

    QString readLine();
    void output(const QString &text);
    void message(const QString &text);
    void errorMessage(const QString &text);

    // attributes
    QTextStream *m_defaultInputStream;
    QTextStream *m_defaultOutputStream;
    QTextStream *m_defaultErrorStream;
    QTextStream *m_inputStream;
    QTextStream *m_outputStream;
    QTextStream *m_errorStream;

    ScriptBreakpointManager *m_bpManager;
    Mode m_mode;
    QMap<qint64, ScriptInfo*> m_scripts;
    QMap<QScriptContext*, QStack<qint64> > m_contextProgramIds;

    QString m_lastInteractiveCommand;
    QString m_commandPrefix;
    int m_stepDepth;
    int m_currentFrameIndex;
    int m_listLineNumber;

    ScriptDebugger *q_ptr;
};

class ScriptInfo
{
public:
    ScriptInfo(const QString &code, const QString &fileName, int lineNumber)
        : m_code(code), m_fileName(fileName), m_lineNumber(lineNumber)
        { }

    inline QString code() const
        { return m_code; }
    inline QString fileName() const
        { return m_fileName; }
    inline int lineNumber() const
        { return m_lineNumber; }

    QString lineText(int lineNumber);
    QMap<int, int> m_lineOffsets;

private:
    int lineOffset(int lineNumber);

    QString m_code;
    QString m_fileName;
    int m_lineNumber;
};

int ScriptInfo::lineOffset(int lineNumber)
{
    QMap<int, int>::const_iterator it = m_lineOffsets.constFind(lineNumber);
    if (it != m_lineOffsets.constEnd())
        return it.value();

    int offset;
    it = m_lineOffsets.constFind(lineNumber - 1);
    if (it != m_lineOffsets.constEnd()) {
        offset = it.value();
        offset = m_code.indexOf(QLatin1Char('\n'), offset);
        if (offset != -1)
            ++offset;
        m_lineOffsets.insert(lineNumber, offset);
    } else {
        int index;
        it = m_lineOffsets.lowerBound(lineNumber);
        if (it != m_lineOffsets.constBegin())
            --it;
        if (it != m_lineOffsets.constBegin()) {
            index = it.key();
            offset = it.value();
        } else {
            index = m_lineNumber;
            offset = 0;
        }
        int j = index;
        for ( ; j < lineNumber; ++j) {
            m_lineOffsets.insert(j, offset);
            offset = m_code.indexOf(QLatin1Char('\n'), offset);
            if (offset == -1)
                break;
            ++offset;
        }
        m_lineOffsets.insert(j, offset);
    }
    return offset;
}

QString ScriptInfo::lineText(int lineNumber)
{
    int startOffset = lineOffset(lineNumber);
    if (startOffset == -1)
        return QString();
    int endOffset = lineOffset(lineNumber + 1);
    if (endOffset == -1)
        return m_code.mid(startOffset);
    else
        return m_code.mid(startOffset, endOffset - startOffset - 1);
}



ScriptDebuggerPrivate::ScriptDebuggerPrivate(QScriptEngine *engine)
    : QScriptEngineAgent(engine), m_mode(Run)
{
    m_commandPrefix = QLatin1String(".");
    m_bpManager = new ScriptBreakpointManager;
    m_defaultInputStream = new QTextStream(stdin);
    m_defaultOutputStream = new QTextStream(stdout);
    m_defaultErrorStream = new QTextStream(stderr);
    m_inputStream = m_defaultInputStream;
    m_outputStream = m_defaultOutputStream;
    m_errorStream = m_defaultErrorStream;
}

ScriptDebuggerPrivate::~ScriptDebuggerPrivate()
{
    delete m_defaultInputStream;
    delete m_defaultOutputStream;
    delete m_defaultErrorStream;
    delete m_bpManager;
    qDeleteAll(m_scripts);
}

QString ScriptDebuggerPrivate::readLine()
{
    return m_inputStream->readLine();
}

void ScriptDebuggerPrivate::output(const QString &text)
{
    *m_outputStream << text;
}

void ScriptDebuggerPrivate::message(const QString &text)
{
    *m_outputStream << text << endl;
    m_outputStream->flush();
}

void ScriptDebuggerPrivate::errorMessage(const QString &text)
{
    *m_errorStream << text << endl;
    m_errorStream->flush();
}

void ScriptDebuggerPrivate::setMode(Mode mode)
{
    m_mode = mode;
}

ScriptDebuggerPrivate::Mode ScriptDebuggerPrivate::mode() const
{
    return m_mode;
}

QScriptContext *ScriptDebuggerPrivate::frameContext(int index) const
{
    QScriptContext *ctx = engine()->currentContext();
    for (int i = 0; i < index; ++i) {
        ctx = ctx->parentContext();
        if (!ctx)
            break;
    }
    return ctx;
}

int ScriptDebuggerPrivate::currentFrameIndex() const
{
    return m_currentFrameIndex;
}

void ScriptDebuggerPrivate::setCurrentFrameIndex(int index)
{
    m_currentFrameIndex = index;
    m_listLineNumber = -1;
}

int ScriptDebuggerPrivate::listLineNumber() const
{
    return m_listLineNumber;
}

void ScriptDebuggerPrivate::setListLineNumber(int lineNumber)
{
    m_listLineNumber = lineNumber;
}

QScriptContext *ScriptDebuggerPrivate::currentFrameContext() const
{
    return frameContext(currentFrameIndex());
}

int ScriptDebuggerPrivate::frameCount() const
{
    int count = 0;
    QScriptContext *ctx = engine()->currentContext();
    while (ctx) {
        ++count;
        ctx = ctx->parentContext();
    }
    return count;
}

ScriptInfo *ScriptDebuggerPrivate::scriptInfo(QScriptContext *context) const
{
    QStack<qint64> pids = m_contextProgramIds.value(context);
    if (pids.isEmpty())
        return 0;
    return m_scripts.value(pids.top());
}

void ScriptDebuggerPrivate::interactive()
{
    setCurrentFrameIndex(0);

    QString qsdbgPrompt = QString::fromLatin1("(qsdbg) ");
    QString dotPrompt = QString::fromLatin1(".... ");
    QString prompt = qsdbgPrompt;

    QString code;

    forever {

         *m_outputStream << prompt;
        m_outputStream->flush();

        QString line = readLine();

        if (code.isEmpty() && (line.isEmpty() || line.startsWith(m_commandPrefix))) {
            if (line.isEmpty())
                line = m_lastInteractiveCommand;
            else
                m_lastInteractiveCommand = line;

            QStringList parts = line.split(QLatin1Char(' '), Qt::SkipEmptyParts);
            if (!parts.isEmpty()) {
                QString command = parts.takeFirst().mid(1);
                if (executeCommand(command, parts))
                    break;
            }

        } else {
            if (line.isEmpty())
                continue;

            code += line;
            code += QLatin1Char('\n');

            if (line.trimmed().isEmpty()) {
                continue;

            } else if (! engine()->canEvaluate(code)) {
                prompt = dotPrompt;

            } else {
                setMode(Run);
                QScriptValue result = engine()->evaluate(code, QLatin1String("typein"));

                code.clear();
                prompt = qsdbgPrompt;

                if (! result.isUndefined()) {
                    errorMessage(result.toString());
                    engine()->clearExceptions();
                }
            }
        }
    }
}

bool ScriptDebuggerPrivate::executeCommand(const QString &command, const QStringList &args)
{
    if (command == QLatin1String("c")
        || command == QLatin1String("continue")) {
        setMode(Run);
        return true;
    } else if (command == QLatin1String("s")
               || command == QLatin1String("step")) {
        setMode(StepInto);
        return true;
    } else if (command == QLatin1String("n")
               || command == QLatin1String("next")) {
        setMode(StepOver);
        m_stepDepth = 0;
        return true;
    } else if (command == QLatin1String("f")
               || command == QLatin1String("frame")) {
        bool ok = false;
        int index = args.value(0).toInt(&ok);
        if (ok) {
            if (index < 0 || index >= frameCount()) {
                errorMessage("No such frame.");
            } else {
                setCurrentFrameIndex(index);
                QScriptContext *ctx = currentFrameContext();
                message(QString::fromLatin1("#%0  %1").arg(index).arg(ctx->toString()));
            }
        }
    } else if (command == QLatin1String("bt")
               || command == QLatin1String("backtrace")) {
        QScriptContext *ctx = engine()->currentContext();
        int index = -1;
        while (ctx) {
            ++index;
            QString line = ctx->toString();
            message(QString::fromLatin1("#%0  %1").arg(index).arg(line));
            ctx = ctx->parentContext();
        }
    } else if (command == QLatin1String("up")) {
        int index = currentFrameIndex() + 1;
        if (index == frameCount()) {
            errorMessage(QString::fromLatin1("Initial frame selected; you cannot go up."));
        } else {
            setCurrentFrameIndex(index);
            QScriptContext *ctx = currentFrameContext();
            message(QString::fromLatin1("#%0  %1").arg(index).arg(ctx->toString()));
        }
    } else if (command == QLatin1String("down")) {
        int index = currentFrameIndex() - 1;
        if (index < 0) {
            errorMessage(QString::fromLatin1("Bottom (innermost) frame selected; you cannot go down."));
        } else {
            setCurrentFrameIndex(index);
            QScriptContext *ctx = currentFrameContext();
            message(QString::fromLatin1("#%0  %1").arg(index).arg(ctx->toString()));
        }
    } else if (command == QLatin1String("b")
               || command == QLatin1String("break")) {
        QString str = args.value(0);
        int colonIndex = str.indexOf(QLatin1Char(':'));
        if (colonIndex != -1) {
            // filename:line form
            QString fileName = str.left(colonIndex);
            int lineNumber = str.mid(colonIndex+1).toInt();
            int id = m_bpManager->setBreakpoint(fileName, lineNumber);
            message(QString::fromLatin1("Breakpoint %0 at %1, line %2.").arg(id+1).arg(fileName).arg(lineNumber));
        } else {
            // function
            QScriptValue fun = engine()->globalObject().property(str);
            if (fun.isFunction()) {
                int id = m_bpManager->setBreakpoint(fun);
                message(QString::fromLatin1("Breakpoint %0 at %1().").arg(id+1).arg(str));
            }
        }
    } else if (command == QLatin1String("d")
               || command == QLatin1String("delete")) {
        int id = args.value(0).toInt() - 1;
        m_bpManager->removeBreakpoint(id);
    } else if (command == QLatin1String("disable")) {
        int id = args.value(0).toInt() - 1;
        m_bpManager->setBreakpointEnabled(id, false);
    } else if (command == QLatin1String("enable")) {
        int id = args.value(0).toInt() - 1;
        m_bpManager->setBreakpointEnabled(id, true);
    } else if (command == QLatin1String("list")) {
        QScriptContext *ctx = currentFrameContext();
        ScriptInfo *progInfo = scriptInfo(ctx);
        if (!progInfo) {
            errorMessage("No source text available for this frame.");
        } else {
            QScriptContextInfo ctxInfo(ctx);
            bool ok;
            int line = args.value(0).toInt(&ok);
            if (ok) {
                line = qMax(1, line - 5);
            } else {
                line = listLineNumber();
                if (line == -1)
                    line = qMax(progInfo->lineNumber(), ctxInfo.lineNumber() - 5);
            }
            for (int i = line; i < line + 10; ++i) {
                message(QString::fromLatin1("%0\t%1").arg(i).arg(progInfo->lineText(i)));
            }
            setListLineNumber(line + 10);
        }
    } else if (command == QLatin1String("info")) {
        if (args.size() < 1) {
        } else {
            QString what = args.value(0);
            if (what == QLatin1String("locals")) {
                QScriptValueIterator it(currentFrameContext()->activationObject());
                while (it.hasNext()) {
                    it.next();
                    QString line;
                    line.append(it.name());
                    line.append(QLatin1String(" = "));
                    line.append(safeValueToString(it.value()));
                    message(line);
                }
            }
        }
    } else if (command == QLatin1String("help")) {
        message("continue - continue execution\n"
                "step     - step into statement\n"
                "next     - step over statement\n"
                "list     - show where you are\n"
                "\n"
                "break    - set breakpoint\n"
                "delete   - remove breakpoint\n"
                "disable  - disable breakpoint\n"
                "enable   - enable breakpoint\n"
                "\n"
                "backtrace - show backtrace\n"
                "up       - one frame up\n"
                "down     - one frame down\n"
                "frame    - set frame\n"
                "\n"
                "info locals - show local variables");
    } else {
        errorMessage(QString::fromLatin1("Undefined command \"%0\". Try \"help\".")
                     .arg(command));
    }

    return false;
}


// QScriptEngineAgent interface

void ScriptDebuggerPrivate::scriptLoad(qint64 id, const QString &program,
                                       const QString &fileName, int lineNumber)
{
    ScriptInfo *info = new ScriptInfo(program, fileName, lineNumber);
    m_scripts.insert(id, info);
}

void ScriptDebuggerPrivate::scriptUnload(qint64 id)
{
    ScriptInfo *info = m_scripts.take(id);
    delete info;
}

void ScriptDebuggerPrivate::functionEntry(qint64 scriptId)
{
    if (scriptId != -1) {
        QScriptContext *ctx = engine()->currentContext();
        QStack<qint64> ids = m_contextProgramIds.value(ctx);
        ids.push(scriptId);
        m_contextProgramIds.insert(ctx, ids);
    }

    if (mode() == StepOver)
        ++m_stepDepth;
}

void ScriptDebuggerPrivate::functionExit(qint64 scriptId,
                                         const QScriptValue &/*returnValue*/)
{
    if (scriptId != -1) {
        QScriptContext *ctx = engine()->currentContext();
        QStack<qint64> ids = m_contextProgramIds.value(ctx);
        Q_ASSERT(!ids.isEmpty());
        Q_ASSERT(ids.top() == scriptId);
        ids.pop();
        m_contextProgramIds.insert(ctx, ids);
    }

    if (mode() == StepOver)
        --m_stepDepth;
}

void ScriptDebuggerPrivate::positionChange(qint64 scriptId,
                                           int lineNumber, int /*columnNumber*/)
{
    ScriptInfo *info = 0;
    bool enterInteractiveMode = false;

    if (m_bpManager->hasBreakpoints()) {
        // check if we hit a breakpoint
        info = m_scripts.value(scriptId);
        QScriptContext *ctx = engine()->currentContext();
        QScriptContextInfo ctxInfo(ctx);
        QScriptValue callee = ctx->callee();

        // try fileName:lineNumber
        int bpid = m_bpManager->findBreakpoint(info->fileName(), lineNumber);
        if ((bpid != -1) && m_bpManager->isBreakpointEnabled(bpid)) {
            message(QString::fromLatin1("Breakpoint %0 at %1:%2")
                    .arg(bpid + 1).arg(info->fileName()).arg(lineNumber));
            if (m_bpManager->isBreakpointSingleShot(bpid))
                m_bpManager->removeBreakpoint(bpid);
        }
        if (bpid == -1) {
            // try function
            bpid = m_bpManager->findBreakpoint(callee);
            if ((bpid != -1) && m_bpManager->isBreakpointEnabled(bpid)) {
                message(QString::fromLatin1("Breakpoint %0, %1()")
                        .arg(bpid + 1).arg(ctxInfo.functionName()));
                if (m_bpManager->isBreakpointSingleShot(bpid))
                    m_bpManager->removeBreakpoint(bpid);
            }
        }
        if ((bpid == -1) && !ctxInfo.functionName().isEmpty()) {
            // try functionName:fileName
            bpid = m_bpManager->findBreakpoint(ctxInfo.functionName(), ctxInfo.fileName());
            if ((bpid != -1) && m_bpManager->isBreakpointEnabled(bpid)) {
                message(QString::fromLatin1("Breakpoint %0, %1():%2").arg(bpid + 1)
                        .arg(ctxInfo.functionName()).arg(ctxInfo.fileName()));
                if (m_bpManager->isBreakpointSingleShot(bpid))
                    m_bpManager->removeBreakpoint(bpid);
            }
        }

        enterInteractiveMode = (bpid != -1);
    }

    switch (mode()) {
    case Run:
        break;

    case StepInto:
        enterInteractiveMode = true;
        break;

    case StepOver:
        enterInteractiveMode = enterInteractiveMode || (m_stepDepth <= 0);
        break;
    }

    if (enterInteractiveMode) {
        if (!info)
            info = m_scripts.value(scriptId);
        Q_ASSERT(info);        
        message(QString::fromLatin1("%0\t%1").arg(lineNumber).arg(info->lineText(lineNumber)));
        interactive();
    }
}

void ScriptDebuggerPrivate::exceptionThrow(qint64 /*scriptId*/,
                                           const QScriptValue &exception,
                                           bool hasHandler)
{
    if (!hasHandler) {
        errorMessage(QString::fromLatin1("uncaught exception: %0").arg(exception.toString()));
        QScriptContext *ctx = engine()->currentContext();
        int lineNumber = QScriptContextInfo(ctx).lineNumber();
        ScriptInfo *info = scriptInfo(ctx);
        QString lineText = info ? info->lineText(lineNumber) : QString("(no source text available)");
        message(QString::fromLatin1("%0\t%1").arg(lineNumber).arg(lineText));
        interactive();
    }
}



ScriptDebugger::ScriptDebugger(QScriptEngine *engine)
    : d_ptr(new ScriptDebuggerPrivate(engine))
{
    d_ptr->q_ptr = this;
    engine->setAgent(d_ptr);
}

ScriptDebugger::ScriptDebugger(QScriptEngine *engine, ScriptDebuggerPrivate &dd)
    : d_ptr(&dd)
{
    d_ptr->q_ptr = this;
    engine->setAgent(d_ptr);
}

ScriptDebugger::~ScriptDebugger()
{
    delete d_ptr;
    d_ptr = 0;
}

void ScriptDebugger::breakAtNextStatement()
{
    Q_D(ScriptDebugger);
    d->setMode(ScriptDebuggerPrivate::StepInto);
}

void ScriptDebugger::setBreakpoint(const QString &fileName, int lineNumber)
{
    Q_D(ScriptDebugger);
    d->m_bpManager->setBreakpoint(fileName, lineNumber);
}

void ScriptDebugger::setBreakpoint(const QString &functionName, const QString &fileName)
{
    Q_D(ScriptDebugger);
    d->m_bpManager->setBreakpoint(functionName, fileName);
}

void ScriptDebugger::setBreakpoint(const QScriptValue &function)
{
    Q_D(ScriptDebugger);
    d->m_bpManager->setBreakpoint(function);
}

QTextStream *ScriptDebugger::inputStream() const
{
    Q_D(const ScriptDebugger);
    return d->m_inputStream;
}

void ScriptDebugger::setInputStream(QTextStream *inputStream)
{
    Q_D(ScriptDebugger);
    d->m_inputStream = inputStream;
}

QTextStream *ScriptDebugger::outputStream() const
{
    Q_D(const ScriptDebugger);
    return d->m_outputStream;
}

void ScriptDebugger::setOutputStream(QTextStream *outputStream)
{
    Q_D(ScriptDebugger);
    d->m_outputStream = outputStream;
}

QTextStream *ScriptDebugger::errorStream() const
{
    Q_D(const ScriptDebugger);
    return d->m_errorStream;
}

void ScriptDebugger::setErrorStream(QTextStream *errorStream)
{
    Q_D(ScriptDebugger);
    d->m_errorStream = errorStream;
}
