package tmcsim.cadsimulator.managers;

import atmsdriver.GoogleMapAnimator;
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.io.PrintWriter;
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.Vector;
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.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 extends Observable
{
    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 logger = 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"),
        HIGHWAY_STATUS_FILE("Highway_Status_File");
        
        public String name;

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


    /**
     * Properties Object.
     */
    private Properties props = null;
    
    /** 
     * The Coordinator object from which we obtain the simulation clock.
     */ 
    private Coordinator theCoordinator;

    /**
     * Highways in traffic network
     */
    private Highways highways;

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

    /**
     * Current simulation clock time
     */
    private String currentClock = "";
    
    /**
     * Path for writing highway data to Json file.
     */
    private String jsonPath;
    
    /**
     * Constructor. Loads the Properties file and initializes the
     * highway network model. 
     *
     * @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 
    {
        try 
        {
            props = loadProperties(propertiesFile);
            jsonPath = props.getProperty(PROPERTIES.HIGHWAY_STATUS_FILE.name);
            //logger.logp(Level.INFO, "Traffic Manager", "Constructor", 
            //        "Highway network json output: " + jsonPath);
            // Initialize the highway model
            incidents = new HashMap<String, List<TrafficEvent>>();
            highways = new Highways(
                    props.getProperty(PROPERTIES.HIGHWAYS_MAP_FILE.name),
                    props.getProperty(PROPERTIES.FEPSIM_IP_ADDR.name),
                    8080); 
            this.theCoordinator = theCoordinator;
        }
        catch (Exception e) 
        {
            logger.logp(Level.SEVERE, "Traffic Manager", "Constructor",
                    "Exception in parsing properties file.", e);
        }
    }
    /**
     * Load the traffic events and start processing the event queue.
     * Usage: addObserver must be called before calling run.
     */
    public void run()
    {
        loadEvents();

        // 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 currentATMStime = "";
                Date simClock = new Date();
                // Obtain the simulation time from the CAD server
                try
                {
                    long simtime = theCoordinator.getCurrentSimulationTime();
                    currentClock = formatTimeInSeconds(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(TrafficModelManager.class.getName()).log(Level.SEVERE, null, ex);
                        System.out.println("Invalid simulation clock time found");
                        System.exit(-1);
                    }
                }
                catch (RemoteException ex)
                {
                    System.out.println("Remote Exception reading sim or ATMS clock time");
                    Logger.getLogger(TrafficModelManager.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;
                    // 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();
                        setChanged();
                        // Send updated list to view
                        // notifyObservers(getEventQueue());
                        // Notify view it should scroll to next event
                        notifyObservers(new Integer(0));
                    }
                    setChanged();
                    notifyObservers(currentClock);
                }
            }
        });
        timer.start();

        if (props.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();
        }
        // Always write to json for google map display
        Thread wtJson = new WriteToJsonThread();
        wtJson.start();
    }
    /** Accessor to event queue
     * 
     * @return defensive copy of current queue of events
     */
    public LinkedList<TrafficEvent> getEventQueue()
    {
        return new LinkedList<TrafficEvent>(eventQueue);
    }
    public Vector<String> getIncidents()
    {
        return new Vector<String>(incidents.keySet());
    }
    public String getClockTime()
    {
        return currentClock;
    }
    public void loadEvents()
    {
        // Read the text file of events and put in a queue
        FileInputStream fis = null;
        try
        {
            fis = new FileInputStream(props.getProperty(PROPERTIES.EVENTS_FILE.name));
        } catch (FileNotFoundException ex)
        {
            Logger.getLogger(TrafficModelManager.class.getName()).log(Level.SEVERE, null, 
                    "Missing Traffic Events file " + props.getProperty(PROPERTIES.EVENTS_FILE.name));
            System.exit(-1);
        }
        // Read all lines from the file of events
        Scanner fileScanner = new Scanner(fis);        
        eventQueue = readBatchFile(fileScanner);
        // Extract the incidents and create a map
        incidents = createIncidentMap(eventQueue);
        setChanged();
        notifyObservers("-:--");
        setChanged();
        notifyObservers(getIncidents());
        setChanged();
        notifyObservers(getEventQueue());
    }
    /**
     * This method verifies that the needed configuration properties are not
     * null. 
     *
     * @param propertiesFile File path (absolute or relative) to the properties
     * file containing configuration data.
     * @return The Properties loaded
     * @throws SimulationException if there is an exception in verifying the
     * properties file
     */
    public static Properties loadProperties(String propertiesFile)
            throws SimulationException
    {
        Properties props;
        // Load the properties file.
        try
        {
            props = new Properties();
            props.load(new FileInputStream(propertiesFile));

        } catch (Exception e)
        {
            throw new SimulationException(SimulationException.INITIALIZE_ERROR);
        }

        // Ensure that the properties file does not have null values for the
        // required information.
        if (props.getProperty(PROPERTIES.HIGHWAYS_MAP_FILE.name) == null
                || props.getProperty(PROPERTIES.FEPSIM_IP_ADDR.name) == null
                || props.getProperty(PROPERTIES.EVENTS_FILE.name) == null
                || props.getProperty(PROPERTIES.OUTPUT_DEST.name) == null)
        {
            System.out.println("Missing property value in "+propertiesFile);
            throw new SimulationException(SimulationException.INITIALIZE_ERROR);
        }

        return props;
    }
    /**
     * Read a file of traffic events.  
     * @param filename the name of the events file
     * @return the chronologically ordered list of events
     */
    public static LinkedList<TrafficEvent> readBatchFile(Scanner scan)
    {
        LinkedList<TrafficEvent> eventList = new LinkedList<TrafficEvent>();
        while (scan.hasNext())
        {
            // Read a line and add it to the event queue
            String line = scan.nextLine().trim();
            // Ignore blank lines and comments
            if (line.length() > 0 && line.charAt(0) != '#')
            {
                TrafficEvent evt;
                try
                {
                    evt = new TrafficEvent(line);
                    eventList.add(evt);
                }
                catch (ParseException ex)
                {
                    Logger.getLogger(TrafficModelManager.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.");
                }
            }
        }
        System.out.println("Events file read, " + eventList.size() + " events queued.");
        // Put the events in chronological order
        Collections.sort(eventList);
        return eventList;
    }

    static Map<String, List<TrafficEvent>> createIncidentMap(LinkedList<TrafficEvent> eventList)
    {
        Map<String, List<TrafficEvent>> incidents = new HashMap<String, List<TrafficEvent>>();
        for (TrafficEvent evt: eventList)
        {
            // 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(evt.incident, evtList);
        }        
        return incidents;
    }
    
    /**
     * 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);

        }
        incidents.remove(incidentNumber);
        // Now refresh the view with the updated queue of events
        setChanged();
        notifyObservers(getEventQueue());
        setChanged();
        notifyObservers(getIncidents());
    }

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

    class WriteToConsoleThread extends Thread
    {

        public void run()
        {
            System.out.println("WriteToConsole Thread starting.");
            // Run indefinitely
            while (true)
            {
                 // Write the highway network status to the Console
                 System.out.println(highways.toString());
                // Pause the thread
                try
                {
                    Thread.sleep(10000);
                }
                catch (InterruptedException ie)
                {
                    ie.printStackTrace();
                }
            }

        }
    }
    /** Writes the highway model to a GeoJson file for reading
     *  by Google Maps.
     */
    class WriteToJsonThread extends Thread
    {

        public void run()
        {
            System.out.println("WriteToJson Thread starting.");
            // Run indefinitely
            while (true)
            {
                 // Write the highway network status to Json
                String geojson = highways.toJson();
                PrintWriter out;
                try
                {
                    // currently writes to local file
                    out = new PrintWriter(jsonPath);
                    out.print(geojson);
                    out.close();
                }
                catch (FileNotFoundException ex)
                {
                    Logger.getLogger(GoogleMapAnimator.class.getName()).log(Level.SEVERE, null, ex);
                }
                // Wait for Google Map to process the data we just sent
                try
                {
                    Thread.sleep(10000);
                }
                catch (InterruptedException ie)
                {
                    ie.printStackTrace();
                }
            }

        }
    }
    
    class WriteToFEPThread extends Thread
    {

        public void run()
        {
            System.out.println("WriteToFEP Thread starting.");
            // Run indefinitely
            boolean running = true;
            while (running)
            {
                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...");
                    running = false;
                }

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

        }
    }
}
