/**
 * 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.sim.types;

import avrora.Defaults;
import avrora.Main;
import avrora.core.*;
import avrora.sim.*;
import avrora.sim.clock.RippleSynchronizer;
import avrora.sim.platform.Platform;
import avrora.sim.platform.PlatformFactory;
import avrora.sim.platform.sensors.*;
import avrora.sim.radio.*;
import cck.text.StringUtil;
import cck.util.*;

import java.io.IOException;
import java.util.*;

/**
 * The <code>SensorSimulation</code> class represents a simulaion type where multiple sensor nodes,
 * each with a microcontroller, sensors, and a radio, are run in parallel. It supports options from
 * the command line that allow a simulation to be constructed with multiple nodes with multiple different
 * programs.
 *
 * @author Ben L. Titzer
 */
public class SensorSimulation extends Simulation {

    public static String HELP = "The sensor network simulation is used for simulating multiple sensor nodes " +
            "simultaneously. These nodes can communicate with each other wirelessly to exchange packets that " +
            "include sensor data and routing information for a multi-hop network. Currently, only the \"mica2\" " +
            "and \"micaz\" platform sensor nodes are supported.";

    public final Option.List NODECOUNT = newOptionList("nodecount", "1",
            "This option is used to specify the number of nodes to be instantiated. " +
            "The format is a list of integers, where each integer specifies the number of " +
            "nodes to instantiate with each program supplied on the command line. For example, " +
            "when set to \"1,2\" one node will be created with the first program loaded onto it, " +
            "and two nodes created with the second program loaded onto them.");
    public final Option.Str TOPOLOGY = newOption("topology", "",
            "This option can be used to specifcy to topology (static, rwp). " +
            "When this option is specified, the radius or free-space radio model will be used " +
            "to model radio propagation.");
    public final Option.Bool LOSSY_MODEL = newOption("lossy-model",false,
            "When this option is set, the radio model takes into account Noise and fadings thus " +
            "implementing in micaz platform the correlation, cca and rssi functions.");
    public final Option.Str NOISE = newOption("Noise", "",
            "This option can be used to specify the name of " +
            "a file that contains a Noise time trace. When this option is specified" +
            "the indoor radio model will be used to model radio propagation.");
    public final Option.Double RANGE = newOption("radio-range", 15.0,
            "This option, when used in conjunction with the -topology option, specifies " +
            "the maximum range for radio communication between nodes. This simple " +
            "idealized radius model will drop all communications between nodes whose " +
            "distance is greater than this threshold value.");
    public final Option.Interval RANDOM_START = newOption("random-start", 0, 0,
            "This option inserts a random delay before starting " +
            "each node in order to prevent artificial cycle-level synchronization. The " +
            "starting delay is pseudo-randomly chosen with uniform distribution over the " +
            "specified interval, which is measured in clock cycles. If the \"random-seed\" " +
            "option is set to a non-zero value, then its value is used as the seed to the " +
            "pseudo-random number generator.");
    public final Option.Long STAGGER_START = newOption("stagger-start", 0,
            "This option causes the simulator to insert a progressively longer delay " +
            "before starting each node in order to avoid artificial cycle-level " +
            "synchronization between nodes. The starting times are staggered by the specified number " +
            "of clock cycles. For example, if this option is given the " +
            "value X, then node 0 will start at time 0, node 1 at time 1*X, node 2 at " +
            "time 2*X, etc.");
    public final Option.List SENSOR_DATA = newOptionList("sensor-data", "",
            "This option accepts a list describing the input data for each sensor node. The format " +
            "for each entry in this list is $sensor:$id:$data, where $sensor is the name of " +
            "the sensor device such as \"light\", $id is the integer ID of the node, and $data is " +
            "the name of a file or the special '.' character, indicating random data. A sensor data " +
            "input file consists of an initial sensor reading which is interpreted as a 10-bit ADC " +
            "result, then a list of time value pairs separated by whitespace; the sensor will continue " +
            "returning the current value until the next (relative) time in seconds, and then the sensor " +
            "will change to the new value. ");
    public final Option.Bool UPDATE_NODE_ID = newOption("update-node-id", true,
            "When this option is set, the sensor network simulator will attempt to update " +
            "the node identifiers stored in the flash memory of the program. For TinyOS programs, " +
            "this identifier is labelled \"TOS_LOCAL_ADDRESS\". For SOS programs, this identifier is " +
            "called \"node_address\". When loading a program onto " +
            "a node, the simulator will search for these labels, and if found, will update the word " +
            "in flash with the node's ID number.");

    class SensorDataInput {
        String sensor;
        String fname;

        void instantiate(Platform p) {
            try {
                Sensor s = (Sensor)p.getDevice(sensor+"-sensor");
                if ( s == null )
                    Util.userError("Sensor device does not exist", sensor);
                if ( ".".equals(fname) ) s.setSensorData(new RandomSensorData(getRandom()));
                else s.setSensorData(new ReplaySensorData(p.getMicrocontroller(), fname));
            } catch ( IOException e) {
                throw Util.unexpected(e);
            }
        }
    }

    /**
     * The <code>SensorNode</code> class extends the <code>Node</code> class of a simulation
     * by adding a reference to the radio device as well as sensor data input. It extends the
     * <code>instantiate()</code> method to create a new thread for the node and to attach the
     * sensor data input.
     */
    protected class SensorNode extends Node {
        Radio radio;
        long startup;
        List<SensorDataInput> sensorInput;

        SensorNode(int id, PlatformFactory pf, LoadableProgram p) {
            super(id, pf, p);
            sensorInput = new LinkedList<SensorDataInput>();
        }

        /**
         * The <code>instantiate()</code> method of the sensor node extends the default simulation node
         * by creating a new thread to execute the node as well as getting references to the radio and
         * adding it to the radio model, adding
         * an optional start up delay for each node, and connecting the node's sensor input to
         * replay or random data as specified on the command line.
         */
        protected void instantiate() {
            createNode();
            updateNodeID();
            addSensorData();
        }

        private void addSensorData() {
            // process sensor data inputs
            for (SensorDataInput sdi : sensorInput) {
                sdi.instantiate(platform);
            }
        }

        private void createNode() {
            thread = new SimulatorThread(this);
            super.instantiate();
            // get the radio device, if it exists.
            Object dev = platform.getDevice("radio");
            if (dev instanceof CC2420Radio) {
                // connect to the cc2420 medium
                CC2420Radio radio = (CC2420Radio)dev;
                this.radio = radio;
                radio.setMedium(createCC2420Medium());
            } else if (dev instanceof CC1000Radio) {
                // connect to the cc1000 medium
                CC1000Radio radio = (CC1000Radio)dev;
                this.radio = radio;
                radio.setMedium(createCC1000Medium());
            }
            simulator.delay(startup);
            if (topology != null) {
                setNodePosition();
                return;
            }
        }

        private Medium createCC2420Medium() {
            if (cc2420_medium == null) {
                createRadioModel();
                if (LOSSY_MODEL.get()){
                    return cc2420_medium = CC2420Radio.createMedium(synchronizer, lossyModel);
                }else{
                    return cc2420_medium = CC2420Radio.createMedium(synchronizer, radiusModel);
                }
            }
            return cc2420_medium;
        }

        private Medium createCC1000Medium() {
            if (cc1000_medium == null) {
                createRadioModel();
                if (LOSSY_MODEL.get()){
                    return cc1000_medium = CC1000Radio.createMedium(synchronizer, lossyModel);
                }else{
                    return cc1000_medium = CC1000Radio.createMedium(synchronizer, radiusModel);
                }
            }
            return cc1000_medium;
        }
        
        private void createRadioModel() {
           if (topology != null) {
                if (LOSSY_MODEL.get()){
                    lossyModel = new LossyModel();
                }
                else {
                    radiusModel = new RadiusModel(1.0, RANGE.get());
                }
            }
        }


        private void setNodePosition() {
            topology.addNode(this);
            Topology.Position p = topology.getPosition(id);
            if (p != null && radio != null) {
                if (LOSSY_MODEL.get()) {
                    lossyModel.setPosition(radio, p);
                }else{
                    radiusModel.setPosition(radio, p);
                }
            }
        }

        private void updateNodeID() {
            if ( UPDATE_NODE_ID.get() ) {
                Program p = path.getProgram();
                SourceMapping smap = p.getSourceMapping();
                if ( smap != null ) {
                    updateVariable(smap, "TOS_LOCAL_ADDRESS", id);          // TinyOS 1.1
                    updateVariable(smap, "node_address", id);               // SOS
                    updateVariable(smap, "TOS_NODE_ID", id);                // Tinyos 2.0
                    updateVariable(smap, "ActiveMessageAddressC$addr", id); // Tinyos 2.0
                    updateVariable(smap, "ActiveMessageAddressC__addr", id); // Tinyos 2.1.1 with nesC 1.3.1
                }
            }
        }

        private void updateVariable(SourceMapping smap, String name, int value) {
            SourceMapping.Location location = smap.getLocation(name);
            if ( location != null ) {
                AtmelInterpreter bi = (AtmelInterpreter)simulator.getInterpreter();
                bi.writeFlashByte(location.lma_addr, Arithmetic.low(value));
                bi.writeFlashByte(location.lma_addr +1, Arithmetic.high(value));
            }
        }

        /**
         * The <code>remove()</code> method removes this node from the simulation. This method extends the
         * default simulation remove method by removing the node from the radio air implementation.
         */
        protected void remove() {
            synchronizer.removeNode(this);
        }
    }

    Topology topology;
    noise noise;
    LossyModel lossyModel;
    RadiusModel radiusModel;
    Medium cc2420_medium;
    Medium cc1000_medium;
    long stagger;

    public SensorSimulation() {
        super("sensor-network", HELP, null);
        addSection("SENSOR NETWORK SIMULATION OVERVIEW", help);
        addOptionSection("This simulation type supports simulating multiple sensor network nodes that communicate " +
                "with each other over radios. There are options to specify how many of each type of sensor node to " +
                "instantiate, as well as the program to be loaded onto each node, and an optional topology file " +
                "that describes the physical layout of the sensor network. Also, each node's sensors can be " +
                "supplied with random or replay sensor data through the \"sensor-data\" option.", options);

        PLATFORM.setNewDefault("micaz");       // set the new default monitors
        MONITORS.setNewDefault("leds,packet"); // set the new default monitors
    }

    /**
     * The <code>newNode()</code> method creates a new node in the simulation. In this implementation,
     * a <code>WiredNode</code> is created that contains, in addition to the simulator, ID, program, etc,
     * a reference to the <code>Radio</code> instance for the node and a <code>SimulatorThread</code> for
     * the node.
     * @param id the integer identifier for the node
     * @param pf the platform factory to use to instantiate the node
     * @param p the program to load onto the node
     * @return a new instance of the <code>SensorNode</code> class for the node
     */
    public Node newNode(int id, PlatformFactory pf, LoadableProgram p) {
        return new SensorNode(id, pf, p);
    }

    /**
     * The <code>process()</code> method processes options and arguments from the command line. In this
     * implementation, this method accepts multiple programs from the command line as arguments as well
     * as options that describe how many of each type of node to instantiate.
     * @param o the options from the command line
     * @param args the arguments from the command line
     * @throws Exception if there is a problem loading any of the files or instantiating the simulation
     */
    public void process(Options o, String[] args) throws Exception {
        options.process(o);
        processMonitorList();

        if ( args.length == 0 )
            Util.userError("Simulation error", "No program specified");
        Main.checkFilesExist(args);
        PlatformFactory pf = getPlatform();

        // build the synchronizer
        synchronizer = new RippleSynchronizer(100000, null);

        // create the topology
        processTopology();
        
        // create the nodes based on arguments
        createNodes(args, pf);

        // process the sensor data input option
        processSensorInput();

        //create Noise time trace
        createNoise();
    }

    private void createNodes(String[] args, PlatformFactory pf) throws Exception {
        Iterator<String> i = NODECOUNT.get().iterator();
        for ( int arg = 0; arg < args.length; arg++ ) {
            int count = i.hasNext() ? StringUtil.evaluateIntegerLiteral((String)i.next()) : 1;
            LoadableProgram lp = new LoadableProgram(args[arg]);
            lp.load();

            // create a number of nodes with the same program
            for (int node = 0; node < count; node++) {
                SensorNode n = (SensorNode)createNode(pf, lp);
                long r = processRandom();
                long s = processStagger();
                n.startup = r + s;
            }
        }
    }
        private void createNoise() throws Exception {
            if (noise == null && !NOISE.isBlank()) {
                    noise = new noise(NOISE.get());
            }else if (noise == null && NOISE.isBlank()){
                    noise = new noise();
            }
    }

    private void processSensorInput() {
       for (String str : SENSOR_DATA.get()) {
            int ind = str.indexOf(':');
            if ( ind <= 0 )
                Util.userError("Sensor data format error", str);
            String sensor = str.substring(0, ind);
            String rest = str.substring(ind+1);
            int ind2 = rest.indexOf(':');
            if ( ind2 <= 0 )
                Util.userError("Sensor data format error", str);
            String id = rest.substring(0, ind2);
            String file = rest.substring(ind2+1);

            addSensorData(id, file, sensor);
        }
    }

    private void addSensorData(String id, String file, String sensor) {
        int num = StringUtil.evaluateIntegerLiteral(id);
        SensorNode node = (SensorNode)getNode(num);
        if ( node != null ) {
            SensorDataInput sdi = new SensorDataInput();
            sdi.fname = file;
            sdi.sensor = sensor;
            node.sensorInput.add(sdi);
            if (! ".".equals(file) )
                Main.checkFileExists(file);
        }
    }

    long processRandom() {
        long low = RANDOM_START.getLow();
        long size = RANDOM_START.getHigh() - low;
        long delay = 0;
        if (size > 0) {
            Random r = getRandom();
            delay = r.nextLong();
            if (delay < 0) delay = -delay;
            delay = delay % size;
        }

        return (low + delay);
    }

    long processStagger() {
        long st = stagger;
        stagger += STAGGER_START.get();
        return st;
    }

    private void processTopology() {
        if (!TOPOLOGY.isBlank()) {
            topology = Defaults.getTopology(TOPOLOGY.get());
            // Util.userError("No such topology model", TOPOLOGY.get());
            topology.processOptions(options);
            topology.start();
        }
    }
}