package org.lucci.madhoc.simulation;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Vector;

import org.lucci.madhoc.network.Application;
import org.lucci.madhoc.network.Connection;
import org.lucci.madhoc.network.Network;
import org.lucci.madhoc.network.Station;
import org.lucci.madhoc.simulation.measure.MeasureHistory;
import org.lucci.madhoc.simulation.measure.MeasureUtility;
import org.lucci.madhoc.simulation.measure.Sensor;
import org.lucci.madhoc.simulation.projection.Projection;
import org.lucci.util.Timer;

public class MadhocSimulation extends Simulation implements Configurable
{
    // the target network of the simulation
    private Network network;

    private boolean enableMobileStationLocation = true;

    private boolean enableStationActivity = true;

    private boolean supportExitAndJoin = false;

    private boolean enableMeasures = true;

    private int garbageCollectionIterations = 10;

    private long measurementDuration = -1;

    private long mobilityUpdateDuration = -1;

    private long connectionsUpdateDuration = -1;

    private long stationActivityDuration = -1;

    private long messageTransferDuration = -1;

    private Collection<SimulationListener> simulationListeners = new Vector<SimulationListener>();

    private boolean mobility_write_xy_files;

    public Collection<SimulationListener> getSimulationListeners()
    {
        return simulationListeners;
    }

    public Collection<Monitor> findRunningApplications()
    {
        Collection<Monitor> v = new Vector<Monitor>();

        for (Monitor monitor : getNetwork().getSimulationApplicationMap().values())
        {
            if (!monitor.hasCompleted())
            {
                v.add(monitor);
            }
        }

        return v;
    }

    /**
     * @return Returns the enableMeasures.
     */
    public boolean isEnableMeasures()
    {
        return enableMeasures;
    }

    /**
     * @param enableMeasures
     *            The enableMeasures to set.
     */
    public void setEnableMeasures(boolean enableMeasures)
    {
        this.enableMeasures = enableMeasures;
    }

    /**
     * @return Returns the enableMobileStationLocation.
     */
    public boolean isEnableMobileStationLocation()
    {
        return enableMobileStationLocation;
    }

    /**
     * @param enableMobileStationLocation
     *            The enableMobileStationLocation to set.
     */
    public void setEnableMobileStationLocation(boolean enableMobileStationLocation)
    {
        this.enableMobileStationLocation = enableMobileStationLocation;
    }

    /**
     * @return Returns the enableStationActivity.
     */
    public boolean isEnableStationActivity()
    {
        return enableStationActivity;
    }

    /**
     * @param enableStationActivity
     *            The enableStationActivity to set.
     */
    public void setEnableStationActivity(boolean enableStationActivity)
    {
        this.enableStationActivity = enableStationActivity;
    }

    public void iterate()
    {
        iterate(true);
    }

    public void iterate(boolean reset)
    {
        System.out.println(getIteration());
        for (SimulationListener l : getSimulationListeners())
            l.iterationStarting(this);

        if (getIteration() == 0)
        {
            setStartDate(System.currentTimeMillis());
        }

        Timer timer = new Timer();

        if (getIteration() % getGarbageCollectionIteration() == 0)
        {
            System.gc();
        }

        if (isEnableMobileStationLocation())
        {
            for (SimulationListener l : getSimulationListeners())
                l.beforeTheStationsMove(this);
            timer.initialize();

            for (Station station : getNetwork().getStations())
            {
                station.getMobilityModel().moveStation(getResolution(), getSimulatedTime());

                if (this.mobility_write_xy_files)
                {
                    station.getMobilityModel().saveLocation();
                }
            }

            mobilityUpdateDuration = timer.getElapsedMillis();
            for (SimulationListener l : getSimulationListeners())
                l.afterTheStationsMove(this);

            for (Station station : getNetwork().getStations())
            {
                if (station.getVolatilityModel() != null)
                {
                    station.setSwitchedOn(station.getVolatilityModel().stationIsEnabled());
                }
            }

            Collection<Connection> removedConnections = getNetwork().removeInvalidConnections();
            for (SimulationListener l : getSimulationListeners())
                l.connectionsHaveVanished(this, removedConnections);

            Collection<Connection> addedConnections = getNetwork().createNewConnections();
            for (SimulationListener l : getSimulationListeners())
                l.connectionsHaveAppeared(this, addedConnections);

            connectionsUpdateDuration = timer.getElapsedMillis();
        }
        else
        {
            mobilityUpdateDuration = -1;
            connectionsUpdateDuration = -1;
        }

        if (isEnableStationActivity())
        {
            for (SimulationListener l : getSimulationListeners())
                l.beforeStationsDo(this);
            timer.initialize();

            // Lock lock = new Lock();
            //            
            // for (int i = getNumberOfThreads(); i > 0; --i)
            // {
            // SimulationThread thread = new SimulationThread();
            // }
            //            

            giveLifeToStations();
            stationActivityDuration = timer.getElapsedMillis();
            for (SimulationListener l : getSimulationListeners())
                l.afterStationsDo(this);
        }

        // System.out.println("\texecute applications " +
        // timer.getElapsedMillis());
        for (SimulationListener l : getSimulationListeners())
            l.beforeDataTransfer(this);
        timer.initialize();
        getNetwork().flushOutgoingMessageQueues();
        messageTransferDuration = timer.getElapsedMillis();
        for (SimulationListener l : getSimulationListeners())
            l.afterDataTransfer(this);

        for (SimulationListener l : getSimulationListeners())
            l.beforeSensing(this);

        // we can then take the measures
        timer.initialize();
        setIteration(getIteration() + 1);

        if (isEnableMeasures())
        {
            takeMesures();
        }

        this.measurementDuration = timer.getElapsedMillis();
        for (SimulationListener l : getSimulationListeners())
            l.afterSensing(this);
        for (SimulationListener l : getSimulationListeners())
            l.iterationHasCompleted(this);

        // then reset the values relative to the current iteration
        // they have to be clean for the next iteration
        if (reset)
        {
            resetIterationScopedValues();
        }

        // measure the efficiency of the simulation
        {
            ++iterationCounter;
            double iterationCountDuration = System.currentTimeMillis() - iterationCountStartDate;

            // if iterations have been counted for more than one second,
            // which is sufficient to get a good measure
            if (iterationCountDuration >= 1000)
            {
                setIterationFrequency((1000d * iterationCounter) / iterationCountDuration);
                iterationCountStartDate = System.currentTimeMillis();
                iterationCounter = 0;
            }
        }
    }

    /**
     * 
     */
    public void resetIterationScopedValues()
    {
        getNetwork().resetIterationScopedValues();

        for (Monitor monitor : getNetwork().getMonitorMap().values())
        {
            monitor.resetIterationScopedValues();
        }
    }

    public Network getNetwork()
    {
        return network;
    }

    /**
     * @param network
     *            The network to set.
     */
    public void setNetwork(Network network)
    {
        this.network = network;
    }

    public int convertBytesPerIterationToKilobitsPerSecond(int byteCount)
    {
        return (int) (getResolution() * byteCount / 100);
    }

    private void giveLifeToStations()
    {
        List<Application> stationApplications = new Vector<Application>(getNetwork().getStationApplications());
        Collections.shuffle(stationApplications, getRandomNumberGenerator().getRandom());

        for (Application app : stationApplications)
        {
            app.doIt(getResolution());
        }
    }

    public long getConnectionsUpdateDuration()
    {
        return connectionsUpdateDuration;
    }

    public long getMeasurementDuration()
    {
        return measurementDuration;
    }

    public long getMessageTransferDuration()
    {
        return messageTransferDuration;
    }

    public long getMobilityUpdateDuration()
    {
        return mobilityUpdateDuration;
    }

    public long getStationActivityDuration()
    {
        return stationActivityDuration;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.lucci.madhoc.util.Configurable#configure(org.lucci.madhoc.util.Configuration)
     */
    public void configure() throws Throwable
    {
        super.configure();
        // creates the network, which is initially empty
        Network net = new Network();
        net.setSimulation(this);
        net.configure();
        setNetwork(net);

        this.mobility_write_xy_files = getConfiguration().getBoolean("mobility_write_xy_files");
        setGarbageCollectionIteration(getConfiguration().getInteger("garbage_collection_iterations"));
        this.simulationListeners.addAll((Collection<SimulationListener>) getConfiguration().getInstantiatedClasses("simulation_listeners"));

        // it is now time to define the applications that will run on the
        // network
        for (Monitor monitor : (Collection<Monitor>) getConfiguration().getInstantiatedClasses("monitors_class"))
        {
            monitor.setNetwork(net);
            monitor.configure();
            getNetwork().deployApplication(monitor);
        }

        if (enableMeasures)
        {
            takeMesures();
        }
    }

    public boolean isSupportExitAndJoin()
    {
        return supportExitAndJoin;
    }

    public void setSupportExitAndJoin(boolean supportExitAndJoin)
    {
        this.supportExitAndJoin = supportExitAndJoin;
    }

    /**
     * 
     */
    private void takeMesures()
    {
        for (Projection projection : getNetwork().getProjectionMap().values())
        {
            for (MeasureHistory repository : projection.getMeasureHistoryMap().values())
            {
                try
                {
                    repository.addValue(repository.getSensor().takeNewValue(projection), getIteration());
                }
                catch (Throwable ex)
                {
                    ex.printStackTrace();
                    repository.addValue(null, getIteration());
                    System.err.println("Warning! Cannot take measure " + repository.getSensor().getName() + " on projection " + projection.getName());
                }
            }

            // in order to allow the projection to be updated, it has to be
            // reinitialized
            projection.setProjectionResult(null);
        }
    }

    public Collection<Sensor> findAllSensors()
    {
        Collection<Sensor> c = new Vector<Sensor>();

        for (Monitor monitor : getNetwork().getMonitorMap().values())
        {
            c.addAll(monitor.getSensorMap().values());
        }

        return c;
    }

    public Sensor findSensor(Class clazz)
    {
        for (Monitor monitor : getNetwork().getMonitorMap().values())
        {
            Sensor sensor = monitor.getSensorMap().get(clazz);

            if (sensor != null)
            {
                return sensor;
            }
        }

        return null;
    }

    public Sensor findSensorByMeasureName(String s)
    {
        for (Sensor sensor : findAllSensors())
        {
            if (sensor.getName().equals(s))
            {
                return sensor;
            }
        }

        return null;
    }

    public int getGarbageCollectionIteration()
    {
        return this.garbageCollectionIterations;
    }

    public void setGarbageCollectionIteration(int garbageCollectionIterations)
    {
        this.garbageCollectionIterations = garbageCollectionIterations;
    }

    public String getOutputAsText()
    {
        try
        {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            PrintStream pos = new PrintStream(bos);
            MeasureUtility.saveNumericalMeasuresToGNUPlotStream(this, pos, true);
            byte[] output = bos.toByteArray();
            pos.close();
            bos.close();
            return new String(output);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
            throw new IllegalStateException();
        }
    }

    public String getOutputAsBytes()
    {
        try
        {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            PrintStream pos = new PrintStream(bos);
            MeasureUtility.saveNumericalMeasuresToGNUPlotStream(this, pos, true);
            byte[] output = bos.toByteArray();
            pos.close();
            bos.close();
            return new String(output);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
            throw new IllegalStateException();
        }
    }

}