package paramsim.paramicssimulator;

import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JOptionPane;

import paramsim.paramicssimulator.ParamicsSimulatorLogMessage.PARAMICS_SIMULATOR_LOG;
import paramsim.paramicssimulator.gui.ParamicsSimulatorGUI;

/**
 * ParamicsSimulator is the main class for this module.  The ParamicsSimulator
 * can be used to simulate the Paramics device.<br><br>  
 * The properties file for the ParamicsCommunicator class contains the following data.<br>
 * <code>
 * -----------------------------------------------------------------------------<br>
 * WorkingDirectory      The working directory of the Paramics Communicator<br>
 * ParamicsStatusLog     The Paramics status log file <br>
 * CameraStatusLog       The camera status log file <br>
 * ExchangeLog			 The exchange log file <br> 
 * CameraControlConfig   The camera control file which maps the camera information <br>
 * DVDPlayersConfig      The DVD Players XML file which contains camera information <br>
 * -----------------------------------------------------------------------------<br>
 * Example File: <br>
 * WorkingDirectory    = c:/paramics/out <br>
 * ParamicsStatusLog   = paramics_status.log <br>
 * CameraStatusLog     = camera_status.log <br>
 * IncidentLog     	   = exchange.log <br>
 * CameraControlConfig = camera_control.txt
 * DVDPlayersConfig    = dvdplayers.xml
 * -----------------------------------------------------------------------------<br>
 * </code>
 *
 * @author Greg Eddington (geddingt@calpoly.edu)
 */
public class ParamicsSimulator extends Observable implements Runnable
{
	/** Error Log **/
	private static Logger paramLogger = Logger.getLogger("paramsim.paramicssimulator");
	
	/** Incident file **/
	private static final String INCIDENT_FILE = "exchange.xml";
	
	/** Paramics Status file **/
	private static final String PARAM_STATUS_FILE = "paramics_status.xml";
	
	/** Camera Status file **/
	private static final String CAMERA_STATUS_FILE = "camera_status.xml";
	
	/** Sleep Time (10 seconds) **/
	private static final int SLEEP_TIME = 10000;
	
	/**
	 * Enumeration containing property names.
	 * @author Greg Eddington
	 */
	private static enum PROPERTIES 
	{
		/** The working directory for Paramics **/
		WORKING_DIR ("WorkingDirectory"),
		
		/** The Paramics log **/
		PARAM_STATUS_LOG ("ParamicsStatusLog"),
		
		/** The camera status log **/
		CAMERA_STATUS_LOG ("CameraStatusLog"),
		
		/** The exchange log **/
		INCIDENT_LOG ("IncidentLog"),
		
		/** The camera control file **/
		CAMERA_CONTROL ("CameraControlConfig"),
		
		/** The still images XML file **/
		DVD_PLAYERS ("DVDPlayersConfig");
		
		public String name;
		
		private PROPERTIES(String n) 
		{
			name = n;
		}
	}
	
	/** 
	 * Enumeration of network statuses.
	 * @author Greg Eddington
	 **/
	public static enum NETWORK_STATUS
	{
		LOADING ("LOADING"),
		WARMING ("WARMING"),
		LOADED ("LOADED"),
		UNKNOWN ("UNKNOWN");
		
		public String message;
		
		private NETWORK_STATUS(String n) 
		{
			message = n;
		}
	}
	
	/** Properties object. */
	private Properties paramicsSimProp = null;	
	
	/** Working Directory **/
	private String workingDirectory = null;
	
	/** Incidents File **/
	private String incidentFile = null;
	
	/** Paramics Status File **/
	private String paramStatusFile = null;
	
	/** Camera Status File **/
	private String cameraStatusFile = null;

	/** Incidents Log **/
	private String incidentLog = null;
	
	/** Paramics Status Log **/
	private String paramStatusLog = null;

	/** Camera Status Log **/
	private String cameraStatusLog = null;
	
	/** Incident Reader **/
	private ParamicsIncidentReader incidentReader = null;
	
	/** The paramics simulator GUI **/
	private ParamicsSimulatorGUI theGUI;
	
	/** The network ID **/
	private int networkId = 1;
	
	/** Simulation Cameras **/
	private List<SimulationCamera> cameras;
	
	/**
	 * Creates a ParamicsSimulator object with properties defined in the propertiesFile
	 * parameter.  It will initialize all file locations.
	 */
	public ParamicsSimulator(String propertiesFile) 
	{		
		try 
		{
			paramicsSimProp = new Properties();
			paramicsSimProp.load(new FileInputStream(propertiesFile));
			
			/** Check property file **/
			for (PROPERTIES property : PROPERTIES.values())
			{
				if (paramicsSimProp.getProperty(property.name) == null || 
					paramicsSimProp.getProperty(property.name).length() == 0) 
				{				
					System.err.println("Invalid Configuration: Properties file missing " +
						property.name + " property.");
					System.exit(0);
				}
			}
			
			// Directory
			workingDirectory = paramicsSimProp.getProperty(PROPERTIES.WORKING_DIR.name).trim();			
			boolean endsWithSlash = workingDirectory.charAt(workingDirectory.length()-1) == '/' || 
				workingDirectory.charAt(workingDirectory.length()-1) == '\\';
			
			// Files
			paramStatusFile = workingDirectory + (endsWithSlash ? "" : "/") + PARAM_STATUS_FILE;
			cameraStatusFile = workingDirectory + (endsWithSlash ? "" : "/") + CAMERA_STATUS_FILE;
			incidentFile = workingDirectory + (endsWithSlash ? "" : "/") + INCIDENT_FILE;
			
			// Logs
			incidentLog = paramicsSimProp.getProperty(PROPERTIES.INCIDENT_LOG.name).trim();
			paramStatusLog = paramicsSimProp.getProperty(PROPERTIES.PARAM_STATUS_LOG.name).trim();
			cameraStatusLog = paramicsSimProp.getProperty(PROPERTIES.CAMERA_STATUS_LOG.name).trim();
			
			// Delete any old logs
			File ilFile = new File(incidentLog);
			File pslFile = new File(paramStatusLog);
			File cslFile = new File(cameraStatusLog);
			ilFile.delete();
			pslFile.delete();
			cslFile.delete();
			
			// Cameras
			String cameraConfig = paramicsSimProp.getProperty(PROPERTIES.CAMERA_CONTROL.name).trim();
			String stillImages = paramicsSimProp.getProperty(PROPERTIES.DVD_PLAYERS.name).trim();
			try
			{
				// Load the cameras
				cameras = CameraParser.loadCameras(stillImages, cameraConfig);
			}
			catch (IOException e)
			{
				// Create an empty camera array
				cameras = new ArrayList<SimulationCamera>();
				
				// Log Error
				paramLogger.logp(Level.SEVERE, "ParamicsSimulator", "Constructor", 
						"Error loading camera config from files \"" + stillImages +
						"\" and \"" + cameraConfig + "\".", e);
			}
			
			// Initialize GUI
			theGUI = new ParamicsSimulatorGUI(this);
			addObserver(theGUI);
			theGUI.addWindowListener
			(
				new WindowListener() 
				{			
					public void windowActivated(WindowEvent arg0) {};
					public void windowClosed(WindowEvent arg0) {};
					public void windowClosing(WindowEvent arg0) { System.exit(0); };
					public void windowDeactivated(WindowEvent arg0) {};
					public void windowDeiconified(WindowEvent arg0) {};
					public void windowIconified(WindowEvent arg0) {};
					public void windowOpened(WindowEvent arg0) {};
				}
			);
			
			// Write an initial packets
			writeParamicsStatus(NETWORK_STATUS.LOADING);
			writeCameraStatus(cameras);
		} 
		catch (Exception e) 
		{
			paramLogger.logp(Level.SEVERE, "ParamicsSimulator", "Constructor", 
					"Exception in reading properties file.", e);
		}
		
		incidentReader = new ParamicsIncidentReader();
	}
	
	/**
	 * Appends a message to a log file.
	 * @param logName The name of the log file.
	 * @param message The message to append.
	 */
	private void writeToLog(String logName, String message)
	{
		try
		{
			FileWriter w = new FileWriter(logName, true);
			w.write(message + "\n\n");
			w.close();
		}
		catch (IOException e)
		{
			paramLogger.logp(Level.WARNING, "ParamicsSimulator", "writeToLog", 
				"Error writing to file " + logName + ".", e);
		}
	}
	
	/**
	 * Runs the Paramics simulator thread.
	 */
	public void run() 
	{
		// Check for packets and update the simulator
		for(;;)
		{
			// Flush the input file
			ParamicsSimulationInfo psi = incidentReader.parse(incidentFile);
			
			// Update the network ID if a packet is received
			if (psi != null)
			{
				networkId = psi.networkId;
				writeToLog(incidentLog, psi.xmlMessage);
				setChanged();
				notifyObservers(new ParamicsSimulatorLogMessage(PARAMICS_SIMULATOR_LOG.EXCHANGE, psi.xmlMessage));
			}
			
			// Write camera status
			writeCameraStatus(cameras);
			
			// Sleep
			try
			{
				Thread.sleep(SLEEP_TIME);
			}
			catch (InterruptedException ie) {}
		}
	}

	/**
	 * Writes the camera status to the camera status XML file read by the Paramics Communicator,
	 * as well as to the camera status log file.  All observers are notified with the new status,
	 * so they can also log it.
	 * 
	 * @param id Camera ID
	 * @param route Route
	 * @param direction The direction of traffic
	 * @param postmile The postmile
	 * @param averageSpeedNE The average speed North or East
	 * @param averageSpeedSW The average speed South or West
	 */
	public void writeCameraStatus(List<SimulationCamera> cameras)
	{
		StringBuilder message = new StringBuilder("<Camera_Status>\n");
		for (SimulationCamera camera : cameras)
		{
			message.append
			(
				"   <Camera>\n" +
				"      <Identifier>" + camera.id + "</Identifier>\n" +
				"      <Route>" + camera.route + "</Route>\n" + 
				"      <Direction>" + camera.direction.xml + "</Direction>\n" +
				"      <Postmile>" + camera.postmile + "</Postmile>\n" + 
				"      <Ave_Speed_NE>" + camera.averageSpeedNE + "</Ave_Speed_NE>\n" +
				"      <Ave_Speed_SW>" + camera.averageSpeedSW + "</Ave_Speed_SW>\n" +
				"   </Camera>\n"
			);
		}
		message.append("</Camera_Status>");
		
		try
		{
			FileWriter w = new FileWriter(cameraStatusFile);
			w.write(message.toString());
			w.close();
		}
		catch (IOException e)
		{
			paramLogger.logp(Level.SEVERE, "ParamicsSimulator", "writeCameraStatus", 
				"Error writing to file " + cameraStatusFile + ".", e);
		}

		writeToLog(cameraStatusLog, message.toString());
		
		setChanged();
		notifyObservers(new ParamicsSimulatorLogMessage(PARAMICS_SIMULATOR_LOG.CAMERA_STATUS, message.toString()));	
	}
	
	/**
	 * Writes the Paramics status to the camera status XML file read by the Paramics Communicator,
	 * as well as to the Paramics status log file.  All observers are notified with the new status,
	 * so they can also log it.
	 * 
	 * @param status The current Paramics status
	 */
	public void writeParamicsStatus(NETWORK_STATUS status)
	{
		String message = "<Paramics>\n   <Network_Status>" + status.message +
						 "</Network_Status>\n   <Network_ID>" + networkId + "</Network_ID>\n</Paramics>";
		
		try
		{
			FileWriter w = new FileWriter(paramStatusFile);
			w.write(message);
			w.close();
		}
		catch (IOException e)
		{
			paramLogger.logp(Level.SEVERE, "TrafficSimulator", "writeParamicsStatus", 
					"Error writing to file " + paramStatusFile + ".", e);
		}
		
		writeToLog(paramStatusLog, message);
		
		setChanged();
		notifyObservers(new ParamicsSimulatorLogMessage(PARAMICS_SIMULATOR_LOG.PARAMICS_STATUS, message));	
	}	
	
	/**
	 * Runs the Paramics simulator.
	 */
	public static void main(String[] args) 
	{
		try
		{
			if(System.getProperty("PARAMICS_SIM_PROPERTIES") != null)
			{
				new Thread(new ParamicsSimulator(System.getProperty(
					"PARAMICS_SIM_PROPERTIES"))).start();
			}
			else
			{
				throw new Exception ("PARAMICS_SIM_PROPERTIES system property not defined.");
			}
		} 
		catch (Exception e) 
		{
			paramLogger.logp(Level.SEVERE, "ParamicsSimulator", "Main", 
					"Error occured initializing application", e);
			
			JOptionPane.showMessageDialog(null, e.getMessage(), "Error - Program Exiting", JOptionPane.ERROR_MESSAGE);	
	        	
			System.exit(-1);
		}
	}
	
	/**
	 * Gets the simulation cameras being used by the Paramics Simulator.
	 * @return A list of Simulation Cameras.
	 */
	public List<SimulationCamera> getCameras()
	{
		return cameras;
	}
}
