/*
 * Copyright (c) 2015, University of Kaiserslautern
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. 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.
 *
 * 3. Neither the name of the copyright holder 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 HOLDER
 * 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.
 *
 * Authors:
 *    Janik Schlemminger
 *    Robert Gernhardt
 *    Matthias Jung
 */

#include<QString>
#include<QFile>
#include<QStringList>
#include<QTextStream>
#include<QSqlError>
#include<iostream>
#include <QFileInfo>
#include"data/tracedb.h"
#include"businessObjects/comment.h"
#include "businessObjects/phases/phasefactory.h"


//define symbol printqueries if all queries should be printed to the console
//#define printqueries

using namespace std;

TraceDB::TraceDB(QString path, bool openExisting)
{
    this->pathToDB = path;

    database = QSqlDatabase::database(path);
    if (database.isValid() && database.isOpen()) {
        // Close the database connection if it exists and was not closed yet.
        database.removeDatabase(path);
        database.close();
    }

    database = QSqlDatabase::addDatabase("QSQLITE", path);
    database.setDatabaseName(path);
    database.open();
    if (!openExisting)
        dropAndCreateTables();
    prepareQueries();
    generalInfo = getGeneralInfoFromDB();
}

void TraceDB::prepareQueries()
{
    selectTransactionsByTimespan = QSqlQuery(database);
    selectTransactionsByTimespan.prepare(queryTexts.selectTransactionsByTimespan);
    selectTransactionById = QSqlQuery(database);

    selectTransactionById.prepare(queryTexts.selectTransactionById);
    selectDebugMessagesByTimespan = QSqlQuery(database);
    selectDebugMessagesByTimespan.prepare("SELECT time, Message FROM DebugMessages WHERE :begin <= time AND time <= :end ");
    selectDebugMessagesByTimespanWithLimit = QSqlQuery(database);
    selectDebugMessagesByTimespanWithLimit.prepare("SELECT time, Message FROM DebugMessages WHERE :begin <= time AND time <= :end LIMIT :limit");
}

void TraceDB::updateComments(vector<Comment> comments)
{

    QSqlQuery query(database);
    query.prepare("DELETE FROM Comments");
    executeQuery(query);
    query.prepare("insert into Comments values(:time,:text)");

    for (const Comment &comment : comments) {
        query.bindValue(":time", comment.Time());
        query.bindValue(":text", comment.Text());
        executeQuery(query);
    }
}

void TraceDB::updateFileDescription(const QString &description)
{
    QSqlQuery query(database);
    query.prepare("UPDATE GeneralInfo SET Description=:description");
    query.bindValue(":description", description);
    executeQuery(query);
}

void TraceDB::refreshData()
{
    generalInfo = getGeneralInfoFromDB();
}

//QueryText must select the fields
//TransactionID, Ranges.begin, Ranges.end, Address, TThread, TChannel, TBank, TRow, TColumn, Phases.ID AS PhaseID, PhaseName, PhaseBegin, PhaseEnd
vector<shared_ptr<Transaction>> TraceDB::getTransactionsWithCustomQuery(
                                 QString queryText)
{
    QSqlQuery query(database);
    query.prepare(queryText);
    executeQuery(query);
    return parseTransactionsFromQuery(query);
}

vector<shared_ptr<Transaction>> TraceDB::getTransactionsInTimespan(
                                 const Timespan &span)
{
    selectTransactionsByTimespan.bindValue(":begin", span.Begin());
    selectTransactionsByTimespan.bindValue(":end", span.End());
    executeQuery(selectTransactionsByTimespan);
    return parseTransactionsFromQuery(selectTransactionsByTimespan);
}


//TODO Remove exception
shared_ptr<Transaction> TraceDB::getTransactionByID(ID id)
{
    selectTransactionById.bindValue(":id", id);
    executeQuery(selectTransactionById);
    auto result = parseTransactionsFromQuery(selectTransactionById);
    if (!result.empty())
        return result[0];
    else
        throw sqlException(("Transaction with ID " + QString::number(
                                id) + " not in DB").toStdString(), this->pathToDB.toStdString());
}


shared_ptr<Transaction> TraceDB::getNextActivate(ID currentTransactionId)
{
    QSqlQuery query(database);
    QString queryText = queryTexts.queryHead +
                        "WHERE TransactionID > :currentID AND PhaseName = 'ACT' LIMIT 1";

    query.prepare(queryText);
    query.bindValue(":currentID", currentTransactionId);
    executeQuery(query);
    return parseTransactionFromQuery(query);
}

shared_ptr<Transaction> TraceDB::getNextPrecharge(ID currentTransactionId)
{
    QSqlQuery query(database);
    QString queryText = queryTexts.queryHead +
                        "WHERE TransactionID > :currentID AND PhaseName IN ('PRE','PRE_ALL') LIMIT 1";

    query.prepare(queryText);
    query.bindValue(":currentID", currentTransactionId);
    executeQuery(query);
    return parseTransactionFromQuery(query);
}

shared_ptr<Transaction> TraceDB::getNextActb(ID currentTransactionId)
{
    QSqlQuery query(database);
    QString queryText = queryTexts.queryHead +
                        "WHERE TransactionID > :currentID AND PhaseName = 'ACTB' LIMIT 1";

    query.prepare(queryText);
    query.bindValue(":currentID", currentTransactionId);
    executeQuery(query);
    return parseTransactionFromQuery(query);
}

shared_ptr<Transaction> TraceDB::getNextPreb(ID currentTransactionId)
{
    QSqlQuery query(database);
    QString queryText = queryTexts.queryHead +
                        "WHERE TransactionID > :currentID AND PhaseName = 'PREB' LIMIT 1";

    query.prepare(queryText);
    query.bindValue(":currentID", currentTransactionId);
    executeQuery(query);
    return parseTransactionFromQuery(query);
}

shared_ptr<Transaction> TraceDB::getNextRefresh(ID currentTransactionId)
{
    QSqlQuery query(database);
    QString queryText = queryTexts.queryHead +
                        "WHERE TransactionID > :currentID AND PhaseName IN ('REFA') LIMIT 1";
    query.prepare(queryText);
    query.bindValue(":currentID", currentTransactionId);
    executeQuery(query);
    return parseTransactionFromQuery(query);
}

shared_ptr<Transaction> TraceDB::getNextRefb(ID currentTransactionId)
{
    QSqlQuery query(database);
    QString queryText = queryTexts.queryHead +
                        "WHERE TransactionID > :currentID AND PhaseName = 'REFB' LIMIT 1";

    query.prepare(queryText);
    query.bindValue(":currentID", currentTransactionId);
    executeQuery(query);
    return parseTransactionFromQuery(query);
}

ID TraceDB::getTransactionIDFromPhaseID(ID phaseID)
{
    QSqlQuery query(database);
    query.prepare("SELECT Transact FROM Phases WHERE ID=:id");
    query.bindValue(":id", phaseID);
    executeQuery(query);

    if (query.next()) {
        return query.value(0).toInt();
    } else {
        throw sqlException("Phase with ID " + to_string(phaseID) + " not in db",
                           this->pathToDB.toStdString());
    }
}

GeneralInfo TraceDB::getGeneralInfoFromDB()
{
    QSqlQuery query(database);
    query.prepare("SELECT NumberOfTransactions,TraceEnd,NumberOfBanks,Clk,UnitOfTime,Traces,Memspec,MCconfig, WindowSize, ControllerThread FROM GeneralInfo");
    executeQuery(query);

    if (query.next()) {
        unsigned int numberOfTransactions = query.value(0).toInt();
        traceTime traceEnd = query.value(1).toLongLong();
        unsigned int numberOfBanks = query.value(2).toInt();
        unsigned int clkPeriod = query.value(3).toInt();
        QString unitOfTime = query.value(4).toString();
        unsigned int numberOfPhases = getNumberOfPhases();

        QString traces  = "Traces: " + query.value(5).toString();
        QString memspec = "Memspec: " + query.value(6).toString();
        QString mcconfig = "MCconfig: " + query.value(7).toString();
        unsigned int windowSize = query.value(8).toInt();
        unsigned int controllerThread = query.value(9).toUInt();

        QString description = (traces + "\n");
        description +=  mcconfig + "\n";
        description +=  memspec + "\n";
        description +=  "Number of Transactions: " + QString::number(
                            numberOfTransactions) + "\n";
        description +=  "Clock period: " + QString::number(clkPeriod) + " " + unitOfTime
                        + "\n";
        description +=  "Length of trace: " + prettyFormatTime(traceEnd) + "\n";
        description += "Window size:" + QString::number(windowSize) + "\n";

        return GeneralInfo(numberOfTransactions, numberOfPhases, Timespan(0, traceEnd),
                           numberOfBanks, description, unitOfTime, clkPeriod, windowSize,
                           controllerThread);
    } else {
        throw sqlException("Tracefile corrupted. No general info table",
                           this->pathToDB.toStdString());
    }
}

unsigned int TraceDB::getNumberOfPhases()
{
    QSqlQuery query(database);
    query.prepare("SELECT COUNT(ID) FROM Phases");
    executeQuery(query);

    query.next();
    return query.value(0).toInt();
}

vector<Comment> TraceDB::getComments()
{
    QSqlQuery query(database);
    query.prepare("SELECT Time,Text From Comments");
    executeQuery(query);
    return parseCommentsFromQuery(query);
}


vector<Comment> TraceDB::getDebugMessagesInTimespan(const Timespan &span)
{
    selectDebugMessagesByTimespan.bindValue(":begin", span.Begin());
    selectDebugMessagesByTimespan.bindValue(":end", span.End());
    executeQuery(selectDebugMessagesByTimespan);

    return parseCommentsFromQuery(selectDebugMessagesByTimespan);
}

vector<Comment> TraceDB::getDebugMessagesInTimespan(const Timespan &span,
                                                    unsigned int limit = 50)
{
    selectDebugMessagesByTimespanWithLimit.bindValue(":begin", span.Begin());
    selectDebugMessagesByTimespanWithLimit.bindValue(":end", span.End());
    selectDebugMessagesByTimespanWithLimit.bindValue(":limit", limit);
    executeQuery(selectDebugMessagesByTimespanWithLimit);
    return parseCommentsFromQuery(selectDebugMessagesByTimespanWithLimit);
}

/*  Helpers
 *
 *
 *
 */


shared_ptr<Transaction> TraceDB::parseTransactionFromQuery(QSqlQuery &query)
{
    auto result = parseTransactionsFromQuery(query);
    if (!result.empty())
        return result[0];
    else
        return shared_ptr<Transaction>();
}

vector<shared_ptr<Transaction>> TraceDB::parseTransactionsFromQuery(
                                 QSqlQuery &query)
{
    vector<shared_ptr<Transaction>> result;

    bool firstIteration = true;
    ID currentID = 0;
    int i = -1;

    while (query.next()) {

        ID id = query.value(0).toInt();

        if (currentID != id || firstIteration) {
            ++i;
            firstIteration = false;
            currentID = id;
            Timespan span(query.value(1).toLongLong(), query.value(2).toLongLong());
            Timespan spanOnStrobe(query.value(3).toLongLong(), query.value(4).toLongLong());
            unsigned int address  = query.value(5).toInt();
            unsigned int burstlength = query.value(6).toInt();
            unsigned int thread = query.value(7).toInt();
            unsigned int channel = query.value(8).toInt();
            unsigned int bank  = query.value(9).toInt();
            unsigned int bankgroup  = query.value(10).toInt();
            unsigned int row   = query.value(11).toInt();
            unsigned int column  = query.value(12).toInt();
            result.push_back(shared_ptr<Transaction>(new Transaction(id, address,
                                                                     burstlength, thread, channel, bank, bankgroup, row, column, span,
                                                                     spanOnStrobe)));
        }

        unsigned int phaseID = query.value(13).toInt();
        QString phaseName = query.value(14).toString();
        Timespan span(query.value(15).toLongLong(), query.value(16).toLongLong());
        result.at(result.size() - 1)->addPhase(PhaseFactory::CreatePhase(phaseID,
                                                                         phaseName, span, result.at(result.size() - 1), *this));
    }
    return result;
}

vector<Comment> TraceDB::parseCommentsFromQuery(QSqlQuery &query)
{
    vector<Comment> result;
    while (query.next()) {
        result.push_back(Comment(query.value(0).toLongLong(),
                                 query.value(1).toString()));
    }
    return result;
}

void TraceDB::executeQuery(QSqlQuery query)
{

    //query.exec returns bool indicating if the query was sucessfull
    if (query.exec()) {
#ifdef printqueries
        cout << queryToString(query).toStdString() << endl;
#endif
    }

    else {
        throw sqlException( ("Query:\n " + queryToString(query) + "\n failed. Error: \n"
                             +
                             query.lastError().text()).toStdString(), this->pathToDB.toStdString());
    }
}

QString TraceDB::queryToString(QSqlQuery query)
{
    QString str = query.lastQuery();
    QMapIterator<QString, QVariant> it(query.boundValues());
    while (it.hasNext()) {
        it.next();
        str.replace(it.key(), it.value().toString());
    }
    return str;
}


void TraceDB::dropAndCreateTables()
{
    executeScriptFile("common/static/createTraceDB.sql");
}

void TraceDB::executeScriptFile(QString fileName)
{
    QSqlQuery query(database);
    QFile scriptFile(fileName);

    if (scriptFile.open(QIODevice::ReadOnly)) {
        // The SQLite driver executes only a single (the first) query in the QSqlQuery
        // if the script contains more queries, it needs to be splitted.
        QStringList scriptQueries = QTextStream(&scriptFile).readAll().split(';');

        for (QString &queryTxt : scriptQueries) {
            if (queryTxt.trimmed().isEmpty()) {
                continue;
            }
            if (!query.exec(queryTxt)) {
                throw sqlException("Querry failed:" + query.lastError().text().toStdString(),
                                   this->pathToDB.toStdString());
            }
            query.finish();
        }
    }
}

