/*
 * Copyright (c) 2014 ARM Limited
 * All rights reserved
 *
 * The license below extends only to copyright in the software and shall
 * not be construed as granting a license to any other intellectual
 * property including but not limited to intellectual property relating
 * to a hardware implementation of the functionality of the software
 * licensed hereunder.  You may use the software subject to the license
 * terms below provided that you ensure that this notice is replicated
 * unmodified and in its entirety in all distributions of the software,
 * modified or unmodified, in source code or in binary form.
 *
 * Copyright (c) 2006 The Regents of The University of Michigan
 * Copyright (c) 2013 Advanced Micro Devices, Inc.
 * Copyright (c) 2013 Mark D. Hill and David A. Wood
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met: redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer;
 * 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;
 * neither the name of the copyright holders 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
 * OWNER 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: Nathan Binkert
 *          Steve Reinhardt
 *          Andrew Bardsley
 *          Matthias Jung
 *          Christian Menard
 */

/**
 * @file
 *
 * Defines an sc_module type to wrap a gem5 simulation.  The 'evaluate'
 * thread on that module implements the gem5 event loop.
 *
 * This currently only supports a single event queue and strictly
 * cooperatively threaded SystemC threads and so there should be at
 * most one Gem5Module instantiated in any simulation.
 */

#include "base/logging.hh"
#include "base/pollevent.hh"
#include "base/trace.hh"
#include "debug/Event.hh"
#include "sc_module.hh"
#include "sim/async.hh"
#include "sim/core.hh"
#include "sim/eventq.hh"
#include "sim/sim_exit.hh"
#include "sim/stat_control.hh"

namespace Gem5SystemC
{

/** There are assumptions throughout Gem5SystemC file that a tick is 1ps.
 *  Make this the case */
void
setTickFrequency()
{
    ::setClockFrequency(1000000000000);
}

Module::Module(sc_core::sc_module_name name) : sc_core::sc_channel(name),
    in_simulate(false)
{
    SC_METHOD(eventLoop);
    sensitive << eventLoopEnterEvent;
    dont_initialize();

    SC_METHOD(serviceExternalEvent);
    sensitive << externalSchedulingEvent;
    dont_initialize();
}

void
Module::SCEventQueue::wakeup(Tick when)
{
    DPRINTF(Event, "waking up SCEventQueue\n");
    /* Don't bother to use 'when' for now */
    module.notify();
}

void
Module::setupEventQueues(Module &module)
{
    fatal_if(mainEventQueue.size() != 0,
        "Gem5SystemC::Module::setupEventQueues must be called"
        " before any gem5 event queues are set up");

    numMainEventQueues = 1;
    mainEventQueue.push_back(new SCEventQueue("events", module));
    curEventQueue(getEventQueue(0));
}

void
Module::catchup()
{
    EventQueue *eventq = getEventQueue(0);
    Tick systemc_time = sc_core::sc_time_stamp().value();
    Tick gem5_time = curTick();

    /* gem5 time *must* lag SystemC as SystemC is the master */
    fatal_if(gem5_time > systemc_time, "gem5 time must lag SystemC time"
        " gem5: %d SystemC: %d", gem5_time, systemc_time);

    eventq->setCurTick(systemc_time);

    if (!eventq->empty()) {
        Tick next_event_time M5_VAR_USED = eventq->nextTick();

        fatal_if(gem5_time > next_event_time,
            "Missed an event at time %d gem5: %d, SystemC: %d",
            next_event_time, gem5_time, systemc_time);
    }
}

void
Module::notify(sc_core::sc_time time_from_now)
{
    externalSchedulingEvent.notify(time_from_now);
}

void
Module::serviceAsyncEvent()
{
    EventQueue *eventq = getEventQueue(0);
    std::lock_guard<EventQueue> lock(*eventq);

    assert(async_event);

    /* Catch up gem5 time with SystemC time so that any event here won't
     * be in the past relative to the current time */
    Tick systemc_time = sc_core::sc_time_stamp().value();

    /* Move time on to match SystemC */
    catchup();

    async_event = false;
    if (async_statdump || async_statreset) {
        Stats::schedStatEvent(async_statdump, async_statreset);
        async_statdump = false;
        async_statreset = false;
    }

    if (async_exit) {
        async_exit = false;
        exitSimLoop("user interrupt received");
    }

    if (async_io) {
        async_io = false;
        pollQueue.service();
    }

    if (async_exception)
        fatal("received async_exception, shouldn't be possible");
}

void
Module::serviceExternalEvent()
{
    EventQueue *eventq = getEventQueue(0);

    if (!in_simulate && !async_event)
        warn("Gem5SystemC external event received while not in simulate");

    if (async_event)
        serviceAsyncEvent();

    if (in_simulate && !eventq->empty())
        eventLoop();
}

void
Module::eventLoop()
{
    EventQueue *eventq = getEventQueue(0);

    fatal_if(!in_simulate, "Gem5SystemC event loop entered while"
        " outside Gem5SystemC::Module::simulate");

    if (async_event)
        serviceAsyncEvent();

    while (!eventq->empty()) {
        Tick next_event_time = eventq->nextTick();

        /* Move time on to match SystemC */
        catchup();

        Tick gem5_time = curTick();

        /* Woken up early */
        if (wait_exit_time > sc_core::sc_time_stamp().value()) {
            DPRINTF(Event, "Woken up early\n");
            wait_exit_time = sc_core::sc_time_stamp().value();
        }

        if (gem5_time < next_event_time) {
            Tick wait_period = next_event_time - gem5_time;
            wait_exit_time = gem5_time + wait_period;

            DPRINTF(Event, "Waiting for %d ticks for next gem5 event\n",
                wait_period);

            /* The next event is scheduled in the future, wait until
             *  then or until externalSchedulingEvent */
            eventLoopEnterEvent.notify(sc_core::sc_time::from_value(
                sc_dt::uint64(wait_period)));

            return;
        } else if (gem5_time > next_event_time) {
            Tick systemc_time = sc_core::sc_time_stamp().value();

            /* Missed event, for some reason the above test didn't work
             *  or an event was scheduled in the past */
            fatal("Missed an event at time %d gem5: %d, SystemC: %d",
                next_event_time, gem5_time, systemc_time);
        } else {
            /* Service an event */
            exitEvent = eventq->serviceOne();

            if (exitEvent) {
                eventLoopExitEvent.notify(sc_core::SC_ZERO_TIME);
                return;
            }
        }
    }

    fatal("Ran out of events without seeing exit event");
}

GlobalSimLoopExitEvent *
Module::simulate(Tick num_cycles)
{
    inform("Entering event queue @ %d.  Starting simulation...", curTick());

    if (num_cycles < MaxTick - curTick())
        num_cycles = curTick() + num_cycles;
    else /* counter would roll over or be set to MaxTick anyhow */
        num_cycles = MaxTick;

    GlobalEvent *limit_event = new GlobalSimLoopExitEvent(num_cycles,
        "simulate() limit reached", 0, 0);

    exitEvent = NULL;

    /* Cancel any outstanding events */
    eventLoopExitEvent.cancel();
    externalSchedulingEvent.cancel();

    in_simulate = true;
    eventLoopEnterEvent.notify(sc_core::SC_ZERO_TIME);

    /* Wait for event queue to exit, guarded by exitEvent just incase
     *  it already has exited and we don't want to completely rely
     *  on notify semantics */
    if (!exitEvent)
        wait(eventLoopExitEvent);

    /* Cancel any outstanding event loop entries */
    eventLoopEnterEvent.cancel();
    in_simulate = false;

    /* Locate the global exit event */
    BaseGlobalEvent *global_event = exitEvent->globalEvent();
    assert(global_event != NULL);

    GlobalSimLoopExitEvent *global_exit_event =
        dynamic_cast<GlobalSimLoopExitEvent *>(global_event);
    assert(global_exit_event != NULL);

    if (global_exit_event != limit_event) {
        limit_event->deschedule();
        delete limit_event;
    }

    return global_exit_event;
}

}