/*
 * Copyright (c) 2022, Technische Universität 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:
 *    Iron Prando da Silva
 */

#include "phasedependenciestracker.h"

#include <chrono>
#include <iostream>

void 
PhaseDependenciesTracker::calculateDependencies(TraceDB& tdb, std::vector<QString>& commands) {
    using std::chrono::high_resolution_clock;
    using std::chrono::duration_cast;
    using std::chrono::duration;
    using std::chrono::microseconds;

    auto deviceInstantiationTimeStart = high_resolution_clock::now();
        auto deviceConfig = ConfigurationFactory::make(tdb);
    auto deviceInstantiationTimeEnd = high_resolution_clock::now();
    auto deviceInstantiationTimeDuration = duration_cast<microseconds>(deviceInstantiationTimeEnd - deviceInstantiationTimeStart);

    mBeginTransaction(tdb);

    mDropTable(tdb);

    if (commands.size() > 0) {
        auto phasesLoadingTimeStart = high_resolution_clock::now();
            auto& phases = mGetFilteredPhases(deviceConfig, tdb, commands);
        auto phasesLoadingTimeEnd = high_resolution_clock::now();
        auto phasesLoadingTimeDuration = duration_cast<microseconds>(phasesLoadingTimeEnd - phasesLoadingTimeStart);

        if (phases.size() != 0) {
            auto dependenciesCalcTimeStart = high_resolution_clock::now();
                auto& entries = mCalculateDependencies(deviceConfig, phases, commands);
            auto dependenciesCalcTimeEnd = high_resolution_clock::now();
            auto dependenciesCalcTimeDuration = duration_cast<microseconds>(dependenciesCalcTimeEnd - dependenciesCalcTimeStart);

            if (entries.size() > 0) {
                mCreateTable(tdb);
            }

            auto tableInsertionTimeStart = high_resolution_clock::now();
                mInsertIntoTable(tdb, entries);
            auto tableInsertionTimeEnd = high_resolution_clock::now();
            auto tableInsertionTimeDuration = duration_cast<microseconds>(tableInsertionTimeEnd - tableInsertionTimeStart);

            auto totalTime = deviceInstantiationTimeDuration + phasesLoadingTimeDuration + dependenciesCalcTimeDuration + tableInsertionTimeDuration;


        std::cout << "PhaseDependenciesTracker times (us):" << std::endl
                  << "\tDevice instantiation: " << deviceInstantiationTimeDuration.count() << std::endl
                  << "\tPhase loading: " << phasesLoadingTimeDuration.count() << std::endl
                  << "\tDependencies calculation: " << dependenciesCalcTimeDuration.count() << std::endl
                  << "\tDB table population: " << tableInsertionTimeDuration.count() << std::endl
                  << " - Total time: " << totalTime.count() << std::endl;

        } else {
            // TODO - not sure if necessary. Still, a possibility
            // mRollbackChanges(tdb);
            // return;
        }

    }

    mCommitTransaction(tdb);
}

void PhaseDependenciesTracker::mDropTable(TraceDB& tdb) {
    QString command = "DROP TABLE IF EXISTS DirectDependencies; ";
    
    auto query = mExecuteQuery(tdb, command);
    query.finish();
}

void PhaseDependenciesTracker::mCreateTable(TraceDB& tdb) {
    QString command = "CREATE TABLE DirectDependencies( DelayedPhaseID INT, DelayedPhaseName, DependencyType, TimeDependency, DependencyPhaseID INT, DependencyPhaseName ); ";
    
    auto query = mExecuteQuery(tdb, command);
    query.finish();
}

void PhaseDependenciesTracker::mInsertIntoTable(TraceDB& tdb, const std::vector<DBDependencyEntry>& entries) {
    static const size_t bulkInsertionSize = 30;
    
    auto numberOfEntries = entries.size();

    QString command;
    size_t counter = 0;
    for (const auto& entry : entries) {
        if (counter == 0) {
            // Reset command string and add first entry
            command = "INSERT INTO 'DirectDependencies' ('DelayedPhaseID', 'DelayedPhaseName', 'DependencyType', 'TimeDependency', 'DependencyPhaseID', 'DependencyPhaseName') ";
            mAddFirstEntryCommandString(command, entry);
            
            counter++;

        } else if (counter == bulkInsertionSize-1) {
            // Write last entry and submit
            mAddEntryCommandString(command, entry);

            auto query = mExecuteQuery(tdb, command);
            query.finish();

            counter = 0;

        } else {
            // Write entry
            mAddEntryCommandString(command, entry);

            counter++;


        }

    }

    if (counter != 0) {
        auto query = mExecuteQuery(tdb, command);
        query.finish();
    }

}

const std::vector<std::shared_ptr<DBPhaseEntryBase>>
PhaseDependenciesTracker::mGetFilteredPhases(const std::shared_ptr<ConfigurationBase> deviceConfig, TraceDB& tdb, const std::vector<QString>& commands)
{
    std::vector<std::shared_ptr<DBPhaseEntryBase>> phases;
    QString queryStr = deviceConfig->getQueryStr(commands);
    auto query = mExecuteQuery(tdb, queryStr);

    if (!query.next())
        return phases;

    if (!query.last()) {
        throw sqlException( ("Query:\n " + tdb.queryToString(query) + "\n failed. Error: \n"
                             "Could not retrieve number of rows\n").toStdString(),
                             tdb.pathToDB.toStdString());
    }

    size_t nrows = query.at() + 1;

    if (!query.first()) {
        throw sqlException( ("Query:\n " + tdb.queryToString(query) + "\n failed. Error: \n"
                             "Could not retrieve number of rows safely\n").toStdString(),
                             tdb.pathToDB.toStdString());
    }

    phases.resize(nrows);

    size_t rowIt = 0;
    do {
        phases[rowIt] = deviceConfig->makePhaseEntry(query);

        ++rowIt;
    } while (query.next());

    if  (rowIt != nrows) {
        throw std::runtime_error("An error occurred while fetching phases in 'PhaseDependenciesTracker::mGetFilteredPhases': expected " + std::to_string(nrows) + " rows, but found " + std::to_string(rowIt) + "\n");
    }

    query.finish();

    return phases;
}

const std::vector<DBDependencyEntry>
PhaseDependenciesTracker::mCalculateDependencies(const std::shared_ptr<ConfigurationBase> deviceConfig, const std::vector<std::shared_ptr<DBPhaseEntryBase>>& phases, std::vector<QString>& commands) {
    std::vector<DBDependencyEntry> entries;
    entries.reserve((size_t) (0.4 * phases.size()));

    // Get dependencies for device
    const DependencyMap deviceDependencies = deviceConfig->getDependencies(commands);

    // Tries to find all timing dependencies for each phase on the trace
    PoolControllerMap poolController = deviceConfig->getPools();
    for (size_t i = 1; i < phases.size(); i++) {
        // Pool dependencies variables reset
        poolController.clear();

        // Auxiliary variables
        const auto phase = phases[i];
        if (phase == nullptr) continue;
        
        // Get time dependency descriptions for the current phase
        const auto& deps = deviceDependencies.at(phase->phaseName);

        // Loop all previous phases until there cannot be any more time dependencies
        for (int j = i-1; j >= 0; j--) {
            // Get next phase to analyse
            const auto& otherPhase = phases[j];
            // Calculates the time difference in nanoseconds
            const auto timeDiff = phase->phaseBegin - otherPhase->phaseBegin;
            // Time difference begin greater than the maximum possible dependency time ends the internal loop
            if (timeDiff > deps.maxTime) break;

            // For each possible dependency for the current phase,
            // checks if otherPhase would match as a dependency
            for (const auto& dep : deps.dependencies) {
                bool isPoolDep = dep.phaseDep.isPool();
                if (!isPoolDep && dep.phaseDep != otherPhase->phaseName) continue;
                
                if (!phase->potentialDependency(dep, otherPhase)) {
                    continue;
                }

                if (isPoolDep) {
                    // Captures activate window and command bus dependencies

                    auto busyTime = poolController.getBusyTime(dep.phaseDep, otherPhase->phaseName);
                    if (busyTime > 0 && timeDiff <= busyTime) {
                        if (timeDiff == busyTime) { 
                            // Captures only the first (exactly matching time) phase in
                            // the pool window as a dependency
                            poolController.push(dep.phaseDep, DBDependencyEntry{
                                phase->id,
                                phase->phaseName.getIDStr(),
                                PhaseDependency::dependencyTypeName(dep.depType),
                                dep.timeDepName,
                                otherPhase->id,
                                otherPhase->phaseName.getIDStr()
                            });
                        } 

                        if (timeDiff < busyTime) {
                            poolController.increment(dep.phaseDep);
                        }
                    
                    }

                    continue; 
                }

                if (timeDiff == dep.timeValue) {
                    entries.emplace_back(DBDependencyEntry{
                        phase->id,
                        phase->phaseName.getIDStr(),
                        PhaseDependency::dependencyTypeName(dep.depType),
                        dep.timeDepName,
                        otherPhase->id,
                        otherPhase->phaseName.getIDStr()
                    });
                }

            }

        }

        poolController.merge(entries);

    }

    return entries;
}

QSqlQuery PhaseDependenciesTracker::mExecuteQuery(TraceDB& tdb, const QString queryStr) {
    QSqlQuery query(tdb.database);
    query.prepare(queryStr);
    tdb.executeQuery(query);

    return query;
}

void PhaseDependenciesTracker::mBeginTransaction(TraceDB& tdb) {
    const QString queryStr = "BEGIN TRANSACTION;";
    auto query = mExecuteQuery(tdb, queryStr);
    query.finish();
}

void PhaseDependenciesTracker::mRollbackChanges(TraceDB& tdb) {
    const QString queryStr = "ROLLBACK;";
    auto query = mExecuteQuery(tdb, queryStr);
    query.finish();
}

void PhaseDependenciesTracker::mCommitTransaction(TraceDB& tdb) {
    const QString queryStr = "COMMIT;";
    auto query = mExecuteQuery(tdb, queryStr);
    query.finish();
}

void PhaseDependenciesTracker::mAddFirstEntryCommandString(QString& command, const DBDependencyEntry& entry) {
    command = command + " SELECT '" + QString::number(entry.delayedPhaseID) + "' AS 'DelayedPhaseID', '"
                      + entry.delayedPhaseName + "' AS 'DelayedPhaseName', '"
                      + entry.dependencyType + "' AS 'DependencyType', '"
                      + entry.timeDependency + "' AS 'TimeDependency', '"
                      + QString::number(entry.dependencyPhaseID) + "' AS 'DependencyPhaseID', '"
                      + entry.dependencyPhaseName + "' AS 'DependencyPhaseName' ";

}

void PhaseDependenciesTracker::mAddEntryCommandString(QString& command, const DBDependencyEntry& entry) {
    command = command + " UNION ALL SELECT '" + QString::number(entry.delayedPhaseID) + "', '"
                      + entry.delayedPhaseName + "', '"
                      + entry.dependencyType + "', '"
                      + entry.timeDependency + "', '"
                      + QString::number(entry.dependencyPhaseID) + "', '"
                      + entry.dependencyPhaseName + "' ";
}
