/*
    Copyright (C) 2009  Arno Rehn <arno@arnorehn.de>

    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.
*/

#include "generatorpreprocessor.h"
#include "generatorenvironment.h"

#include <rpp/pp-environment.h>
#include <rpp/pp-macro.h>

#include <QCoreApplication>

#include <QtDebug>

QList<QString> parsedHeaders;

Preprocessor::Preprocessor(const QList<QDir>& includeDirs, const QStringList& defines, const QFileInfo& file)
    : m_includeDirs(includeDirs), m_defines(defines), m_file(file)
{
    pp = new rpp::pp(this);
    pp->setEnvironment(new GeneratorEnvironment(pp));
    if (file.exists())
        m_fileStack.push(file);
    
    m_topBlock = new rpp::MacroBlock(0);
    
    // some basic definitions
    rpp::pp_macro* exportMacro = new rpp::pp_macro;
    exportMacro->name = IndexedString("__cplusplus");
    exportMacro->definition.append(IndexedString('1'));
    exportMacro->function_like = false;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);

    exportMacro = new rpp::pp_macro;
    exportMacro->name = IndexedString("__GNUC__");
    exportMacro->definition.append(IndexedString('4'));
    exportMacro->function_like = false;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);

    exportMacro = new rpp::pp_macro;
    exportMacro->name = IndexedString("__GNUC_MINOR__");
    exportMacro->definition.append(IndexedString('1'));
    exportMacro->function_like = false;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);

    exportMacro = new rpp::pp_macro;
#if defined(Q_OS_LINUX)
    exportMacro->name = IndexedString("__linux__");
#elif defined(Q_OS_WIN32)
    exportMacro->name = IndexedString("WIN32");
#elif defined(Q_OS_WIN64)
    exportMacro->name = IndexedString("WIN64");
#elif defined(Q_OS_DARWIN)
    exportMacro->name = IndexedString("__APPLE__");
#elif defined(Q_OS_SOLARIS)
    exportMacro->name = IndexedString("__sun");
#else
    // fall back to linux if nothing matches
    exportMacro->name = IndexedString("__linux__");
#endif
    exportMacro->function_like = false;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);

#if defined(Q_OS_DARWIN64)
    exportMacro = new rpp::pp_macro;
    exportMacro->name = IndexedString("__LP64__");
    exportMacro->function_like = false;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);
#endif

#if (defined(QT_ARCH_ARM) || defined (QT_ARCH_ARMV6)) && !defined(QT_NO_ARM_EABI)
    exportMacro = new rpp::pp_macro;
    exportMacro->name = IndexedString("__ARM_EABI__");
    exportMacro->function_like = false;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);
#endif

    // ansidecl.h will define macros for keywords if we don't define __STDC__
    exportMacro = new rpp::pp_macro;
    exportMacro->name = IndexedString("__STDC__");
    exportMacro->function_like = false;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);

    // the following macros are gcc specialties
    exportMacro = new rpp::pp_macro;
    exportMacro->name = IndexedString("__extension__");
    exportMacro->function_like = false;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);

    exportMacro = new rpp::pp_macro;
    exportMacro->name = IndexedString("__restrict");
    exportMacro->function_like = false;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);

    exportMacro = new rpp::pp_macro;
    exportMacro->name = IndexedString("__const");
    exportMacro->definition.append(IndexedString("const"));
    exportMacro->function_like = false;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);

    exportMacro = new rpp::pp_macro;
    exportMacro->name = IndexedString("__attribute__");
    exportMacro->function_like = true;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);

    exportMacro = new rpp::pp_macro;
    exportMacro->name = IndexedString("__SMOKEGEN_RUN__");
    exportMacro->definition.append(IndexedString('1'));
    exportMacro->function_like = false;
    exportMacro->variadics = false;
    m_topBlock->setMacro(exportMacro);

    foreach (QString define, defines) {
        exportMacro = new rpp::pp_macro;
        exportMacro->name = IndexedString(define);
        exportMacro->function_like = false;
        exportMacro->variadics = false;
        m_topBlock->setMacro(exportMacro);
    }
    
    pp->environment()->visitBlock(m_topBlock);
}


Preprocessor::~Preprocessor()
{
    delete pp;
}

void Preprocessor::setFile(const QFileInfo& file)
{
    m_fileStack.clear();
    m_fileStack.push(file);
    m_file = file;
}

QFileInfo Preprocessor::file()
{
    return m_file;
}


void Preprocessor::setIncludeDirs(QList< QDir > dirs)
{
    m_includeDirs = dirs;
}


QList< QDir > Preprocessor::includeDirs()
{
    return m_includeDirs;
}


void Preprocessor::setDefines(QStringList defines)
{
    m_defines = defines;
}


QStringList Preprocessor::defines()
{
    return m_defines;
}


PreprocessedContents Preprocessor::lastContents()
{
    return m_contents;
}


PreprocessedContents Preprocessor::preprocess()
{
    m_contents = pp->processFile(m_file.absoluteFilePath());
    return m_contents;
}

rpp::Stream* Preprocessor::sourceNeeded(QString& fileName, rpp::Preprocessor::IncludeType type, int sourceLine, bool skipCurrentPath)
{
#ifdef Q_WS_MAC
    static QRegExp frameworkExpr("([^/]+)/(.*)");
#endif

    if (m_fileStack.top().fileName() == fileName && type == rpp::Preprocessor::IncludeGlobal) {
#ifdef DEBUG
        qDebug("prevented possible endless loop because of #include <%s>", qPrintable(fileName));
#endif
        return 0;
    }
    
    // are the contents already cached?
    if (type == rpp::Preprocessor::IncludeGlobal && m_cache.contains(fileName)) { 
        QPair<QFileInfo, PreprocessedContents>& cached = m_cache[fileName];
        m_fileStack.push(cached.first);
        return new HeaderStream(&cached.second, &m_fileStack);
    }
    
    QString path;
    QFileInfo info(fileName);

    // smokegen chokes on gcc's string.h, so use our simplified version here
    if (type == rpp::Preprocessor::IncludeGlobal && fileName == "string.h") {
        static QString customStringHPath = qApp->applicationDirPath() + "/../share/smokegen/string.h";
        path = customStringHPath;
    } else if (info.isAbsolute()) {
        path = fileName;
    } else if (type == rpp::Preprocessor::IncludeLocal) {
        info.setFile(m_fileStack.last().dir(), fileName);
        if (info.isFile())
            path = info.absoluteFilePath();
    }
    if (path.isEmpty()) {
#ifdef Q_WS_MAC
        QString framework;
        QString header;
        if (frameworkExpr.exactMatch(fileName)) {
            framework = frameworkExpr.cap(1);
            header = frameworkExpr.cap(2);
        }
#endif

        Q_FOREACH (const QDir& dir, m_includeDirs) {
            info.setFile(dir, fileName);
            if (info.isFile()) {
                path = info.absoluteFilePath();
                break;
            }

#ifdef Q_WS_MAC
            QDir parentDir = dir;
            parentDir.cdUp();
            if (parentDir.dirName() == framework + ".framework" && dir.dirName() == "Headers") {
                info.setFile(dir, header);
                if (info.isFile()) {
                    path = info.absoluteFilePath();
                    break;
                }
            }
#endif
        }
    }
    
    if (path.isEmpty()) {
#ifdef DEBUG
        qDebug("PP: File not found: %s", qPrintable(fileName));
#endif
        return 0;
    }
    
    QFile file(path);
    file.open(QFile::ReadOnly);
    QByteArray array = file.readAll();
    file.close();
    
    parsedHeaders << path;
    
    if (type == rpp::Preprocessor::IncludeGlobal) {
        // cache the identifier (fileName), the accompanying QFileInfo and the contents
        QHash<QString, QPair<QFileInfo, PreprocessedContents> >::iterator iter =
            m_cache.insert(fileName, qMakePair(QFileInfo(path), convertFromByteArray(array)));
        /* Push the QFileInfo of the included file on the file stack, so if sourceNeeded() is called for that file,
           we know where to search for headers included with a IncludeLocal. The HeaderStream will be destroyed when the
           file has been processed and it'll then pop() the stack */
        m_fileStack.push(iter.value().first);
        return new HeaderStream(&iter.value().second, &m_fileStack);
    } else {
        /* Don't cache locally included files - the meaning of a file name may change when recursing into
           other directories.
           We also don't need to push any files on the fileStack because the directory for local includes is
           not changing. */
        m_localContent.append(convertFromByteArray(array));
        m_fileStack.push(QFileInfo(path));
        return new HeaderStream(&m_localContent.last(), &m_fileStack);
    }
    return 0;
}

