package tmcsim.cadsimulator.managers;

import atmsdriver.ConsoleTrafficDriver;
import atmsdriver.model.Highways;
import atmsdriver.model.LoopDetector;
import atmsdriver.model.TrafficEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.rmi.RemoteException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Properties;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JOptionPane;
import javax.swing.JWindow;
import javax.swing.Timer;
import javax.swing.UIManager;
import tmcsim.cadsimulator.Coordinator;
import tmcsim.cadsimulator.viewer.model.CADSimulatorState;
import tmcsim.client.ATMSBatchDriver;
import tmcsim.client.ATMSBatchViewer;
import tmcsim.common.SimulationException;

/**
 * Traffic Model Manager is a model and controller for the Traffic Model
 * used in the simulation.  It represents all the highways and traffic
 * events that occur.
 * @author jdalbey
 * @version 2.0
 */
public class TrafficModelManager
{
    private final static int ONE_SECOND = 1000;
    private static final int FEPSIM_INTERVAL = 30000;
    private final static SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");

    /**
     * Error Logger.
     */
    private static Logger atmsLogger = Logger.getLogger("tmcsim.cadsimulator.managers");

    /**
     * Enumeration containing property names for Properties parsing.
     *
     * @author Matthew Cechini
     */
    private static enum PROPERTIES
    {
        /**
         *
         */
        HIGHWAYS_MAP_FILE("Highways_Map_File"),
        /**
         *
         */
        FEPSIM_IP_ADDR("FEPSim_IP_addr"),
        /**
         *
         */
        EVENTS_FILE("Events_File"),
        OUTPUT_DEST("Output_Destination");
        
        public String name;

        private PROPERTIES(String n)
        {
            name = n;
        }
    };


    /**
     * Properties Object.
     */
    private Properties atmsProperties = null;
    /**
     * Highways in traffic network
     */
    final private Highways highways;

    /**
     * LinkedList of batch events
     */
    private LinkedList<TrafficEvent> eventQueue;
    /**
     * Map of incidents to events
     */
    private Map<String, List<TrafficEvent>> incidents;

    /**
     * GUI for this driver
     */
    private TrafficModelViewer theView;
    
    /**
     * Constructor. Loads the Properties file and initializes the
     * ATMSCommunicator with the parsed data.
     *
     * @param propertiesFile Target file path of properties file.
     * @param theCoordinator Reference to the simulation Coordinator from whom we get simulation
     * elapsed time.
     */
    public TrafficModelManager(String propertiesFile, final Coordinator theCoordinator) 
            throws SimulationException 
    {
        if (!loadProperties(propertiesFile))
        {
            System.exit(0);
        }
//        final Coordinator theCoordinator = theCoordinator;
        // Initialize the highway model
        incidents = new HashMap<String, List<TrafficEvent>>();
        highways = new Highways(
                atmsProperties.getProperty(PROPERTIES.HIGHWAYS_MAP_FILE.name),
                //"config/vds_data/highways_fullmap.txt",
                //        "192.168.251.46", 8080);  //IP address of FEP Sim Linux VM
                atmsProperties.getProperty(PROPERTIES.FEPSIM_IP_ADDR.name),
                8080); 

        // READ THE BATCH FILE OF COMMANDS and put in a queue
        eventQueue = readBatchFile();
        // Launch the display
        theView = new TrafficModelViewer(this, new ArrayList<String>(incidents.keySet()));
        theView.setVisible(true);
        theView.update("0:00", "1:11", eventQueue);

        // Create a timer that fetches the simulation time every second.
        Timer timer = new Timer(ONE_SECOND, new ActionListener()
        {
            // Every second, see if an event should be launched
            public void actionPerformed(ActionEvent e)
            {
                String currentClock = "";
                String currentATMStime = "";
                Date simClock = new Date();
                // Obtain the simulation time from the CAD server
                try
                {
                    long simtime = theCoordinator.getCurrentSimulationTime();
                    currentClock = formatInterval(simtime);
                    // For Debugging, show the ATMS time
//                    long ATMStime = theCoorInt.getATMStime();       
//                    Date atmsdate = new Date(ATMStime);
//                    currentATMStime = formatter.format(atmsdate);
                    try
                    {
                        simClock = formatter.parse(currentClock);
                    }
                    catch (ParseException ex)
                    {
                        Logger.getLogger(ATMSBatchDriver.class.getName()).log(Level.SEVERE, null, ex);
                        System.out.println("Invalid simulation clock time found in ATMSDriverClient");
                        System.exit(-1);
                    }
                    //System.out.println("Current clock: " + currentClock);
                }
                catch (RemoteException ex)
                {
                    System.out.println("Remote Exception reading sim or ATMS clock time");
                    Logger.getLogger(ATMSBatchDriver.class.getName()).log(Level.SEVERE, null, ex);
                }
                // If we have any events left to process
                if (!eventQueue.isEmpty())
                {
                    // Get the time to launch the next event
                    TrafficEvent nextEvent = eventQueue.peek();
                    Date eventTime = nextEvent.eventDate;
                    //System.out.println("Next event will be launched at: " + formatter.format(eventTime));
                    // Check the queue of events to see if the first
                    // item should be launched.  IF so, 
                    // issue that command and remove it from queue.
                    if (eventTime.before(simClock) || eventTime.equals(simClock))
                    {
                        System.out.println("LAUNCHING EVENT: " + nextEvent.toString());
                        // apply colorization to highways
                        highways.applyColorToHighwayStretch(nextEvent.routeNumber, nextEvent.dir,
                                nextEvent.postmile, nextEvent.range, nextEvent.color);
                        // Remove this event from the queue, we're done with it.
                        eventQueue.remove();
                    }

                    theView.update(currentClock, currentATMStime, eventQueue);
                }
            }
        });
        timer.start();

        if (atmsProperties.getProperty(PROPERTIES.OUTPUT_DEST.name).equals("FEP"))
        {
            // Start the FEP thread (to update ATMS every 30 sec). (See class def below)
            Thread wtfep = new WriteToFEPThread();
            wtfep.start();
        }
        else
        {
            Thread wtConsole = new WriteToConsoleThread();
            wtConsole.start();
        }
        
    }

    /**
     * This method verifies that the CAD Simulator Host and Port values are not
     * null. Also, if a CAD Position or User ID do not exist in the properties
     * file, the user is prompted to enter values. These values are written to
     * the properties file. If the user cancels the process of entering these
     * values, the verification fails.
     *
     * @param propertiesFile File path (absolute or relative) to the properties
     * file containing configuration data.
     * @return True if the properties file is valid, false if not.
     * @throws SimulationException if there is an exception in verifying the
     * properties file, or if the user cancels input.
     */
    private boolean loadProperties(String propertiesFile)
            throws SimulationException
    {
        // Load the properties file.
        try
        {
            atmsProperties = new Properties();
            atmsProperties.load(new FileInputStream(propertiesFile));

        } catch (Exception e)
        {
            atmsLogger.logp(Level.SEVERE, "TrafficModelManager", "Constructor",
                    "Exception in parsing properties file.", e);
            throw new SimulationException(SimulationException.INITIALIZE_ERROR,
                    e);            
        }


        // Ensure that the properties file does not have null values for the
        // required information.
        if (atmsProperties.getProperty(PROPERTIES.HIGHWAYS_MAP_FILE.name) == null
                || atmsProperties.getProperty(PROPERTIES.FEPSIM_IP_ADDR.name) == null
                || atmsProperties.getProperty(PROPERTIES.EVENTS_FILE.name) == null
                || atmsProperties.getProperty(PROPERTIES.OUTPUT_DEST.name) == null)
        {
            atmsLogger.logp(Level.SEVERE, "TrafficModelManager",
                    "Constructor", "Null value in properties file.");
            throw new SimulationException(SimulationException.INITIALIZE_ERROR);
        }

        return true;
    }
    /**
     * Read a file of traffic events.  
     * @return the chronologically ordered list of events
     */
    // Method is package private to facilitate unit testing.
    LinkedList<TrafficEvent> readBatchFile()
    {
        FileInputStream fis;
        LinkedList<TrafficEvent> eventList = new LinkedList<TrafficEvent>();
        try
        {
            fis = new FileInputStream(
                    //"config/vds_data/atmsBatchEvents.txt");
            atmsProperties.getProperty(PROPERTIES.EVENTS_FILE.name));
            // Read all lines from the file of events
            Scanner scan = new Scanner(fis);
            while (scan.hasNext())
            {
                // Read a line and add it to the event queue
                String line = scan.nextLine().trim();
                if (line.charAt(0) != '#')
                {
                    TrafficEvent evt;
                    try
                    {
                        evt = new TrafficEvent(line);
                        eventList.add(evt);
                        String incident = evt.incident;
                        // Add the line to the list for the corresponding incident
                        List evtList;
                        if (incidents.containsKey(evt.incident))
                        {
                            evtList = incidents.get(evt.incident);
                        }
                        else
                        {
                            evtList = new ArrayList<String>();
                        }
                        evtList.add(evt);
                        // and put it back in the map
                        incidents.put(incident, evtList);
                    }
                    catch (ParseException ex)
                    {
                        Logger.getLogger(ATMSBatchDriver.class.getName()).log(Level.SEVERE, null, ex);
                        System.out.println("Wrong format data in batch event file: " + line + " \nskipping.");
                        System.out.println("Skipping badly formatted event.");
                    }
                }
            }
        }
        catch (FileNotFoundException ex)
        {
            Logger.getLogger(ATMSBatchDriver.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.out.println("Events file read, " + eventList.size() + " events queued.");
        // Put the events in chronological order
        Collections.sort(eventList);
        return eventList;
    }

    /**
     * Clear an incident. For each event associated with an incident, turn the
     * dots in its range Green and remove it from the event queue.
     *
     * @param incidentNumber incident to be cleared.
     */
    public void clearIncident(String incidentNumber)
    {
        boolean ok = incidents.containsKey(incidentNumber);
        if (!ok)
        {
            System.out.println("Sorry, that incident number isn't found.");
            return;
        }
        System.out.println("Clearing incident " + incidentNumber);
        List<TrafficEvent> events = incidents.get(incidentNumber);
        // Process each event associated with this incident 
        for (TrafficEvent event : events)
        {
            System.out.println("Event: " + event + " cleared.");
            eventQueue.remove(event);

            // apply colorization to highways, forcing to green, indicating cleared
            highways.applyColorToHighwayStretch(event.routeNumber, event.dir,
                    event.postmile, event.range, LoopDetector.DOTCOLOR.GREEN);

        }
        // Now refresh the view with the updated queue of events
        theView.update("0:00", "0:00", eventQueue);
    }

    /**
     * Format a time in seconds as HH:MM:SS
     *
     * @param l
     * @return
     */
    private String formatInterval(final long l)
    {
        final long hr = TimeUnit.SECONDS.toHours(l);
        final long min = TimeUnit.SECONDS.toMinutes(l - TimeUnit.HOURS.toSeconds(hr));
        final long sec = TimeUnit.SECONDS.toSeconds(l - TimeUnit.HOURS.toSeconds(hr) - TimeUnit.MINUTES.toSeconds(min));
        return String.format("%02d:%02d:%02d", hr, min, sec);
    }

    /**
     * Construct the CADClient with the properties file path, either from the
     * command line arguments or default.
     *
     * @param args Command line arguments.
     */
    public static void main(String[] args) throws RemoteException
    {
        final String CONFIG_FILE_NAME = "traffic_model_config.properties";        
        if (System.getProperty("CONFIG_DIR") == null)
        {
            System.setProperty("CONFIG_DIR", "config");
        }
        CADSimulatorState theModel = new CADSimulatorState();
        Coordinator theCoordinator = new Coordinator(theModel);
        try
        {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            new TrafficModelManager(System.getProperty("CONFIG_DIR") + System.getProperty("file.separator") + CONFIG_FILE_NAME,
            theCoordinator);

        }
        catch (Exception e)
        {
            atmsLogger.logp(Level.SEVERE, "SimulationManager", "Main",
                    "Error initializing application.");

            JOptionPane.showMessageDialog(new JWindow(), e.getMessage(),
                    "Error - Program Exiting", JOptionPane.ERROR_MESSAGE);

            System.exit(-1);
        }

    }
    class WriteToConsoleThread extends Thread
    {

        public void run()
        {
            System.out.println("WriteToConsole Thread starting.");
            // Run indefinitely
            while (true)
            {
                 // Write the highway network status to the FEP Simulator
                 System.out.println(highways.toString());

                // Wait for FEP Sim to process the data we just sent
                try
                {
                    Thread.sleep(1000);
                }
                catch (InterruptedException ie)
                {
                    ie.printStackTrace();
                }
            }

        }
    }
    
    class WriteToFEPThread extends Thread
    {

        public void run()
        {
            System.out.println("WriteToFEP Thread starting.");
            // Run indefinitely
            while (true)
            {
                try
                {
                    // Write the highway network status to the FEP Simulator
                    highways.writeToFEP();
                }
                catch (SimulationException ex)
                {
                    // Ask user if they want to proceed without FEP Sim connection
                    int reply = JOptionPane.showConfirmDialog(null, "Failed to connect to FEP Sim, proceed anyway?", "Network Failure", JOptionPane.YES_NO_OPTION);
                    if (reply == JOptionPane.NO_OPTION)
                    {
                        System.exit(0);
                    }
                    System.out.println("Skipping writeToFEP...");
                }

                // Wait for FEP Sim to process the data we just sent
                try
                {
                    Thread.sleep(FEPSIM_INTERVAL);
                }
                catch (InterruptedException ie)
                {
                    ie.printStackTrace();
                }
            }

        }
    }
}
