/**
 * Copyright (c) 2004-2005, Regents of the University of California
 * 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 University of California, Los Angeles 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.
 */

package avrora.actions;

import avrora.core.*;
import avrora.core.SourceMapping.Location;
import avrora.sim.*;
import avrora.sim.util.SimUtil;
import avrora.Defaults;
import avrora.monitors.Monitor;
import cck.text.*;
import cck.util.*;

import java.util.*;

/**
 * The <code>SimAction</code> is an abstract class that collects many of the options common to single node and
 * multiple-node simulations into one place.
 *
 * @author Ben L. Titzer
 */
public class SimAction extends Action {

    public static final String HELP = "The \"simulate\" action creates a simulation with the specified program(s) " +
            "for the specified node(s). The simulation type might be as simple as a single node with a single " +
            "program, or a multiple-node sensor network simulation or robotics simulation.";

    public final Option.Bool REPORT_SECONDS = newOption("report-seconds", false,
            "This option causes all times printed out by the simulator to be reported " +
            "in seconds rather than clock cycles.");
    public final Option.Long SECONDS_PRECISION = newOption("seconds-precision", 6,
            "This option sets the precision (number of decimal places) reported for " +
            "event times in the simulation.");
    public final Option.Str SIMULATION = newOption("simulation", "single",
            "The \"simulation\" option selects from the available simulation types, including a single node " +
            "simulation, a sensor network simulation, or a robotics simulation.");
    public final Option.Bool THROUGHPUT = newOption("throughput", false,
            "This option enables reporting of simulator throughput (i.e. mhz).");

    protected Simulation simulation;
    protected long startms;
    protected boolean reported;

    public SimAction() {
        super(HELP);
    }

    /**
     * The <code>run()</code> method is called by the main class.
     *
     * @param args the command line arguments after the options have been stripped out
     * @throws Exception if there is a problem loading the program, or an exception occurs during
     *                             simulation
     */
    public void run(String[] args) throws Exception {
        SimUtil.REPORT_SECONDS = REPORT_SECONDS.get();
        SimUtil.SECONDS_PRECISION = (int)SECONDS_PRECISION.get();

        simulation = Defaults.getSimulation(SIMULATION.get());
        simulation.process(options, args);

        ShutdownThread shutdownThread = new ShutdownThread();
        Runtime.getRuntime().addShutdownHook(shutdownThread);
        printSimHeader();
        try {
            startms = System.currentTimeMillis();
            simulation.start();
            simulation.join();
        } catch (Throwable t) {
            exitSimulation(t);
            Runtime.getRuntime().removeShutdownHook(shutdownThread);
        } finally {
            exitSimulation(null);
            Runtime.getRuntime().removeShutdownHook(shutdownThread);
        }
    }

    private void exitSimulation(Throwable thrown) {
        synchronized (this) {
            if (!reported) {
                reported = true;
                report(thrown);
            }
        }
    }

    private void report(Throwable thrown) {
        long delta = System.currentTimeMillis() - startms;
        try {
            if (thrown != null) throw thrown;
        } catch (BreakPointException e) {
            Terminal.printYellow("Simulation terminated");
            Terminal.println(": breakpoint at " + StringUtil.addrToString(e.address) + " reached.");
        } catch (TimeoutException e) {
            Terminal.printYellow("Simulation terminated");
            Terminal.println(": timeout reached at pc = " + StringUtil.addrToString(e.address) + ", time = " + e.state.getCycles());
        } catch (AsynchronousExit e) {
            Terminal.printYellow("Simulation terminated asynchronously");
            Terminal.nextln();
        } catch (Util.Error e) {
            Terminal.printRed("Simulation terminated");
            Terminal.print(": ");
            e.report();
        } catch (Throwable t) {
            Terminal.printRed("Simulation terminated with unexpected exception");
            Terminal.print(": ");
            t.printStackTrace();
        } finally {
            TermUtil.printSeparator();
            reportTime(simulation, delta, THROUGHPUT.get());
            reportMonitors(simulation);
        }
    }

    /**
     * The <code>getLocationList()</code> method is to used to parse a list of program locations and turn them
     * into a list of <code>Main.Location</code> instances.
     *
     * Note: currently not used.
     * 
     * @param program the program to look up labels in
     * @param locs       the list of strings that are program locations
     * @return a list of program locations
     */
    public static List<Location> getLocationList(Program program, List<String> locs) {
        HashSet<Location> locset = new HashSet<Location>(locs.size()*2);

        SourceMapping lm = program.getSourceMapping();

        for (String val : locs) {
            SourceMapping.Location l = lm.getLocation(val);
            if ( l == null )
                Util.userError("Label unknown", val);
            locset.add(l);
        }

        List<Location> loclist = Collections.list(Collections.enumeration(locset));
        Collections.sort(loclist, SourceMapping.LOCATION_COMPARATOR);

        return loclist;
    }

    /**
     * The <code>printSimHeader()</code> method simply prints the first line of output that names
     * the columns for the events outputted by the rest of the simulation.
     */
    protected static void printSimHeader() {
        TermUtil.printSeparator(Terminal.MAXLINE, "Simulation events");
        Terminal.printGreen("Node          Time   Event");
        Terminal.nextln();
        TermUtil.printThinSeparator(Terminal.MAXLINE);
    }

    protected static void reportMonitors(Simulation sim) {
        Iterator<Simulation.Node> i = sim.getNodeIterator();
        while (i.hasNext()) {
            for (Monitor mon : i.next().getMonitors()) {
                mon.report();
            }
        }
    }

    protected static void reportTime(Simulation sim, long diff, boolean throughput) {
        // calculate total throughput over all threads
        Iterator<Simulation.Node> i = sim.getNodeIterator();
        long aggCycles = 0;
        long maxCycles = 0;
        while ( i.hasNext() ) {
            Simulation.Node n = i.next();
            Simulator simulator = n.getSimulator();
            if ( simulator == null ) continue;
            long count = simulator.getClock().getCount();
            aggCycles += count;
            if ( count > maxCycles ) maxCycles = count;
        }
        TermUtil.reportQuantity("Simulated time", maxCycles, "cycles");
        if (throughput) {
            TermUtil.reportQuantity("Time for simulation", TimeUtil.milliToSecs(diff), "seconds");
            int nn = sim.getNumberOfNodes();
            double thru = ((double)aggCycles) / (diff * 1000);
            TermUtil.reportQuantity("Total throughput", (float)thru, "mhz");
            if ( nn > 1 )
                TermUtil.reportQuantity("Throughput per node", (float)(thru / nn), "mhz");
        }
    }

    /**
     * The <code>BreakPointException</code> is an exception that is thrown by the simulator before it executes
     * an instruction which has a breakpoint. When this exception is thrown within the simulator, the
     * simulator is left in a state where it is ready to be resumed where it left off by the
     * <code>start()</code> method. When resuming, the breakpointed instruction will not cause a second
     * <code>BreakPointException</code> until the the instruction is executed a second time.
     *
     * @author Ben L. Titzer
     */
    public static class BreakPointException extends RuntimeException {
        
        private static final long serialVersionUID = 1L;

        /**
         * The <code>address</code> field stores the address of the instruction that caused the breakpoint.
         */
        public final int address;

        /**
         * The <code>state</code> field stores a reference to the state of the simulator when the breakpoint
         * occurred, before executing the instruction.
         */
        public final State state;

        public BreakPointException(int a, State s) {
            super("breakpoint @ " + StringUtil.addrToString(a) + " reached");
            address = a;
            state = s;
        }
    }

    /**
     * The <code>TimeoutException</code> is thrown by the simulator when a timeout reaches zero. Timeouts can
     * be used to ensure termination of the simulator during testing, and implementing timestepping in
     * surrounding tools such as interactive debuggers or visualizers.
     * <p/>
     * When the exception is thrown, the simulator is left in a state that is safe to be resumed by a
     * <code>start()</code> call.
     *
     * @author Ben L. Titzer
     */
    public static class TimeoutException extends RuntimeException {

        private static final long serialVersionUID = 1L;

        /**
         * The <code>address</code> field stores the address of the next instruction to be executed after the
         * timeout.
         */
        public final int address;

        /**
         * The <code>state</code> field stores the state of the simulation at the point at which the timeout
         * occurred.
         */
        public final State state;

        /**
         * The <code>timeout</code> field stores the value (in clock cycles) of the timeout that occurred.
         */
        public final long timeout;

        public TimeoutException(int a, State s, long t, String l) {
            super("timeout @ " + StringUtil.addrToString(a) + " reached after " + t + ' ' + l);
            address = a;
            state = s;
            timeout = t;
        }
    }

    public static class AsynchronousExit extends RuntimeException {

        private static final long serialVersionUID = 1L;
    }

    public class ShutdownThread extends Thread {
        public void run() {
            exitSimulation(new AsynchronousExit());
        }
    }
}
