/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** Commercial Usage
**
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
** GNU Lesser General Public License Usage
**
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at http://qt.nokia.com/contact.
**
**************************************************************************/

#include "cdbbreakpoint.h"
#include "cdbmodules.h"
#include "breakhandler.h"
#include "cdbdebugengine_p.h"

#include <QtCore/QTextStream>
#include <QtCore/QDir>
#include <QtCore/QDebug>
#include <QtCore/QMap>

#include <psapi.h>

enum { debugBP = 0 };

namespace Debugger {
namespace Internal {

// The CDB breakpoint expression syntax is:
//    `foo.cpp:523`[ "condition"]
//    module!function[ "condition"]

static const char sourceFileQuoteC = '`';

CDBBreakPoint::CDBBreakPoint() :
    ignoreCount(0),
    lineNumber(-1),
    oneShot(false),
    enabled(true)
{
}

CDBBreakPoint::CDBBreakPoint(const BreakpointData &bpd) :
     fileName(QDir::toNativeSeparators(bpd.fileName)),
     condition(bpd.condition),
     ignoreCount(0),
     funcName(bpd.funcName),
     lineNumber(-1),
     oneShot(false),
     enabled(bpd.enabled)
{
    if (!bpd.ignoreCount.isEmpty())
        ignoreCount = bpd.ignoreCount.toInt();
    if (!bpd.lineNumber.isEmpty())
        lineNumber = bpd.lineNumber.toInt();
}

int CDBBreakPoint::compare(const CDBBreakPoint& rhs) const
{
    if (ignoreCount > rhs.ignoreCount)
        return 1;
    if (ignoreCount < rhs.ignoreCount)
        return -1;
    if (lineNumber > rhs.lineNumber)
        return 1;
    if (lineNumber < rhs.lineNumber)
        return -1;
    if (oneShot && !rhs.oneShot)
        return 1;
    if (!oneShot && rhs.oneShot)
        return -1;
    if (enabled && !rhs.enabled)
        return 1;
    if (!enabled && rhs.enabled)
        return -1;
    if (const int fileCmp = fileName.compare(rhs.fileName))
        return fileCmp;
    if (const int  funcCmp = funcName.compare(rhs.funcName))
        return funcCmp;
    if (const int condCmp = condition.compare(rhs.condition))
        return condCmp;
    return 0;
}

void CDBBreakPoint::clear()
{
     ignoreCount = 0;
     oneShot = false;
     enabled = true;
     clearExpressionData();     
}

void CDBBreakPoint::clearExpressionData()
{
    fileName.clear();
    condition.clear();
    funcName.clear();
    lineNumber = -1;
}

QDebug operator<<(QDebug dbg, const CDBBreakPoint &bp)
{
    QDebug nsp = dbg.nospace();
    if (!bp.fileName.isEmpty()) {
        nsp << "fileName='" << bp.fileName << ':' << bp.lineNumber << '\'';
    } else {
        nsp << "funcName='" << bp.funcName << '\'';
    }
    if (!bp.condition.isEmpty())
        nsp << " condition='" << bp.condition << '\'';
    if (bp.ignoreCount)
        nsp << " ignoreCount=" << bp.ignoreCount;
    if (bp.enabled)
        nsp << " enabled";
    if (bp.oneShot)
        nsp << " oneShot";
    return dbg;
}

QString CDBBreakPoint::expression() const
{
    // format the breakpoint expression (file/function and condition)
    QString rc;
    QTextStream str(&rc);
    if (funcName.isEmpty()) {
        const QChar sourceFileQuote = QLatin1Char(sourceFileQuoteC);
        str << sourceFileQuote << QDir::toNativeSeparators(fileName) << QLatin1Char(':') << lineNumber << sourceFileQuote;
    } else {
        str << funcName;
    }
    if (!condition.isEmpty()) {
        const QChar doubleQuote = QLatin1Char('"');
        str << QLatin1Char(' ') << doubleQuote << condition << doubleQuote;
    }
    return rc;
}

bool CDBBreakPoint::apply(CIDebugBreakpoint *ibp, QString *errorMessage) const
{
    const QString expr = expression();
    if (debugCDB)
        qDebug() << Q_FUNC_INFO << *this << expr;
    const HRESULT hr = ibp->SetOffsetExpressionWide(reinterpret_cast<PCWSTR>(expr.utf16()));
    if (FAILED(hr)) {
        *errorMessage = QString::fromLatin1("Unable to set breakpoint '%1' : %2").
                        arg(expr, msgComFailed("SetOffsetExpressionWide", hr));
        return false;
    }
    // Pass Count is ignoreCount + 1
    ibp->SetPassCount(ignoreCount + 1u);
    ULONG flags = 0;
    if (enabled)
        flags |= DEBUG_BREAKPOINT_ENABLED;
    if (oneShot)
        flags |= DEBUG_BREAKPOINT_ONE_SHOT;
    ibp->AddFlags(flags);
    return true;
}

static inline QString msgCannotAddBreakPoint(const QString &why)
{
    return QString::fromLatin1("Unable to add breakpoint: %1").arg(why);
}

bool CDBBreakPoint::add(CIDebugControl* debugControl,
                        QString *errorMessage,
                        unsigned long *id,
                        quint64 *address) const
{
    IDebugBreakpoint2* ibp = 0;
    if (address)
        *address = 0;
    if (id)
        *id = 0;
    HRESULT hr = debugControl->AddBreakpoint2(DEBUG_BREAKPOINT_CODE, DEBUG_ANY_ID, &ibp);
    if (FAILED(hr)) {
        *errorMessage = msgCannotAddBreakPoint(msgComFailed("AddBreakpoint2", hr));
        return false;
    }
    if (!ibp) {
        *errorMessage = msgCannotAddBreakPoint(QLatin1String("<Unknown error>"));
        return false;
    }
    if (!apply(ibp, errorMessage))
        return false;
    // GetOffset can fail when attaching to remote processes, ignore return
    if (address) {
        hr = ibp->GetOffset(address);
        if (FAILED(hr))
            *address = 0;
    }
    if (id) {
        hr = ibp->GetId(id);
        if (FAILED(hr)) {
            *errorMessage = msgCannotAddBreakPoint(msgComFailed("GetId", hr));
            return false;
        }
    }
    return true;
}

// Helper for normalizing file names:
// Map the device paths in  a file name to back to drive letters
// "/Device/HarddiskVolume1/file.cpp" -> "C:/file.cpp"

static bool mapDeviceToDriveLetter(QString *s)
{
    enum { bufSize = 512 };
    // Retrieve drive letters and get their device names.
    // Do not cache as it may change due to removable/network drives.
    TCHAR driveLetters[bufSize];
    if (!GetLogicalDriveStrings(bufSize-1, driveLetters))
        return false;

    TCHAR driveName[MAX_PATH];
    TCHAR szDrive[3] = TEXT(" :");
    for (const TCHAR *driveLetter = driveLetters; *driveLetter; driveLetter++) {
        szDrive[0] = *driveLetter; // Look up each device name
        if (QueryDosDevice(szDrive, driveName, MAX_PATH)) {
            const QString deviceName = QString::fromUtf16(driveName);
            if (s->startsWith(deviceName)) {
                s->replace(0, deviceName.size(), QString::fromUtf16(szDrive));
                return true;
            }
        }
    }
    return false;
}

// Helper for normalizing file names:
// Determine normalized case of a Windows file name (camelcase.cpp -> CamelCase.cpp)
// as the debugger reports lower case file names.
// Restriction: File needs to exists and be non-empty and will be to be opened/mapped.
// This is the MSDN-recommended way of doing that. The result should be cached.

static inline QString normalizeFileNameCaseHelper(const QString &f)
{
    HANDLE hFile = CreateFile(f.utf16(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    if(hFile == INVALID_HANDLE_VALUE)
        return f;
    // Get the file size. We need a non-empty file to map it.
    DWORD dwFileSizeHi = 0;
    DWORD dwFileSizeLo = GetFileSize(hFile, &dwFileSizeHi);
    if (dwFileSizeLo == 0 && dwFileSizeHi == 0) {
        CloseHandle(hFile);
        return f;
    }
    // Create a file mapping object.
    HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 1, NULL);
    if (!hFileMap)  {
        CloseHandle(hFile);
        return f;
    }

    // Create a file mapping to get the file name.
    void* pMem = MapViewOfFile(hFileMap, FILE_MAP_READ, 0, 0, 1);
    if (!pMem) {
        CloseHandle(hFileMap);
        CloseHandle(hFile);
        return f;
    }

    QString rc;
    WCHAR pszFilename[MAX_PATH];
    pszFilename[0] = 0;
    // Get a file name of the form "/Device/HarddiskVolume1/file.cpp"
    if (GetMappedFileName (GetCurrentProcess(), pMem, pszFilename, MAX_PATH)) {
        rc = QString::fromUtf16(pszFilename);
        if (!mapDeviceToDriveLetter(&rc))
            rc.clear();
    }

    UnmapViewOfFile(pMem);
    CloseHandle(hFileMap);
    CloseHandle(hFile);
    return rc.isEmpty() ? f : rc;
}

// Make sure file can be found in editor manager and text markers
// Use '/', correct case and capitalize drive letter. Use a cache.

typedef QHash<QString, QString> NormalizedFileCache;
Q_GLOBAL_STATIC(NormalizedFileCache, normalizedFileNameCache)

QString CDBBreakPoint::normalizeFileName(const QString &f)
{
    QTC_ASSERT(!f.isEmpty(), return f)
    const NormalizedFileCache::const_iterator it = normalizedFileNameCache()->constFind(f);
    if (it != normalizedFileNameCache()->constEnd())
        return it.value();
    QString normalizedName = QDir::fromNativeSeparators(normalizeFileNameCaseHelper(f));
    // Upcase drive letter for consistency even if case mapping fails.
    if (normalizedName.size() > 2 && normalizedName.at(1) == QLatin1Char(':'))
        normalizedName[0] = normalizedName.at(0).toUpper();
    normalizedFileNameCache()->insert(f, normalizedName);
    return f;
}

void CDBBreakPoint::clearNormalizeFileNameCache()
{
    normalizedFileNameCache()->clear();
}

bool CDBBreakPoint::retrieve(CIDebugBreakpoint *ibp, QString *errorMessage)
{
    clear();
    WCHAR wszBuf[MAX_PATH];
    const HRESULT hr =ibp->GetOffsetExpressionWide(wszBuf, MAX_PATH, 0);
    if (FAILED(hr)) {
        *errorMessage = QString::fromLatin1("Cannot retrieve breakpoint: %1").
                        arg(msgComFailed("GetOffsetExpressionWide", hr));
        return false;
    }
    // Pass Count is ignoreCount + 1
    ibp->GetPassCount(&ignoreCount);
    if (ignoreCount)
        ignoreCount--;
    ULONG flags = 0;
    ibp->GetFlags(&flags);
    oneShot = (flags & DEBUG_BREAKPOINT_ONE_SHOT);
    enabled = (flags & DEBUG_BREAKPOINT_ENABLED);
    const QString expr = QString::fromUtf16(reinterpret_cast<const ushort *>(wszBuf));
    if (!parseExpression(expr)) {
        *errorMessage = QString::fromLatin1("Parsing of '%1' failed.").arg(expr);
        return false;
    }
    return true;
}

bool CDBBreakPoint::parseExpression(const QString &expr)
{
    clearExpressionData();
    const QChar sourceFileQuote = QLatin1Char(sourceFileQuoteC);
    // Check for file or function
    int conditionPos = 0;
    if (expr.startsWith(sourceFileQuote)) { // `c:\foo.cpp:523`[ "condition"]
        // Do not fall for the drive letter colon here
        const int colonPos = expr.indexOf(QLatin1Char(':'), 3);
        if (colonPos == -1)
            return false;
        conditionPos = expr.indexOf(sourceFileQuote, colonPos + 1);
        if (conditionPos == -1)
            return false;
        fileName = normalizeFileName(expr.mid(1, colonPos - 1));
        const QString lineNumberS = expr.mid(colonPos + 1, conditionPos - colonPos - 1);
        bool lineNumberOk = false;
        lineNumber = lineNumberS.toInt(&lineNumberOk);
        if (!lineNumberOk)
            return false;
        conditionPos++;
    } else {
        // Check function token
        conditionPos = expr.indexOf(QLatin1Char(' '));
        if (conditionPos != -1) {
            funcName = expr.mid(0, conditionPos);
            conditionPos++;
        } else {
            funcName = expr;
            conditionPos = expr.size();
        }
    }
    // Condition? ".if bla"
    if (conditionPos >= expr.size())
        return true;
    const QChar doubleQuote = QLatin1Char('"');
    conditionPos = expr.indexOf(doubleQuote, conditionPos);
    if (conditionPos == -1)
        return true;
    conditionPos++;
    const int condEndPos = expr.lastIndexOf(doubleQuote);
    if (condEndPos == -1)
        return false;
    condition = expr.mid(conditionPos, condEndPos - conditionPos);
    return true;
}

bool CDBBreakPoint::getBreakPointCount(CIDebugControl* debugControl, ULONG *count, QString *errorMessage /* = 0*/)
{
    const HRESULT hr = debugControl->GetNumberBreakpoints(count);
    if (FAILED(hr)) {
        if (errorMessage)
            *errorMessage = QString::fromLatin1("Cannot determine breakpoint count: %1").
                            arg(msgComFailed("GetNumberBreakpoints", hr));
        return false;
    }
    return true;
}

bool CDBBreakPoint::getBreakPoints(CIDebugControl* debugControl, QList<CDBBreakPoint> *bps, QString *errorMessage)
{
    ULONG count = 0;
    bps->clear();
    if (!getBreakPointCount(debugControl, &count, errorMessage))
        return false;
    // retrieve one by one and parse
    for (ULONG b= 0; b < count; b++) {
        IDebugBreakpoint2 *ibp = 0;
        const HRESULT hr = debugControl->GetBreakpointByIndex2(b, &ibp);
        if (FAILED(hr)) {
            *errorMessage = QString::fromLatin1("Cannot retrieve breakpoint %1: %2").
                            arg(b).arg(msgComFailed("GetBreakpointByIndex2", hr));
            return false;
        }
        CDBBreakPoint bp;
        if (!bp.retrieve(ibp, errorMessage))
            return false;
        bps->push_back(bp);
    }
    return true;
}

// Find a breakpoint by id
static inline QString msgNoBreakPointWithId(unsigned long id, const QString &why)
{
    return QString::fromLatin1("Unable to find breakpoint with id %1: %2").arg(id).arg(why);
}

static IDebugBreakpoint2 *breakPointById(CIDebugControl *ctl, unsigned long id, QString *errorMessage)
{
    CIDebugBreakpoint *ibp = 0;
    const HRESULT hr = ctl->GetBreakpointById2(id, &ibp);
    if (FAILED(hr)) {
        *errorMessage = msgNoBreakPointWithId(id, msgComFailed("GetBreakpointById2", hr));
        return 0;
    }
    if (!ibp) {
        *errorMessage = msgNoBreakPointWithId(id, QLatin1String("<not found>"));
        return 0;
    }
    return ibp;
}

// Remove breakpoint by id
static bool removeBreakPointById(CIDebugControl *ctl, unsigned long id, QString *errorMessage)
{
    if (debugBP)
        qDebug() << Q_FUNC_INFO << id;
    CIDebugBreakpoint *ibp = breakPointById(ctl, id, errorMessage);
    if (!ibp)
        return false;
    const HRESULT hr = ctl->RemoveBreakpoint2(ibp);
    if (FAILED(hr)) {
        *errorMessage = QString::fromLatin1("Cannot remove breakpoint %1: %2").arg(id).arg(msgComFailed("RemoveBreakpointById2", hr));
        return false;
    }
    return true;
}

// Set enabled by id

// Change enabled state of a breakpoint by id
static inline QString msgCannotSetBreakPointEnabled(unsigned long id, bool enabled, const QString &why)
{
    return QString::fromLatin1("Cannot %1 breakpoint %2: %3").
            arg(QLatin1String(enabled ? "enable" : "disable")).arg(id).arg(why);
}

static bool setBreakPointEnabledById(CIDebugControl *ctl, unsigned long id, bool enabled, QString *errorMessage)
{
    if (debugBP)
        qDebug() << Q_FUNC_INFO << id << enabled;
    CIDebugBreakpoint *ibp = breakPointById(ctl, id, errorMessage);
    if (!ibp) {
        *errorMessage = msgCannotSetBreakPointEnabled(id, enabled, *errorMessage);
        return false;
    }
    // Compare flags
    ULONG flags;
    HRESULT hr = ibp->GetFlags(&flags);
    if (FAILED(hr)) {
        *errorMessage = msgCannotSetBreakPointEnabled(id, enabled, msgComFailed("GetFlags", hr));
        return false;
    }
    const bool wasEnabled = (flags & DEBUG_BREAKPOINT_ENABLED);
    if (wasEnabled == enabled)
        return true;
    // Set new value
    if (enabled) {
        flags |= DEBUG_BREAKPOINT_ENABLED;
    } else {
        flags &= ~DEBUG_BREAKPOINT_ENABLED;
    }
    hr = ibp->SetFlags(flags);
    if (FAILED(hr)) {
        *errorMessage = msgCannotSetBreakPointEnabled(id, enabled, msgComFailed("SetFlags", hr));
        return false;
    }
    return true;
}

static inline QString msgCannotSetBreakAtFunction(const QString &func, const QString &why)
{
    return QString::fromLatin1("Cannot set a breakpoint at '%1': %2").arg(func, why);
}

// Synchronize (halted) engine breakpoints with those of the BreakHandler.
bool CDBBreakPoint::synchronizeBreakPoints(CIDebugControl* debugControl,
                                           CIDebugSymbols *syms,
                                           BreakHandler *handler,
                                           QString *errorMessage, QStringList *warnings)
{    
    errorMessage->clear();
    warnings->clear();
    // Do an initial check whether we are in a state that allows
    // for modifying  breakPoints
    ULONG engineCount;
    if (!getBreakPointCount(debugControl, &engineCount, errorMessage)) {
        *errorMessage = QString::fromLatin1("Cannot modify breakpoints: %1").arg(*errorMessage);
        return false;
    }
    QString warning;
    // Insert new ones
    bool updateMarkers = false;
    foreach (BreakpointData *nbd, handler->insertedBreakpoints()) {
        warning.clear();
        // Function breakpoints: Are the module names specified?
        bool breakPointOk = false;
        if (nbd->funcName.isEmpty()) {
            breakPointOk = true;
        } else {
            switch (resolveSymbol(syms, &nbd->funcName, &warning)) {
            case ResolveSymbolOk:
                breakPointOk = true;
                break;
            case ResolveSymbolAmbiguous:
                warnings->push_back(msgCannotSetBreakAtFunction(nbd->funcName, warning));
                warning.clear();
                breakPointOk = true;
                break;
            case ResolveSymbolNotFound:
            case ResolveSymbolError:
                warnings->push_back(msgCannotSetBreakAtFunction(nbd->funcName, warning));
                warning.clear();
                break;
            };
        } // function breakpoint
        // Now add...
        if (breakPointOk) {
            quint64 address;
            unsigned long id;
            CDBBreakPoint ncdbbp(*nbd);
            breakPointOk = ncdbbp.add(debugControl, &warning, &id, &address);
            if (breakPointOk) {
                if (debugBP)
                    qDebug() << "Added " << id << " at " << address << ncdbbp;
                handler->takeInsertedBreakPoint(nbd);
                updateMarkers = true;
                nbd->pending = false;
                nbd->bpNumber = QString::number(id);
                nbd->bpAddress = QLatin1String("0x") + QString::number(address, 16);
                // Take over rest as is
                nbd->bpCondition = nbd->condition;
                nbd->bpIgnoreCount = nbd->ignoreCount;
                nbd->bpFileName = nbd->fileName;
                nbd->bpLineNumber = nbd->lineNumber;
                nbd->bpFuncName = nbd->funcName;
            }
        } // had symbol
        if (!breakPointOk && !warning.isEmpty())
            warnings->push_back(warning);    }
    // Delete
    foreach (BreakpointData *rbd, handler->takeRemovedBreakpoints()) {
        if (!removeBreakPointById(debugControl, rbd->bpNumber.toUInt(), &warning))
            warnings->push_back(warning);
        delete rbd;
    }
    // Enable/Disable
    foreach (BreakpointData *ebd, handler->takeEnabledBreakpoints())
        if (!setBreakPointEnabledById(debugControl, ebd->bpNumber.toUInt(), true, &warning))
            warnings->push_back(warning);
    foreach (BreakpointData *dbd, handler->takeDisabledBreakpoints())
        if (!setBreakPointEnabledById(debugControl, dbd->bpNumber.toUInt(), false, &warning))
            warnings->push_back(warning);

    if (updateMarkers)
        handler->updateMarkers();

    if (debugBP > 1) {
        QList<CDBBreakPoint> bps;
        CDBBreakPoint::getBreakPoints(debugControl, &bps, errorMessage);
        qDebug().nospace() << "### Breakpoints in engine: " << bps;
    }
    return true;
}

} // namespace Internal
} // namespace Debugger
