/*
 * 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
 *    Eder F. Zulian
 */

#include <iostream>
#include <algorithm>

#include "TlmRecorder.h"
#include "protocol.h"
#include "dramExtension.h"
#include "xmlAddressdecoder.h"
#include "../controller/core/configuration/Configuration.h"
#include "../controller/Controller.h"

using namespace std;


TlmRecorder::TlmRecorder(sc_module_name /*name*/, string uri,
                         string dbname) : sqlScriptURI(uri), dbName(dbname), totalNumTransactions(1),
    simulationTimeCoveredByRecording(SC_ZERO_TIME)
{
    recordedData.reserve(transactionCommitRate);
    setUpTransactionTerminatingPhases();
    openDB(TlmRecorder::dbName.c_str());
    char *sErrMsg;
    sqlite3_exec(db, "PRAGMA main.page_size = 4096", NULL, NULL, &sErrMsg);
    sqlite3_exec(db, "PRAGMA main.cache_size=10000", NULL, NULL, &sErrMsg);
    sqlite3_exec(db, "PRAGMA main.locking_mode=EXCLUSIVE", NULL, NULL, &sErrMsg);
    sqlite3_exec(db, "PRAGMA main.synchronous=OFF", NULL, NULL, &sErrMsg);
    sqlite3_exec(db, "PRAGMA journal_mode = OFF", NULL, NULL, &sErrMsg);

    createTables(TlmRecorder::sqlScriptURI);
    prepareSqlStatements();

    printDebugMessage("Starting new database transaction");
}

TlmRecorder::~TlmRecorder()
{
    if (db)
        closeConnection();
    sqlite3_finalize(insertTransactionStatement);
    sqlite3_finalize(insertRangeStatement);
    sqlite3_finalize(updateRangeStatement);
    sqlite3_finalize(insertPhaseStatement);
    sqlite3_finalize(updatePhaseStatement);
    sqlite3_finalize(insertGeneralInfoStatement);
    sqlite3_finalize(insertDebugMessageStatement);
    sqlite3_finalize(updateDataStrobeStatement);
    sqlite3_finalize(insertPowerStatement);
}

void TlmRecorder::recordPower(double timeInSeconds, double averagePower)
{
    sqlite3_bind_double(insertPowerStatement, 1, timeInSeconds);
    sqlite3_bind_double(insertPowerStatement, 2, averagePower);
    executeSqlStatement(insertPowerStatement);
}

void TlmRecorder::recordPhase(tlm::tlm_generic_payload &trans,
                              tlm::tlm_phase phase, sc_time time)
{
    if (currentTransactionsInSystem.count(&trans) == 0)
        introduceTransactionSystem(trans);

    string phaseName = phaseNameToString(phase);
    string phaseBeginPrefix = "BEGIN_";
    string phaseEndPrefix = "END_";

    if (phaseName.find(phaseBeginPrefix) != string::npos) {
        phaseName.erase(0, phaseBeginPrefix.length());
        assert(currentTransactionsInSystem.count(&trans) != 0);
        currentTransactionsInSystem[&trans].insertPhase(phaseName, time);
    } else {
        phaseName.erase(0, phaseEndPrefix.length());
        assert(currentTransactionsInSystem.count(&trans) != 0);
        currentTransactionsInSystem[&trans].setPhaseEnd(phaseName, time);
    }

    bool phaseTerminatesTransaction = count(transactionTerminatingPhases.begin(),
                                            transactionTerminatingPhases.end(), phase) == 1;
    if (phaseTerminatesTransaction)
        removeTransactionFromSystem(trans);

    simulationTimeCoveredByRecording = time;
}


void TlmRecorder::updateDataStrobe(const sc_time &begin, const sc_time &end,
                                   tlm::tlm_generic_payload &trans)
{
    assert(currentTransactionsInSystem.count(&trans) != 0);
    currentTransactionsInSystem[&trans].timeOnDataStrobe.start = begin;
    currentTransactionsInSystem[&trans].timeOnDataStrobe.end = end;
}


void TlmRecorder::recordDebugMessage(std::string message, sc_time time)
{
    insertDebugMessageInDB(message, time);
}


// ------------- internal -----------------------

void TlmRecorder::introduceTransactionSystem(tlm::tlm_generic_payload &trans)
{
    unsigned int id = totalNumTransactions++;
    currentTransactionsInSystem[&trans].id = id;
    currentTransactionsInSystem[&trans].address = trans.get_address();
    currentTransactionsInSystem[&trans].burstlength = trans.get_streaming_width();
    currentTransactionsInSystem[&trans].dramExtension = DramExtension::getExtension(
                                                            trans);

    if (DramExtension::getExtension(trans).getThread().ID() ==
            Controller::ControllerThreadId())
        currentTransactionsInSystem[&trans].timeOfGeneration = SC_ZERO_TIME;
    else
        currentTransactionsInSystem[&trans].timeOfGeneration =
            GenerationExtension::getExtension(&trans).TimeOfGeneration();

    printDebugMessage("New transaction #" + to_string(id) + " generation time " +
                      currentTransactionsInSystem[&trans].timeOfGeneration.to_string());

    if (id % transactionCommitRate == 0) {
        printDebugMessage(
            "Committing transactions " + to_string(id - transactionCommitRate + 1) + " - "
            + to_string(id));
        commitRecordedDataToDB();
    }
}

void TlmRecorder::removeTransactionFromSystem(tlm::tlm_generic_payload &trans)
{
    assert(currentTransactionsInSystem.count(&trans) != 0);

    printDebugMessage("Removing transaction #" + to_string(
                          currentTransactionsInSystem[&trans].id));

    Transaction &recordingData = currentTransactionsInSystem[&trans];
    recordedData.push_back(recordingData);
    currentTransactionsInSystem.erase(&trans);
}

void TlmRecorder::commitRecordedDataToDB()
{
    sqlite3_exec(db, "BEGIN;", 0, 0, 0);
    for (Transaction &recordingData : recordedData) {
        assert(recordingData.recordedPhases.size() > 0);
        insertTransactionInDB(recordingData);
        for (Transaction::Phase &phaseData : recordingData.recordedPhases) {
            insertPhaseInDB(phaseData.name, phaseData.interval.start,
                            phaseData.interval.end, recordingData.id);
        }

        sc_time rangeBegin = recordingData.recordedPhases.front().interval.start;
        sc_time rangeEnd = recordingData.recordedPhases.back().interval.end;
        insertRangeInDB(recordingData.id, rangeBegin, rangeEnd);
    }

    sqlite3_exec(db, "COMMIT;", 0, 0, 0);
    recordedData.clear();
}


void TlmRecorder::Transaction::insertPhase(string name, sc_time begin)
{
    recordedPhases.push_back(Phase(name, begin));
}

void TlmRecorder::Transaction::setPhaseEnd(string name, sc_time end)
{
    // Find the latest recorder phase for that transaction with a matching name and update it
    // Note: Transaction have the same phase multiple times (e.g. PRE->ACT->REF->ACT->RD) only update the latest
    // one that has been recorder
    for (int i = recordedPhases.size() - 1; i >= 0; i--) {
        Phase &data = recordedPhases[i];
        if (data.name == name) {
            data.interval.end = end;
            return;

        }
    }
    SC_REPORT_FATAL("Recording Error",
                    "While trying to set phase end: phaseBegin has not been recorded");
}

void TlmRecorder::openDB(std::string name)
{
    ifstream f(name.c_str());
    if (f.good()) {
        if (remove(name.c_str()) != 0) {
            SC_REPORT_FATAL("TlmRecorder", "Error deleting file" );
        }
    }

    if (sqlite3_open(name.c_str(), &db) != SQLITE_OK) {
        SC_REPORT_FATAL("Error in TraceRecorder", "Error cannot open database");
        sqlite3_close(db);
    }
}


void TlmRecorder::createTables(string pathToURI)
{
    string initial = loadTextFileContents(pathToURI);
    executeSqlCommand(initial);
}

void TlmRecorder::setUpTransactionTerminatingPhases()
{
    transactionTerminatingPhases.push_back(tlm::END_RESP);

    // Refresh All
    transactionTerminatingPhases.push_back(static_cast<const tlm::tlm_phase>
                                           (END_REFA));

    // Refresh Bank
    transactionTerminatingPhases.push_back(static_cast<const tlm::tlm_phase>
                                           (END_REFB));
    transactionTerminatingPhases.push_back(static_cast<const tlm::tlm_phase>
                                           (END_ACTB));
    transactionTerminatingPhases.push_back(static_cast<const tlm::tlm_phase>
                                           (END_PREB));

    // Phases for Power Down
    transactionTerminatingPhases.push_back(static_cast<const tlm::tlm_phase>
                                           (END_PDNA));
    transactionTerminatingPhases.push_back(static_cast<const tlm::tlm_phase>
                                           (END_PDNP));
    transactionTerminatingPhases.push_back(static_cast<const tlm::tlm_phase>
                                           (END_SREF));

    // Phases for Power Down Bankwise
    transactionTerminatingPhases.push_back(static_cast<const tlm::tlm_phase>
                                           (END_PDNAB));
    transactionTerminatingPhases.push_back(static_cast<const tlm::tlm_phase>
                                           (END_PDNPB));
    transactionTerminatingPhases.push_back(static_cast<const tlm::tlm_phase>
                                           (END_SREFB));
}

void TlmRecorder::prepareSqlStatements()
{
    insertTransactionString =
        "INSERT INTO Transactions VALUES (:id,:rangeID,:address,:burstlength,:thread,:channel,:bank,:bankgroup,:row,:column,:dataStrobeBegin,:dataStrobeEnd, :timeOfGeneration,:command)";
    insertRangeString = "INSERT INTO Ranges VALUES (:id,:begin,:end)";
    updateRangeString = "UPDATE Ranges SET  End = :end WHERE ID = :id";
    updateDataStrobeString =
        "UPDATE Transactions SET DataStrobeBegin = :begin, DataStrobeEnd = :end WHERE ID = :id";

    insertPhaseString =
        "INSERT INTO Phases (PhaseName,PhaseBegin,PhaseEnd,Transact) VALUES (:name,:begin,:end,:transaction)";
    updatePhaseString =
        "UPDATE Phases SET PhaseEnd = :end WHERE Transact = :trans AND PhaseName = :name";
    insertGeneralInfoString =
        "INSERT INTO GeneralInfo (NumberOfTransactions,TraceEnd,NumberOfBanks,clk,UnitOfTime,MCconfig,Memspec,Traces, WindowSize, FlexibleRefresh, MaxRefBurst, ControllerThread) VALUES"
        "(:numberOfTransactions,:end,:numberOfBanks,:clk,:unitOfTime,:mcconfig,:memspec,:traces,:windowSize, :flexibleRefresh, :maxRefBurst, :controllerThread)";
    insertDebugMessageString =
        "INSERT INTO DebugMessages (Time,Message) Values (:time,:message)";
    insertPowerString = "INSERT INTO Power VALUES (:time,:averagePower)";

    sqlite3_prepare_v2(db, insertTransactionString.c_str(), -1,
                       &insertTransactionStatement, 0);
    sqlite3_prepare_v2(db, insertRangeString.c_str(), -1, &insertRangeStatement, 0);
    sqlite3_prepare_v2(db, updateRangeString.c_str(), -1, &updateRangeStatement, 0);
    sqlite3_prepare_v2(db, insertPhaseString.c_str(), -1, &insertPhaseStatement, 0);
    sqlite3_prepare_v2(db, updatePhaseString.c_str(), -1, &updatePhaseStatement, 0);
    sqlite3_prepare_v2(db, updateDataStrobeString.c_str(), -1,
                       &updateDataStrobeStatement, 0);
    sqlite3_prepare_v2(db, insertGeneralInfoString.c_str(), -1,
                       &insertGeneralInfoStatement, 0);
    sqlite3_prepare_v2(db, insertDebugMessageString.c_str(), -1,
                       &insertDebugMessageStatement, 0);
    sqlite3_prepare_v2(db, insertPowerString.c_str(), -1, &insertPowerStatement, 0);
}

void TlmRecorder::insertDebugMessageInDB(string message, const sc_time &time)
{
    sqlite3_bind_int64(insertDebugMessageStatement, 1, time.value());
    sqlite3_bind_text(insertDebugMessageStatement, 2, message.c_str(),
                      message.length(), 0);
    executeSqlStatement(insertDebugMessageStatement);
}

void TlmRecorder::insertGeneralInfo()
{
    sqlite3_bind_int64(insertGeneralInfoStatement, 1, totalNumTransactions - 1);
    sqlite3_bind_int64(insertGeneralInfoStatement, 2,
                       simulationTimeCoveredByRecording.value());
    sqlite3_bind_int(insertGeneralInfoStatement, 3,
                     Configuration::getInstance().memSpec.NumberOfBanks);
    sqlite3_bind_int(insertGeneralInfoStatement, 4,
                     Configuration::getInstance().memSpec.clk.value());
    sqlite3_bind_text(insertGeneralInfoStatement, 5, "PS", 2, NULL);
    sqlite3_bind_text(insertGeneralInfoStatement, 6, mcconfig.c_str(),
                      mcconfig.length(), NULL);
    sqlite3_bind_text(insertGeneralInfoStatement, 7, memspec.c_str(),
                      memspec.length(), NULL);
    sqlite3_bind_text(insertGeneralInfoStatement, 8, traces.c_str(),
                      traces.length(), NULL);
    if (!Configuration::getInstance().EnableWindowing)
        sqlite3_bind_int64(insertGeneralInfoStatement, 9, 0);
    else
        sqlite3_bind_int64(insertGeneralInfoStatement, 9,
                           (Configuration::getInstance().memSpec.clk *
                            Configuration::getInstance().WindowSize).value());
    if (Configuration::getInstance().ControllerCoreRefEnablePostpone
            || Configuration::getInstance().ControllerCoreRefEnablePullIn) {
        sqlite3_bind_int(insertGeneralInfoStatement, 10, 1);
        sqlite3_bind_int(insertGeneralInfoStatement, 11,
                         std::max(Configuration::getInstance().ControllerCoreRefMaxPulledIn,
                                  Configuration::getInstance().ControllerCoreRefMaxPostponed));
    } else {
        sqlite3_bind_int(insertGeneralInfoStatement, 10, 0);
        sqlite3_bind_int(insertGeneralInfoStatement, 11, 0);
    }
    sqlite3_bind_int(insertGeneralInfoStatement, 12,
                     Controller::ControllerThreadId());                 ;
    executeSqlStatement(insertGeneralInfoStatement);
}

void TlmRecorder::insertTransactionInDB(Transaction &recordingData)
{
    sqlite3_bind_int(insertTransactionStatement, 1, recordingData.id);
    sqlite3_bind_int(insertTransactionStatement, 2, recordingData.id);
    sqlite3_bind_int(insertTransactionStatement, 3, recordingData.address);
    sqlite3_bind_int(insertTransactionStatement, 4, recordingData.burstlength);
    sqlite3_bind_int(insertTransactionStatement, 5,
                     recordingData.dramExtension.getThread().ID());
    sqlite3_bind_int(insertTransactionStatement, 6,
                     recordingData.dramExtension.getChannel().ID());
    sqlite3_bind_int(insertTransactionStatement, 7,
                     recordingData.dramExtension.getBank().ID());
    sqlite3_bind_int(insertTransactionStatement, 8,
                     recordingData.dramExtension.getBankGroup().ID());
    sqlite3_bind_int(insertTransactionStatement, 9,
                     recordingData.dramExtension.getRow().ID());
    sqlite3_bind_int(insertTransactionStatement, 10,
                     recordingData.dramExtension.getColumn().ID());
    sqlite3_bind_int64(insertTransactionStatement, 11,
                       recordingData.timeOnDataStrobe.start.value());
    sqlite3_bind_int64(insertTransactionStatement, 12,
                       recordingData.timeOnDataStrobe.end.value());
    sqlite3_bind_int64(insertTransactionStatement, 13,
                       recordingData.timeOfGeneration.value());

    executeSqlStatement(insertTransactionStatement);
}

void TlmRecorder::insertRangeInDB(unsigned int id, const sc_time &begin,
                                  const sc_time &end)
{
    sqlite3_bind_int(insertRangeStatement, 1, id);
    sqlite3_bind_int64(insertRangeStatement, 2, begin.value());
    sqlite3_bind_int64(insertRangeStatement, 3, end.value());
    executeSqlStatement(insertRangeStatement);
}
void TlmRecorder::insertPhaseInDB(string phaseName, const sc_time &begin,
                                  const sc_time &end,
                                  unsigned int transactionID)
{
    sqlite3_bind_text(insertPhaseStatement, 1, phaseName.c_str(),
                      phaseName.length(), 0);
    sqlite3_bind_int64(insertPhaseStatement, 2, begin.value());
    sqlite3_bind_int64(insertPhaseStatement, 3, end.value());
    sqlite3_bind_int(insertPhaseStatement, 4, transactionID);
    executeSqlStatement(insertPhaseStatement);
}


void TlmRecorder::executeSqlStatement(sqlite3_stmt *statement)
{
    int errorCode = sqlite3_step(statement);
    if (errorCode != SQLITE_DONE) {
        reportFatal("Error in TraceRecorder",
                    string("Could not execute statement. Error code: ") + to_string(errorCode));
    }
    sqlite3_reset(statement);
}

void TlmRecorder::executeSqlCommand(string command)
{
    printDebugMessage("Creating database by running provided sql script");

    char *errMsg = 0;
    int rc = sqlite3_exec(db, command.c_str(), NULL, 0, &errMsg);
    if (rc != SQLITE_OK) {
        SC_REPORT_FATAL("SQLITE Error", errMsg);
        sqlite3_free(errMsg);
    }

    printDebugMessage("Database created successfully");
}

void TlmRecorder::printDebugMessage(std::string message)
{
    DebugManager::getInstance().printDebugMessage(this->name(), message);
}

void TlmRecorder::closeConnection()
{
    commitRecordedDataToDB();
    insertGeneralInfo();
    printDebugMessage(
        "Number of transactions written to DB: " + std::to_string(
            totalNumTransactions - 1));
    printDebugMessage("tlmPhaseRecorder:\tEnd Recording");
    sqlite3_close(db);
    db = NULL;
}

