/*
 * 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:
 *    Robert Gernhardt
 *    Matthias Jung
 *    Felipe S. Prado
 */

#include "core/configuration/Configuration.h"
#include "Controller.h"
#include <iostream>

void Controller::buildScheduler()
{
    string selectedScheduler = Configuration::getInstance().Scheduler;

    if (selectedScheduler == "FIFO") {
        scheduler = new Fifo(*controllerCore);
    } else if (selectedScheduler == "FIFO_STRICT") {
        scheduler = new FifoStrict(*this, *controllerCore);
    } else if (selectedScheduler == "FR_FCFS") {
        scheduler = new FR_FCFS(*controllerCore);
    } else if (selectedScheduler == "FR_FCFS_RP") {
        scheduler = new FR_FCFS_RP(*controllerCore);
    } else if (selectedScheduler == "FR_FCFS_GRP") {
        scheduler = new FR_FCFS_GRP(*controllerCore, this);
    } else if (selectedScheduler == "GRP") {
        scheduler = new GRP(*controllerCore, this);
    } else if (selectedScheduler == "SMS") {
        scheduler = new SMS("SMS", *controllerCore,
                            Configuration::getInstance().SJFProbability);
    }
    //else if (selectedScheduler == "PAR_BS")
    //{
    //    scheduler = new PAR_BS(*controllerCore,Configuration::getInstance().RefreshAwareScheduling,
    //                           Configuration::getInstance().Capsize);
    //}
    //    else if (selectedScheduler == "Grouper")
    //    {
    //        scheduler = new ReadWriteGrouper(*controllerCore);
    //    }
    else
        reportFatal(name(), "unsupported scheduler: " + selectedScheduler);
}

//Send the next scheduled command to the DRAM
void Controller::send(const ScheduledCommand &command,
                      tlm_generic_payload &payload)
{
    sc_assert(command.getStart() >= sc_time_stamp());

    tlm_phase phase;

    switch (command.getCommand()) {
    //TODO: refactor tlm recorder
    case Command::Read:
        phase = BEGIN_RD;
        break;
    case Command::ReadA:
        phase = BEGIN_RDA;
        break;
    case Command::Write:
        phase = BEGIN_WR;
        break;
    case Command::WriteA:
        phase = BEGIN_WRA;
        break;
    case Command::AutoRefresh:
        if (!Configuration::getInstance().BankwiseLogic) {
            phase = BEGIN_REFA;
        } else
            phase = BEGIN_REFB;
        break;
    case Command::Activate:
        phase = BEGIN_ACT;
        break;
    case Command::ActB:
        phase = BEGIN_ACTB;
        break;
    case Command::Precharge:
        phase = BEGIN_PRE;
        break;
    case Command::PreB:
        phase = BEGIN_PREB;
        break;
    case Command::PrechargeAll:
        phase = BEGIN_PRE_ALL;
        break;
    case Command::PDNA:
        if (!Configuration::getInstance().BankwiseLogic) {
            phase = BEGIN_PDNA;
        } else
            phase = BEGIN_PDNAB;
        break;
    case Command::PDNAX:
        if (!Configuration::getInstance().BankwiseLogic) {
            phase = END_PDNA;
        } else
            phase = END_PDNAB;
        break;
    case Command::PDNP:
        if (!Configuration::getInstance().BankwiseLogic) {
            phase = BEGIN_PDNP;
        } else
            phase = BEGIN_PDNPB;
        break;
    case Command::PDNPX:
        if (!Configuration::getInstance().BankwiseLogic) {
            phase = END_PDNP;
        } else
            phase = END_PDNPB;
        break;
    case Command::SREF:
        if (!Configuration::getInstance().BankwiseLogic) {
            phase = BEGIN_SREF;
        } else
            phase = BEGIN_SREFB;
        break;
    case Command::SREFX:
        if (!Configuration::getInstance().BankwiseLogic) {
            phase = END_SREF;
        } else
            phase = END_SREFB;
        break;
    default:
        SC_REPORT_FATAL(0, "unsupported command was sent by controller");
        break;
    }

    sc_time notDelay = command.getStart() - sc_time_stamp();
    printDebugMessage(phaseNameToString(phase) + " notification in " +
                      notDelay.to_string());
    controllerCorePEQ.notify(payload, phase, notDelay);
}

//Trigger the next planned refresh or the power down mode on the DRAM
void Controller::send(Trigger trigger, sc_time time,
                      tlm_generic_payload &payload)
{
    sc_assert(time >= sc_time_stamp());

    tlm_phase phase;

    if (trigger == Trigger::REFTrigger) {
        phase = REF_TRIGGER;
    } else if (trigger == Trigger::PDNTrigger) {
        phase = PDN_TRIGGER;
    } else {
        SC_REPORT_FATAL("controller wrapper", "unknown trigger");
    }

    sc_time delay = time - sc_time_stamp();
    printDebugMessage(phaseNameToString(phase) + " notification in " +
                      delay.to_string());
    controllerCorePEQ.notify(payload, phase, delay);
}

void Controller::controllerCorePEQCallback(tlm_generic_payload &payload,
                                           const tlm_phase &phase)
{
    if (phase == REF_TRIGGER) {
        controllerCore->triggerRefresh(payload);
    } else if (phase == PDN_TRIGGER) {
        controllerCore->powerDownManager->sleep(DramExtension::getExtension(
                                                    payload).getBank(), sc_time_stamp());
    } else {
        Bank bank = DramExtension::getBank(payload);
        sendToDram(payload, phase, SC_ZERO_TIME);

        if (phase == BEGIN_RD || phase == BEGIN_WR) {
            scheduleNextFromScheduler(DramExtension::getBank(payload));
        } else if (phase == BEGIN_REFB)
            printDebugMessage("Entering REFB on bank " + to_string(bank.ID()));
        else if (phase == BEGIN_REFA)
            printDebugMessage("Entering REFA");
        else if (containsPhase(phase, { BEGIN_PDNAB, BEGIN_PDNPB, BEGIN_SREFB }))
            printDebugMessage("Entering PowerDown " + phaseNameToString(
                                  phase) + " on bank " + to_string(bank.ID()));
        else if (containsPhase(phase, { END_PDNAB, END_PDNPB, END_SREFB }))
            printDebugMessage("Leaving PowerDown " + phaseNameToString(
                                  phase) + " on bank " + to_string(bank.ID()));
        else if (containsPhase(phase, { BEGIN_PDNA, BEGIN_PDNP, BEGIN_SREF }))
            printDebugMessage("Entering PowerDown " + phaseNameToString(
                                  phase) + " on all banks");
        else if (containsPhase(phase, { END_PDNA, END_PDNP, END_SREF }))
            printDebugMessage("Leaving PowerDown " + phaseNameToString(
                                  phase) + " on all banks" );
        else if (containsPhase(phase, { BEGIN_RD, BEGIN_WR, BEGIN_ACTB, BEGIN_ACT, BEGIN_PREB, BEGIN_PRE, BEGIN_PRE_ALL, BEGIN_RDA, BEGIN_WRA })) {
            printDebugMessage("Controller has sent to DRAM this: " + phaseNameToString(
                                  phase) + " bank "  + to_string(bank.ID()));
        }
        else
            SC_REPORT_FATAL(0,
                            "Controller Core PEQ in controller wrapper was triggered with unsupported phase");
    }
}

tlm_sync_enum Controller::nb_transport_fw(tlm_generic_payload &payload,
                                          tlm_phase &phase, sc_time &fwDelay)
{
    sc_time notDelay = fwDelay;

    if (phase == BEGIN_REQ) {
        notDelay += Configuration::getInstance().memSpec.clk;

        //Bandwidth IDLE
        if ((getTotalNumberOfPayloadsInSystem() == 0) && idleState) {
            endBandwidthIdleCollector();
        }
    } else if (phase == END_RESP) {
        // Badnwith IDLE
        if (getTotalNumberOfPayloadsInSystem() == 1) {
            startBandwidthIdleCollector();
        }
    }

    printDebugMessage("[fw] " + phaseNameToString(phase) + " notification in " +
                      notDelay.to_string());
    frontendPEQ.notify(payload, phase, notDelay);

    return TLM_ACCEPTED;
}

unsigned int Controller::transport_dbg(tlm::tlm_generic_payload &trans)
{
    return iSocket->transport_dbg(trans);
}

void Controller::frontendPEQCallback(tlm_generic_payload &payload,
                                     const tlm_phase &phase)
{
    if (phase == BEGIN_REQ) {
        printDebugMessage(string("Payload in system: ") + to_string(
                              getTotalNumberOfPayloadsInSystem()));
        payload.acquire();
        payloadEntersSystem(payload);
        if (getTotalNumberOfPayloadsInSystem() >
                controllerCore->config.MaxNrOfTransactions) {
            printDebugMessage("##Backpressure: Max number of transactions in system reached");
            backpressure = &payload;
            return;
        }
        payload.set_response_status(tlm::TLM_OK_RESPONSE);
        sendToFrontend(payload, END_REQ, SC_ZERO_TIME);

        scheduler->storeRequest(&payload);
        scheduleNextFromScheduler(DramExtension::getExtension(payload).getBank());
    } else if (phase == PendingRequest) {
        // Schedule a pending request.
        scheduleNextFromScheduler(DramExtension::getExtension(payload).getBank());
    } else if (phase == END_RESP) {
        if (backpressure != NULL) {
            printDebugMessage("##Backpressure released");
            backpressure->set_response_status(tlm::TLM_OK_RESPONSE);
            sendToFrontend(*backpressure, END_REQ, SC_ZERO_TIME);

            scheduler->storeRequest(backpressure);
            scheduleNextFromScheduler(DramExtension::getExtension(backpressure).getBank());
            backpressure = NULL;
        }

        payloadLeavesSystem(payload);
        responseQueue.pop();
        payload.release();

        if(!responseQueue.empty()) {
            sendToFrontend(*(responseQueue.front()), BEGIN_RESP, SC_ZERO_TIME);
        }

    } else {
        SC_REPORT_FATAL(0,
                        "Front-end PEQ in controller wrapper was triggered with unknown phase");
    }
}

void Controller::payloadEntersSystem(tlm_generic_payload &payload)
{
    Bank bank = DramExtension::getExtension(payload).getBank();
    printDebugMessage(
        "Payload enters system on bank " + to_string(bank.ID()) +
        ". Total number of payloads in Controller: "
        + to_string(getTotalNumberOfPayloadsInSystem()));
    numberOfPayloadsInSystem[bank]++;
    // Set Start Time for Simulation
    if (startTimeSet == false) {
        printDebugMessage("Simulation Timer Start");
        startTime = sc_time_stamp() - Configuration::getInstance().memSpec.clk;
        startTimeSet = true;
    }
}

void Controller::payloadLeavesSystem(tlm_generic_payload &payload)
{
    Bank bank = DramExtension::getExtension(payload).getBank();
    numberOfPayloadsInSystem[bank]--;
    printDebugMessage(
        "Payload left system on bank " + to_string(bank.ID()) +
        ". Total number of payloads in Controller: "
        + to_string(getTotalNumberOfPayloadsInSystem()));
    controllerCore->powerDownManager->triggerSleep(bank, sc_time_stamp());
}

unsigned int Controller::getTotalNumberOfPayloadsInSystem()
{
    unsigned int sum = 0;
    for (Bank bank : controllerCore->getBanks()) {
        sum += numberOfPayloadsInSystem[bank];
    }
    return sum;
}

void Controller::scheduleNextFromScheduler(Bank bank)
{
    if (controllerCore->bankIsBusy(bank)) {
        return;
    }

    bool rescheduled = true;
    pair<Command, tlm::tlm_generic_payload *> nextRequest =
        scheduler->getNextRequest(bank);
    if (nextRequest.second != NULL) {
        schedule(nextRequest.first, *nextRequest.second);
    } else {
        gp *pendingRequest = scheduler->getPendingRequest(bank);
        if (pendingRequest != NULL) {
            rescheduled = true;
            frontendPEQ.notify(*(pendingRequest), PendingRequest,
                               Configuration::getInstance().memSpec.clk);
        }
    }

    queue<Bank> blocked;
    while (!blockedRequests.empty()) {
        bank = blockedRequests.front();
        blockedRequests.pop();

        pair<Command, tlm::tlm_generic_payload *> nextRequest =
            scheduler->getNextRequest(bank);
        if (nextRequest.second != NULL) {
            schedule(nextRequest.first, *nextRequest.second);
        } else {
            gp *pendingRequest = scheduler->getPendingRequest(bank);
            if (pendingRequest != NULL) {
                //Pending request
                if (!rescheduled) {
                    rescheduled = true;
                    frontendPEQ.notify(*(pendingRequest), PendingRequest,
                                       Configuration::getInstance().memSpec.clk);
                } else
                    blocked.push(bank);
            }
        }
    }
    blockedRequests = blocked;
}

void Controller::schedule(Command command, gp &payload)
{
    controllerCore->powerDownManager->wakeUp(DramExtension::getBank(payload),
                                             sc_time_stamp());
    if (controllerCore->scheduleRequest(command, payload)) {
        printDebugMessage("\t-> Next payload was scheduled by core [" + commandToString(
                              command) + "] (unblocked)");
    }
}

void Controller::sendToFrontend(tlm_generic_payload &payload,
                                const tlm_phase &phase, const sc_time &delay)
{
    tlm_phase TPhase = phase;
    sc_time TDelay = delay;
    tSocket->nb_transport_bw(payload, TPhase, TDelay);
}

tlm_sync_enum Controller::nb_transport_bw(tlm_generic_payload &payload,
                                          tlm_phase &phase, sc_time &bwDelay)
{
    printDebugMessage("[bw] " + phaseNameToString(phase) + " notification in " +
                      bwDelay.to_string());
    dramPEQ.notify(payload, phase, bwDelay);
    return TLM_ACCEPTED;
}

void Controller::dramPEQCallback(tlm_generic_payload &payload,
                                 const tlm_phase &phase)
{
    Bank bank = DramExtension::getExtension(payload).getBank();
    printDebugMessage("Received " + phaseNameToString(phase) + " on bank " +
                      to_string(bank.ID()) + " from DRAM");

    if (phase == END_RD || phase == END_WR) {
        if(responseQueue.empty()) {
            sendToFrontend(payload, BEGIN_RESP, SC_ZERO_TIME);
        }
        responseQueue.push(&payload);
    } else if (phase == END_RDA || phase == END_WRA) {
        if(responseQueue.empty()) {
            sendToFrontend(payload, BEGIN_RESP, SC_ZERO_TIME);
        }
        responseQueue.push(&payload);
        scheduleNextFromScheduler(bank);
    } else if (phase == END_REFA) {
        printDebugMessage("Finished auto refresh on all banks ");

        bool sleepy = true;
        for (Bank bank : controllerCore->getBanks()) {
            if (numberOfPayloadsInSystem[bank] != 0) {
                sleepy = false;
                scheduleNextFromScheduler(bank);
            }
        }

        if (sleepy == true) {
            controllerCore->powerDownManager->sleep(0, sc_time_stamp());
        }
    } else if (phase == END_REFB) {
        printDebugMessage("Finished auto refresh on bank " + to_string(bank.ID()));

        if (numberOfPayloadsInSystem[bank] == 0) {
            controllerCore->powerDownManager->sleep(bank, sc_time_stamp());
        } else {
            scheduleNextFromScheduler(bank);
        }
        scheduleNextFromScheduler(bank);
    } else if (containsPhase(phase, {END_PREB, END_PRE, END_ACTB, END_ACT})) {
        scheduleNextFromScheduler(bank);
    }
    else if (phase == END_PRE_ALL) {
        // No need to trigger anything for a END_PRE_ALL. It is followed by a AUTO_REFRESH anyway (in our current
        // scheduler implementation)
    } else {
        string str =
            string("DRAM PEQ in controller wrapper was triggered with unsupported phase ")
            + phaseNameToString(phase);
        SC_REPORT_FATAL(0, str.c_str());
    }
}

void Controller::sendToDram(tlm_generic_payload &payload,
                            const tlm_phase &phase, const sc_time &delay)
{
    tlm_phase TPhase = phase;
    sc_time TDelay = delay;
    iSocket->nb_transport_fw(payload, TPhase, TDelay);
}

void Controller::printDebugMessage(string message)
{
    debugManager.printDebugMessage(name(), message);
}

bool Controller::containsPhase(tlm_phase phase, std::vector<tlm_phase> phases)
{
    for (tlm_phase p : phases) {
        if (p == phase)
            return true;
    }
    return false;
}

void Controller::end_of_simulation()
{
    terminateSimulation();
}

void Controller::terminateSimulation()
{
    if (Configuration::getInstance().BankwiseLogic) {
        for (Bank bank : controllerCore->getBanks()) {
            controllerCore->powerDownManager->wakeUp(bank, clkAlign(sc_time_stamp()));
        }
    } else {
        controllerCore->powerDownManager->wakeUp(0, clkAlign(sc_time_stamp()));
    }

    endTime   = sc_time_stamp();
}

void Controller::startBandwidthIdleCollector()
{
    printDebugMessage("IDLE Start");
    idleStart = sc_time_stamp();
    endTime   = sc_time_stamp();
    idleState = true;
}

void Controller::endBandwidthIdleCollector()
{
    printDebugMessage("IDLE End");
    idleTime += sc_time_stamp() - idleStart +
                Configuration::getInstance().memSpec.clk;
    idleState = false;
}

sc_time Controller::getIdleTime()
{
    printDebugMessage("IDLE Time: " + idleTime.to_string());
    return idleTime;
}

sc_time Controller::getEndTime()
{
    printDebugMessage("End Time: " + endTime.to_string());
    return endTime;
}

sc_time Controller::getStartTime()
{
    printDebugMessage("Start Time: " + startTime.to_string());
    return startTime;
}
