/****************************************************************************
**
** Copyright (C) 2008-2010 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (developer.feedback@nokia.com)
**
** This file is part of the HbTools module of the UI Extensions for Mobile.
**
** GNU Lesser General Public License Usage
** 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.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights.  These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** If you have questions regarding the use of this file, please contact
** Nokia at developer.feedback@nokia.com.
**
****************************************************************************/

#include <hbiconsource_p.h>
#include <hbthemeindex_p.h>
#include <hbhash_p.h>

#include <assert.h>
#include <iostream>
#include <QApplication>
#include <QStringList>
#include <QTextStream>
#include <QFileInfo>
#include <QLibrary>
#include <QString>
#include <QFile>
#include <QMap>
#include <QDir>

#if !defined(QT_DLL) && !defined(QT_SHARED)
#include <QtPlugin>
Q_IMPORT_PLUGIN(qsvg)
#endif

#define RESOURCE_LIB_NAME "HbCore"
#define WIN32_DEBUG_SUFFIX "d"
#define MAC_DEBUG_SUFFIX "_debug"


// Global variables
static bool verboseOn = false;
static quint32 version = 1; // Current theme index format version

QList<HbThemeIndexItemData> IndexItems;
QMap<quint32, QString> AddedItems;

// Lists that hold icons to be automatically mirrored or icons that are locked
QStringList MirroredList;
QStringList LockedList;


// ------


void createMirroredList(const QString &fullThemePath)
{
    if (verboseOn) {
        std::cout << "Parsing mirrored list for theme " << fullThemePath.toStdString() << "\n";
    }
    // Find mirrored.txt file
    QString filename = fullThemePath + "/mirrored.txt";
    // Try to read file
    QFile file(filename);
    if (file.open(QIODevice::ReadOnly)) {
        QString line;

        while(!file.atEnd()) {        
            QByteArray dirtyLine = file.readLine();
            line = QString(dirtyLine).trimmed();
            // Skip empty lines and comment lines
            if (line.isEmpty() || line.at(0) == '#') {
                continue; 
            }
            MirroredList.append(line);
        }
    }
}

void createLockedList(const QString &fullThemePath)
{
    if (verboseOn) {
        std::cout << "Parsing locked list for theme " << fullThemePath.toStdString() << "\n";
    }
    // Find locked.txt file
    QString filename = fullThemePath + "/locked.txt";
    // Try to read file
    QFile file(filename);
    if (file.open(QIODevice::ReadOnly)) {
        QString line;

        while(!file.atEnd()) {        
            QByteArray dirtyLine = file.readLine();
            line = QString(dirtyLine).trimmed();
            // Skip empty lines and comment lines
            if (line.isEmpty() || line.at(0) == '#') {
                continue; 
            }
            LockedList.append(line);
        }
    }
}

void appendItem(HbThemeIndexItemData &itemData, const QString &itemName)
{
    bool alreadyExists = false;

    if (AddedItems.contains(itemData.itemNameHash)) {
        // If there already is item with same hash, it means either:
        // - same icon is in both scalable and pixmap folder and then we won't append it again
        // - hashing has failed, it generated same hash for 2 different strings -> TODO: do we care? We could e.g. save hash seed in index...
        if (AddedItems.value(itemData.itemNameHash) == itemName) {
            alreadyExists = true;
        } else {
            // Two different strings have same hash!!!
            std::cout << "ERROR: Two different strings (" << AddedItems.value(itemData.itemNameHash).toStdString() << ", " << itemName.toStdString() << ") have same hash\n";
            alreadyExists = true;
        }
    }

    if (!alreadyExists) {
        IndexItems.append(itemData);
        AddedItems.insert(itemData.itemNameHash, itemName);
    }
    
    if (verboseOn) {
        if (!alreadyExists) {
            std::cout << "----------------------------------------------------------------\n";
            std::cout << "Added item" << IndexItems.count() << "\n";
            std::cout << "Item name: " << itemName.toStdString() << " hash: " << itemData.itemNameHash << "\n";
            std::cout << "Item type: " << itemData.itemType << "\n";
            if (itemData.itemType == HbThemeIndexItemData::SvgItem ||
                itemData.itemType == HbThemeIndexItemData::PngItem ||
                itemData.itemType == HbThemeIndexItemData::MngItem ||
                itemData.itemType == HbThemeIndexItemData::GifItem ||
                itemData.itemType == HbThemeIndexItemData::XpmItem ||
                itemData.itemType == HbThemeIndexItemData::JpgItem ||
                itemData.itemType == HbThemeIndexItemData::NvgItem ||
                itemData.itemType == HbThemeIndexItemData::SvgzItem ||
                itemData.itemType == HbThemeIndexItemData::QpicItem) {

                std::cout << "Default size: width: " << itemData.defaultWidth << " height: " << itemData.defaultHeight << "\n";
            
                if (itemData.mirroredItemType != HbThemeIndexItemData::NotDefined) {
                    std::cout << "Mirrored default size: width:" << itemData.mirroredWidth << " height: " << itemData.mirroredHeight << "\n";
                }

                if (itemData.flags & HbThemeIndexItemData::Mirrorable) {
                    std::cout << "Icon is automatically mirrored\n";
                }
            } else if (itemData.itemType == HbThemeIndexItemData::ColorItem) {
                std::cout << "Color value: " << itemData.colorValue << "\n";
                if (itemData.flags & HbThemeIndexItemData::Reference) {
                    std::cout << "Item is reference\n";
                }
            }
            if (itemData.flags & HbThemeIndexItemData::Locked) {
                std::cout << "Item is locked\n";
            }

            std::cout << "----------------------------------------------------------------\n\n";
        } else { // Item already added in index with some other extension, do not add duplicates
            std::cout << "----------------------------------------------------------------\n";
            std::cout << "WARNING! Skipped already existing item:" << itemName.toStdString() << "\n";
        }
    }
}

HbThemeIndexItemData::Type getItemType(const QString &itemName)
{
    if (itemName.endsWith(".svg")) {
        return HbThemeIndexItemData::SvgItem;
    } else if (itemName.endsWith(".png")) {
        return HbThemeIndexItemData::PngItem;
    } else if (itemName.endsWith(".mng")) {
        return HbThemeIndexItemData::MngItem;
    } else if (itemName.endsWith(".gif")) {
        return HbThemeIndexItemData::GifItem;
    } else if (itemName.endsWith(".xpm")) {
        return HbThemeIndexItemData::XpmItem;
    } else if (itemName.endsWith(".jpg")) {
        return HbThemeIndexItemData::JpgItem;
    } else if (itemName.endsWith(".nvg")) {
        return HbThemeIndexItemData::NvgItem;
    } else if (itemName.endsWith(".svgz")) {
        return HbThemeIndexItemData::SvgzItem;
    } else if (itemName.endsWith(".qpic")) {
        return HbThemeIndexItemData::QpicItem;
    } else if (itemName.endsWith(".fxml")) {
        return HbThemeIndexItemData::FxmlItem;
    } else if (itemName.endsWith(".axml")) {
        return HbThemeIndexItemData::AxmlItem;
    }

    return HbThemeIndexItemData::NotDefined;
}

void processFile(const QFileInfo &info) //, const QString &themename)
{
    QString fullFilename = info.absoluteFilePath();
    QString filename = info.fileName();

    HbThemeIndexItemData itemData;

    // First get correct item type
    itemData.itemType = getItemType(filename);

    switch (itemData.itemType) {
        case HbThemeIndexItemData::SvgItem: // fallback all icon types
        case HbThemeIndexItemData::PngItem:
        case HbThemeIndexItemData::MngItem:
        case HbThemeIndexItemData::GifItem:
        case HbThemeIndexItemData::XpmItem:
        case HbThemeIndexItemData::JpgItem:
        case HbThemeIndexItemData::NvgItem:
        case HbThemeIndexItemData::SvgzItem:
        case HbThemeIndexItemData::QpicItem:
            {
            // Define fileName (remove file extension)
            QString iconname;
            // If we come here, the filename must end with .* (e.g. .svg)
            iconname = filename.left(filename.lastIndexOf('.'));

            itemData.itemNameHash = hbHash(iconname);

            // Define default size
            HbIconSource source(fullFilename);
            if (itemData.itemType != HbThemeIndexItemData::NvgItem) {
                if (!source.imageReader()) {
                    std::cout << "ERROR: could not read file " << fullFilename.toStdString() << "\n";
                    return; // Don't add invalid file to the index
                }
            }
            QSize defaultSize = source.defaultSize().toSize();
            itemData.defaultWidth = static_cast<quint32>(defaultSize.width());
            itemData.defaultHeight = static_cast<quint32>(defaultSize.height());

            QString mirroredFilepath = fullFilename;

            // Define mirrored filename if there is a separate mirrored version of the
            // icon in 'mirrored' folder and in that case get also its default size

            int index1 = mirroredFilepath.lastIndexOf('/');
            int index2 = mirroredFilepath.lastIndexOf('\\');

            int index = index1 < index2 ? index2 : index1;

            if (index>0) {
                mirroredFilepath = mirroredFilepath.left(index);
                mirroredFilepath.append(QString("/mirrored/"));

                QStringList extList;
                extList << ".svg" << ".png" << ".mng" << ".gif" << ".xpm" << ".jpg" << ".nvg" << ".svgz" << ".qpic";

                foreach(QString ext, extList) {
                    QString mirroredFilenameCandidate = mirroredFilepath + iconname + ext;

                    if (QFile::exists(mirroredFilenameCandidate)) {
                        HbIconSource mirroredSource(fullFilename);
                        if ((ext != ".nvg") && !mirroredSource.imageReader()) {
                            std::cout << "ERROR: could not read file " << fullFilename.toStdString() << "\n";
                            continue; // Don't register invalid mirrored icon
                        }
                        itemData.mirroredItemType = getItemType(mirroredFilenameCandidate);
                        // Define mirrored icon size
                        QSize mirroredSize = mirroredSource.defaultSize().toSize();
                        itemData.mirroredWidth = static_cast<quint32>(mirroredSize.width());
                        itemData.mirroredHeight = static_cast<quint32>(mirroredSize.height());
                        break;
                    }
                }
            }

            if (MirroredList.contains(iconname)) {
                itemData.flags |= HbThemeIndexItemData::Mirrorable;
                // Remove all found items from the list so that in the end we can handle with missing items.
                MirroredList.removeOne(iconname);
            }

            if (LockedList.contains(iconname)) {
                itemData.flags |= HbThemeIndexItemData::Locked;
                // Remove all found items from the list so that in the end we can handle with missing items.
                LockedList.removeOne(iconname);
            }
            appendItem(itemData, iconname);
            break;
            }

        case HbThemeIndexItemData::AxmlItem: // fallback, these are handled same way
        case HbThemeIndexItemData::FxmlItem:
            {
            // Define fileName (file extension not removed)
            itemData.itemNameHash = hbHash(filename);

            if (LockedList.contains(filename)) {
                itemData.flags |= HbThemeIndexItemData::Locked;
                // Remove all found items from the list so that in the end we can handle with missing items.
                LockedList.removeOne(filename);
            }
            appendItem(itemData, filename);
            break;
            }

        default:
            {
            // Don't append unknown items to the index.
            break;
            }
    } // end switch
}

bool themeIndexItemDataLessThan(const HbThemeIndexItemData &d1, const HbThemeIndexItemData &d2)
{
    return d1.itemNameHash < d2.itemNameHash;
}

void indexColorVariables(const QString &filename)
{
    QFile file(filename);

    // Index only if given CSS file exists in theme.
    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QTextStream in(&file);

        while(!in.atEnd()) {
            QString line = in.readLine().trimmed();

            if (line.startsWith("qtc_")) {
                HbThemeIndexItemData itemData;

                // Extract name and value from line
                QString name = line.mid(0, line.indexOf(':')).trimmed();
                QString value;
                if (line.at(line.indexOf(':') + 1) == QChar('#')) {
                    // qtc_conv_list_received_normal:#B5B5B5;
                    int startIndex = line.indexOf('#');
                    int endIndex = line.indexOf(';');
                    value = line.mid(startIndex + 1, endIndex - startIndex - 1).trimmed();
                } else if (line.indexOf("var(") >= 0) {
                    // qtc_conv_list_received_normal:var(qtc_conv_list_sent_normal);
                    itemData.flags |= HbThemeIndexItemData::Reference;
                    int startIndex = line.indexOf("var(") + 4;
                    int endIndex = line.indexOf(')');
                    value = line.mid(startIndex, endIndex - startIndex).trimmed();
                }

                itemData.itemNameHash = hbHash(name);
                itemData.itemType = HbThemeIndexItemData::ColorItem;
                bool ok = false;
                itemData.colorValue = (quint32)value.toUInt(&ok, 16);   // Might cause compiler warning in 64 bit systems
                appendItem(itemData, name);
            }
        }
        file.close();
    } else if (verboseOn) {
        std::cout << "No " << filename.toStdString() << " in theme!\n";
    }
    return;
}

void processDir(const QDir &dir, const QString &themename, const QString targetName)
{
    QString iconPath(dir.absolutePath()+"/icons/"+themename);
    QString animationPath(dir.absolutePath()+"/animations/"+themename);
    QString effectPath(dir.absolutePath()+"/effects/"+themename);

    // Do the initialization
    IndexItems.clear();
    AddedItems.clear();
    MirroredList.clear();
    LockedList.clear();
    createMirroredList(iconPath);
    createLockedList(iconPath);

    // Only check icons under scalable and pixmap folders
    QDir scalableDir(iconPath+"/scalable");
    if (scalableDir.exists()) {
        QFileInfoList entries = scalableDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
        for (int i=0; i<entries.count(); i++) {
            QFileInfo info = entries.at(i);
            processFile(info);
        }
    }
 
    QDir pixmapDir(iconPath+"/pixmap");
    if (pixmapDir.exists()) {
        QFileInfoList entries = pixmapDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
        for (int i=0; i<entries.count(); i++) {
            QFileInfo info = entries.at(i);
            processFile(info);
        }
    }

    QDir animationDir(animationPath);
    if (animationDir.exists()) {
        QFileInfoList entries = animationDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
        for (int i=0; i<entries.count(); i++) {
            QFileInfo info = entries.at(i);
            processFile(info);
        }
    }

    QDir effectDir(effectPath);
    if (effectDir.exists()) {
        QFileInfoList entries = effectDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
        for (int i=0; i<entries.count(); i++) {
            QFileInfo info = entries.at(i);
            processFile(info);
        }
    }

    // There might still be items in mirrored list (e.g. frames are not actual items, but they are still mirrored)
    // So just create empty items for the rest of mirrored items in list
    foreach (QString mirrored, MirroredList) {
        HbThemeIndexItemData itemData;
        itemData.itemNameHash = hbHash(mirrored);
        itemData.flags |= HbThemeIndexItemData::Mirrorable;
        appendItem(itemData, mirrored);
    }

    // Read application and widget color group CSS files and index their content
    // Temporary check
    if (QFile::exists(dir.absolutePath() + "/style/" + themename + "/variables/color/hbapplicationcolorgroup.css") &&
        QFile::exists(dir.absolutePath() + "/style/" + themename + "/variables/color/hbwidgetcolorgroup.css")) {
        if (verboseOn) {
            std::cout << "Processing hbapplicationcolorgroup.css and hbwidgetcolorgroup.css";
        }
        indexColorVariables(dir.absolutePath() + "/style/" + themename + "/variables/color/hbapplicationcolorgroup.css");
        indexColorVariables(dir.absolutePath() + "/style/" + themename + "/variables/color/hbwidgetcolorgroup.css");
    } else {
        if (verboseOn) {
            std::cout << "Processing hbcolorgroup.css";
        }
        indexColorVariables(dir.absolutePath() + "/style/" + themename + "/variables/color/hbcolorgroup.css");
    }

    QDir targetDir;
    if (!targetDir.exists(targetName)) {
        targetDir.mkpath(targetName);
    }
    QString filename = targetName + themename + ".themeindex";

    QFile::remove(filename);
    QFile indexFile(filename);
    if (!indexFile.open(QIODevice::ReadWrite)) {
        std::cout << "ERROR: could not open index file!\n";
        return;
    }
    
    // Write the header in the beginning of the file
    HbThemeIndexHeaderV1 header;
    header.version = version;
    header.itemCount = IndexItems.count();

    if (verboseOn) {
        std::cout << "============================TOTALS==============================\n";
        std::cout << "Added " << header.itemCount << " items.\n";
        std::cout << "================================================================\n";
    }

    // Sort the list
    qStableSort(IndexItems.begin(), IndexItems.end(), themeIndexItemDataLessThan);
     
    // Write header info into the file stream
    qint64 ret = indexFile.write(reinterpret_cast<const char *>(&header), sizeof(HbThemeIndexHeaderV1));
    assert(ret == sizeof(HbThemeIndexHeaderV1));
    
    // Write items into the file stream
    foreach(const HbThemeIndexItemData &itemData, IndexItems) {
        ret = indexFile.write(reinterpret_cast<const char *>(&itemData), sizeof(HbThemeIndexItemData));
        assert(ret == sizeof(HbThemeIndexItemData));
    }
    
    indexFile.close();
}

void showHelp() {
    std::cout << "Themeindexer.exe usage:\n\n";
    std::cout << "hbthemeindexer [-v] -f filename OR (-n themename) -s themes source directory -t theme index file target directory\n\n";

    std::cout << "-n \t\ttheme to be indexed (\"<themename>.themeindex\"). If omitted, all found\n";
    std::cout << "\t\tthemes are indexed.\n";
    std::cout << "-s \t\tthemes source directory is scanned recursively and all the recognized\n";
    std::cout << "\t\tresource files for given theme are aded in the theme index.\n";
    std::cout << "-t \t\ttarget directory for the index file.\n";

    std::cout << "-f <filename>\tfile which contains multiple themes to be indexed. Each in its own row.\n";
    std::cout << "-v \t\tverbose output\n\n";

    std::cout << "Example 1:\n";
    std::cout << "Themeindexer.exe -n mytheme -s c:/mythemes -t c:/mythemes\n";
    std::cout << "Example 2:\n";
    std::cout << "Themeindexer.exe -f c:/mythemes/themes.txt\n\n";
}

void loadHbResource()
{
    bool loadSuccess;
    // To load resources embedded in hb library
    QString resourceLibName(RESOURCE_LIB_NAME);
    QLibrary hbLib(resourceLibName);
    loadSuccess = hbLib.load();
    
    if ( !loadSuccess ) {
        // Library may not be loaded, if it was built in debug mode and the name in debug mode is
        // different, change the name to debug version in that scenario
#ifdef Q_OS_WIN32
        resourceLibName += WIN32_DEBUG_SUFFIX;
#elif defined(Q_OS_MAC)
        resourceLibName += MAC_DEBUG_SUFFIX;
#endif
        // On symbian library name in debug mode is same as that in release mode,
        // so no need to do anything for that
        hbLib.setFileName(resourceLibName);
        loadSuccess = hbLib.load();
    }
}

int main(int argc, char *argv[])
{
    QApplication app(argc, argv, false); // GUIenabled=false

    if (argc <= 2) {
        showHelp();
    } else {
        // Load HbCore resource to be able to index hbdefault theme
        loadHbResource();

        QString filename;
        QString themename;
        QDir basedir;
        QString targetname;
        QStringList args(app.arguments());

        for (int n = 0; n < args.count(); n++) {
            if (args[n].toLower() == "-n") {
                themename = args[n+1];
                n++;
            } else if (args[n].toLower() == "-s") {
                basedir = QDir(args[n+1]);
                n++;
            } else if (args[n].toLower() == "-t") {
                targetname = args[n+1];
                n++;
            } else if (args[n].toLower() == "-v") {
                verboseOn = true;
            } else if (args[n].toLower() == "-f") {
                filename = args[n+1];
            }
        }

        if (filename.length() > 0) {
            if (!QFile::exists(filename)) {
                std::cout << "Error: file " << filename.toStdString() << " does not exist.\n";
            } else {
                // Open file and parse lines. Each line should have three value separated with:
                QFile themesToBeIndexed(filename);
                if (themesToBeIndexed.open(QIODevice::ReadOnly | QIODevice::Text)) {
                    QTextStream in(&themesToBeIndexed);

                    while(!in.atEnd()) {
                        QString line = in.readLine();

                        QStringList values = line.split(' ');
                        if (values.count() == 3) {
                            themename = values[0];
                            basedir = values[1];
                            targetname = values[2];

                            targetname.replace('\\', '/');
                            // Check that targetname has / at the end
                            if (!targetname.endsWith('/')) {
                                targetname.append('/');
                            }
                            processDir(basedir, themename, targetname);
                        }
                    }

                    themesToBeIndexed.close();

                    // Loop through themes string list and call processDir
                } else {
                    std::cout << "Error: file " << filename.toStdString() << " could not be opened.\n";
                }
            }
        } else {
            // Index only given dir

            targetname.replace('\\', '/');
            // Check that targetname has / at the end
            if (!targetname.endsWith('/')) {
                targetname.append('/');
            }

            if (themename.isEmpty()) {
                // Theme name not given, determine available themes
                QDir icondir(basedir);
                if (icondir.cd("icons")) {
                    QStringList entries = icondir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
                    foreach (const QString &entry, entries) {
                        QDir entrydir(icondir.filePath(entry));
                        if (entrydir.exists("index.theme")) {
                            processDir(basedir, entrydir.dirName(), targetname);
                        }
                    }
                }
            } else {
                // Process only given theme
                processDir(basedir, themename, targetname);
            }

        }
    }

    IndexItems.clear();
    return 0;
}

