/*
 * Copyright (c) 2018, 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:
 *    Felipe S. Prado
 *    Matthias Jung
 */

#ifndef RECORDABLEDRAM_H_
#define RECORDABLEDRAM_H_

#include "Dram.h"
#include "../common/TlmRecorder.h"

struct RecordableDram : public Dram {
    SC_HAS_PROCESS(RecordableDram);
    RecordableDram(sc_module_name name, TlmRecorder *tlmRecorder):
        Dram(name), tlmRecorder(tlmRecorder)
    {
        // Create a thread that is triggered every $powerWindowSize
        // to generate a Power over Time plot in the Trace analyzer:
        if (Configuration::getInstance().PowerAnalysis
                && Configuration::getInstance().EnableWindowing)
            SC_THREAD(powerWindow);
    }

    ~RecordableDram()
    {
        if (Configuration::getInstance().PowerAnalysis) {
            // Obtain the residual energy which was not covered by
            // previous windows
            DRAMPower->calcEnergy();
            recordPower();
        }
        tlmRecorder->closeConnection();
    }

    TlmRecorder *tlmRecorder;
    sc_time powerWindowSize = Configuration::getInstance().memSpec.clk *
                              Configuration::getInstance().WindowSize;

    virtual tlm::tlm_sync_enum nb_transport_fw(tlm::tlm_generic_payload &payload,
                                               tlm::tlm_phase &phase, sc_time &delay)
    {
        // Recording time used by the traceAnalyzer
        sc_time recTime = sc_time_stamp() + delay;

        // These are terminating phases recorded by the DRAM. The execution
        // time of the related command must be taken into consideration.
        if (phase == END_PDNA || phase == END_PDNAB) {
            recTime += getExecutionTime(Command::PDNAX, payload);
        } else if (phase == END_PDNP || phase == END_PDNPB) {
            recTime += getExecutionTime(Command::PDNPX, payload);
        } else if (phase == END_SREF || phase == END_SREFB) {
            recTime += getExecutionTime(Command::SREFX, payload);
        }

        unsigned int thr = DramExtension::getExtension(payload).getThread().ID();
        unsigned int ch = DramExtension::getExtension(payload).getChannel().ID();
        unsigned int bg = DramExtension::getExtension(payload).getBankGroup().ID();
        unsigned int bank = DramExtension::getExtension(payload).getBank().ID();
        unsigned int row = DramExtension::getExtension(payload).getRow().ID();
        unsigned int col = DramExtension::getExtension(payload).getColumn().ID();

        printDebugMessage("Recording " + phaseNameToString(phase) +  " thread " +
                          to_string(thr) + " channel " + to_string(ch) + " bank group " + to_string(
                              bg) + " bank " + to_string(bank) + " row " + to_string(row) + " column " +
                          to_string(col) + " at " + recTime.to_string());

        tlmRecorder->recordPhase(payload, phase, recTime);

        return Dram::nb_transport_fw(payload, phase, delay);
    }

    // When working with floats, we have to decide ourselves what is an
    // acceptable definition for "equal". Here the number is compared with a
    // suitable error margin (0.00001).
    bool is_equal(double a, double b, const double epsilon = 1e-05)
    {
        return std::fabs(a - b) < epsilon;
    }

    // This Thread is only triggered when Power Simulation is enabled.
    // It estimates the current average power which will be stored in the trace database for visualization purposes.
    void powerWindow()
    {
        unsigned long long clk_cycles = 0;

        do {
            // At the very beginning (zero clock cycles) the energy is 0, so we wait first
            wait(powerWindowSize);

            clk_cycles = sc_time_stamp().value() /
                         Configuration::getInstance().memSpec.clk.value();

            DRAMPower->calcWindowEnergy(clk_cycles);

            // During operation the energy should never be zero since the device is always consuming
            assert(!is_equal(DRAMPower->getEnergy().window_energy, 0.0));

            // Store the time (in seconds) and the current average power (in mW) into the database
            recordPower();

            // Here considering that DRAMPower provides the energy in pJ and the power in mW
            printDebugMessage(string("\tWindow Energy: \t") + to_string(
                                  DRAMPower->getEnergy().window_energy *
                                  Configuration::getInstance().NumberOfDevicesOnDIMM) + string("\t[pJ]"));
            printDebugMessage(string("\tWindow Average Power: \t") + to_string(
                                  DRAMPower->getPower().window_average_power *
                                  Configuration::getInstance().NumberOfDevicesOnDIMM) + string("\t[mW]"));

        } while (true);
    }

    void recordPower()
    {
        tlmRecorder->recordPower(sc_time_stamp().to_seconds(),
                                 DRAMPower->getPower().window_average_power
                                 * Configuration::getInstance().NumberOfDevicesOnDIMM);
    }
};

#endif /* RECORDABLEDRAM_H_ */

