package tmcsim.paramicscommunicator;

import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Observable;
import java.util.Observer;
import java.util.Properties;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.UIManager;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import tmcsim.common.CADProtocol.PARAMICS_ACTIONS;
import tmcsim.common.CADProtocol.PARAMICS_COMM_TAGS;
import tmcsim.paramicscommunicator.FileIOUpdate.IO_TYPE;
import tmcsim.paramicscommunicator.FileRegUpdate.REG_TYPE;
import tmcsim.paramicscommunicator.gui.ParamicsCommunicatorGUI;

/**
 * ParamicsCommunicator is the main class for this module. The Paramics
 * Communicator is used to provide communication between the CAD Simulator and
 * the Paramics traffic modeler. While the application is running, data is
 * received on a socket from the CAD Simulator. Transmitted data are XML
 * documents containing information and action commands. The CAD Simulator
 * registers readers and writers with the ParamicsCommunicator. Any data read by
 * a ParamicsReader is sent back to the CAD Simulator. All data to be written by
 * a ParamicsWriter is received through the socket.<br><br>
 * The properties file for the ParamicsCommunicator class contains the following
 * data.<br>
 * <code>
 * -----------------------------------------------------------------------------<br>
 * Socket Port The port number to use for socket communication.<br>
 * Working Directory The working directory use for Paramics file
 * communication.<br>
 * Error File The target file to use for error logging.<br>
 * -----------------------------------------------------------------------------<br>
 * Example File: <br>
 * SocketPort = 4450 <br>
 * WorkingDirectory = c:\\tmc_simulator\\ <br>
 * ErrorFile = sim_mgr_error.xml <br>
 * -----------------------------------------------------------------------------<br>
 * </code>
 *
 * @author Matthew Cechini (mcechini@calpoly.edu)
 * @version $Date: 2009/04/17 16:27:46 $ $Revision: 1.7
 */
public class ParamicsCommunicator extends Observable implements Observer, Runnable
{

    private static final String CONFIG_FILE_NAME = "paramics_communicator_config.properties";
	/**
     * Error logger.
     */
    private static Logger paramLogger = Logger.getLogger("tmcsim.paramicscommunicator");

    /**
     * Enumeration containing property names.
     *
     * @author Matthew Cechini
     */
    private static enum PROPERTIES
    {

        SOCKET_PORT("SocketPort"),
        WORKING_DIR("WorkingDirectory"),
        GUI_VISIBLE("GUIvisible");
        public String name;

        private PROPERTIES(String n)
        {
            name = n;
        }
    }
    /**
     * Properties object.
     */
    private Properties paramicsCommProp = null;
    /**
     * Current working directory where files will be read and written
     */
    private String workingDirectory = null;
    /**
     * Socket used to create socket communication with the CAD Simulator.
     */
    private ServerSocket serverSocket = null;
    /**
     * Soccket used to communicate with CAD Simulator.
     */
    private Socket paramicsSocket = null;
    /**
     * Input Stream for reading data from the CAD Simulator.
     */
    private ObjectInputStream in = null;
    /**
     * Output Stream for writing data to the CAD Simulator.
     */
    private ObjectOutputStream out = null;
    /**
     * Map of all current ParamicsFileWriters referenced by I/O ID.
     */
    private TreeMap<String, ParamicsFileWriter> writers = null;
    /**
     * Map of all current ParamicsFileReaders referenced by I/O ID.
     */
    private TreeMap<String, ParamicsFileReader> readers = null;
    /**
     * The view class for the ParamicsCommunicator.
     */
    private ParamicsCommunicatorGUI theGUI;

    /**
     * Constructor. Read in the property values. If the properties file does not
     * contain a value for the working directory, open a dialog to prompt the
     * user for the path of the Paramics working directory. An empty string is
     * not accepted. A null signifies that the user pressed cancel. Prompt the
     * user to accept the cancel and exit the application if confirmed. Continue
     * until a valid directory has been entered, that exists, and append a '\'
     * to the end of the directory if necessary.
     *
     * Initialize the Sockets and begin communication.
     *
     * @param propertiesFilePath File Path of ParamicsCommunicator properties
     * file.
     */
    public ParamicsCommunicator(String propertiesFile)
    {
        paramLogger.logp(Level.INFO, "ParamicsCommunicator", "Constructor",
                "Entering ");

        writers = new TreeMap<String, ParamicsFileWriter>();
        readers = new TreeMap<String, ParamicsFileReader>();

        theGUI = new ParamicsCommunicatorGUI();
        addObserver(theGUI);
        theGUI.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        try
        {
            paramicsCommProp = new Properties();
            paramicsCommProp.load(new FileInputStream(propertiesFile));

            if (paramicsCommProp.getProperty(PROPERTIES.SOCKET_PORT.name) == null)
            {
                JOptionPane.showMessageDialog(theGUI,
                        "Properties file missing CAD Simulator Port information.",
                        "Invalid Configuration", JOptionPane.ERROR_MESSAGE);
                System.exit(0);
            } else if (paramicsCommProp.getProperty(PROPERTIES.WORKING_DIR.name) == null
                    || paramicsCommProp.getProperty(PROPERTIES.WORKING_DIR.name).length() == 0)
            {

                try
                {
                    String workingDir = null;

                    while (workingDir == null || workingDir.length() == 0)
                    {
                        workingDir = JOptionPane.showInputDialog(null,
                                "Please set the output directory for Paramics communication.",
                                "Paramics Working Directory", JOptionPane.QUESTION_MESSAGE);

                        if (workingDir == null)
                        {
                        } else if (!new File(workingDir).exists())
                        {
                            JOptionPane.showMessageDialog(null,
                                    "Directory does not exist.",
                                    "Invalid Working Directory", JOptionPane.WARNING_MESSAGE);

                            workingDir = null;
                        } else if (!new File(workingDir).isDirectory())
                        {
                            JOptionPane.showMessageDialog(null,
                                    workingDir + " is not a directory.",
                                    "Invalid Working Directory", JOptionPane.WARNING_MESSAGE);

                            workingDir = null;
                        }
                    }

                    if (workingDir.lastIndexOf("\\") != workingDir.length() - 1)
                    {
                        workingDir = workingDir + "\\";
                    }

                    paramicsCommProp.setProperty(PROPERTIES.WORKING_DIR.name, workingDir);
                    paramicsCommProp.store(new FileOutputStream(propertiesFile), "");
                } catch (IOException ioe)
                {
                    paramLogger.logp(Level.SEVERE, "ParamicsCommunicator", "Constructor",
                            "Exception in writing properties file.", ioe);
                }

            }

            workingDirectory = paramicsCommProp.getProperty(
                    PROPERTIES.WORKING_DIR.name).trim();

        } catch (Exception e)
        {
            paramLogger.logp(Level.SEVERE, "ParamicsCommunicator", "Constructor",
                    "Exception in reading properties file.", e);
        }

        // Should we display the GUI?
        String visibleProp = paramicsCommProp.getProperty(PROPERTIES.GUI_VISIBLE.name);
        // If no property was given, or if it was given and says True
        if (visibleProp == null || (visibleProp.toLowerCase().equals("true")))
        {
            theGUI = new ParamicsCommunicatorGUI(); // it sets itself visible
            addObserver(theGUI);
            theGUI.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);
        }

        try
        {
            initializeSockets(Integer.parseInt(paramicsCommProp.getProperty(
                    PROPERTIES.SOCKET_PORT.name).trim()));
        } catch (Exception e)
        {
            paramLogger.logp(Level.SEVERE, "ParamicsCommunicator", "Constructor",
                    "Exception in initializing sockets.", e);
        }

    }

    /**
     * Transmits a message XML document object to the CAD Simulator.
     *
     * @param mess The ParamicsCommMessage to be transmitted.
     */
    private void write(Document mess)
    {

        synchronized (paramicsSocket)
        {
            try
            {
                out.writeObject(mess);
                out.flush();
            } catch (Exception e)
            {
                paramLogger.logp(Level.SEVERE, "ParamicsCommunicator", "write",
                        "Exception in writing to the socket.", e);
            }
        }
    }

    /**
     * Observer/Observable update method. The Paramics Communicator observers
     * registered ParamicsReaders. When messages are to be sent, they are sent
     * through this method. All messages are ParamicsCommMessage objects. Send
     * these messages to the write() method for transmission on the socket.
     */
    public void update(Observable o, Object arg)
    {

        if (arg instanceof Document)
        {
            write((Document) arg);
        }
    }

    /**
     * Runnable method. While this thread is not interrupted, read in an object
     * from the socket input stream. If an object exists, call doMessage() to
     * parse and perform the received action in the message.
     */
    public void run()
    {

        while (true)
        {
            try
            {
                doMessage((Document) in.readObject());
            } catch (SocketTimeoutException ste)
            {
                //just try again
            } catch (EOFException eofe)
            {
                paramLogger.logp(Level.SEVERE, "ParamicsCommunicator",
                        "run", "EOF Exception in reading data from the socket.", eofe);
            } catch (Exception e)
            {
                paramLogger.logp(Level.SEVERE, "ParamicsCommunicator",
                        "run", "Exception in reading data from the socket.", e);

                JOptionPane.showMessageDialog(theGUI,
                        "Connection has been lost to the CAD Simulator.  "
                        + "Paramics Communicator will now shutdown.",
                        "Dropped Connection", JOptionPane.ERROR_MESSAGE);
                break;
            }
            try
            {
                Thread.sleep(2000);
                paramLogger.logp(Level.INFO, "ParamicsCommunicator",
                        "run", "sleeping.");
            } catch (InterruptedException ex)
            {
                paramLogger.logp(Level.INFO, "ParamicsCommunicator",
                        "run", "Exception in reading data from the socket.", ex);
            }
        }


        try
        {
            in.close();
        } catch (Exception e)
        {
        }
        try
        {
            out.close();
        } catch (Exception e)
        {
        }
        try
        {
            serverSocket.close();
        } catch (Exception e)
        {
        }
        try
        {
            paramicsSocket.close();
        } catch (Exception e)
        {
        }

    }

    /**
     * Perform the action represented in XML document message received from the
     * CAD Simulator. First determine if the action is from a READER, WRITER,
     * and RESET. If the paramics action is REGISTER, add a new
     * ParamicsFileReader/Writer to the local list of readers/writers and update
     * the GUI with a FileRegUpdate object. If the paramics action is
     * UNREGISTER, remove the ParamicsFileReader/Writer from the local list of
     * readers/writers and update the GUI with a FileRegUpdate object. If RESET
     * is received, clear all readers and writers.
     *
     * @param mess Received XML document message.
     */
    private void doMessage(Document mess)
    {

        Element rootElement = mess.getDocumentElement();

        String id = null;
        String action = null;

        switch (PARAMICS_COMM_TAGS.fromString(rootElement.getNodeName()))
        {
            case READER:
                id = rootElement.getAttribute(PARAMICS_COMM_TAGS.ID.tag);
                action = rootElement.getAttribute(PARAMICS_COMM_TAGS.ACTION.tag);

                switch (PARAMICS_ACTIONS.fromString(action))
                {
                    case REGISTER:
                        Integer interval = Integer.parseInt(rootElement.getChildNodes().item(0).getTextContent());
                        String targetFile = rootElement.getChildNodes().item(1).getTextContent();

                        readers.put(id, new ParamicsFileReader(workingDirectory, id,
                                interval, targetFile));
                        readers.get(id).addObserver(this);
                        readers.get(id).addObserver(theGUI);

                        setChanged();
                        notifyObservers(new FileRegUpdate(IO_TYPE.READ,
                                REG_TYPE.REGISTER, id, targetFile, interval));
                        break;
                    case UNREGISTER:
                        readers.get(id).deleteObserver(this);
                        readers.get(id).deleteObserver(theGUI);
                        readers.remove(id);

                        setChanged();
                        notifyObservers(new FileRegUpdate(IO_TYPE.READ,
                                REG_TYPE.UNREGISTER, id, null, null));
                        break;
                }
                break;
            case WRITER:
                id = rootElement.getAttribute(PARAMICS_COMM_TAGS.ID.tag);
                action = rootElement.getAttribute(PARAMICS_COMM_TAGS.ACTION.tag);

                switch (PARAMICS_ACTIONS.fromString(action))
                {
                    case REGISTER:
                        String targetFile = rootElement.getChildNodes().item(0).getTextContent();

                        writers.put(id, new ParamicsFileWriter(id,
                                workingDirectory, targetFile));
                        writers.get(id).addObserver(theGUI);

                        setChanged();
                        notifyObservers(new FileRegUpdate(IO_TYPE.WRITE,
                                REG_TYPE.REGISTER, id, targetFile, null));
                        break;
                    case UNREGISTER:
                        writers.remove(id);

                        writers.get(id).deleteObserver(theGUI);

                        setChanged();
                        notifyObservers(new FileRegUpdate(IO_TYPE.WRITE,
                                REG_TYPE.UNREGISTER, id, null, null));
                        break;
                    case WRITE_FILE:
                        writers.get(id).writeMessage((Element) rootElement.getChildNodes().item(0));
                        break;
                }
                break;
            case RESET:
                readers.clear();
                writers.clear();
                break;
        }
    }

    /**
     * Method waits to accept a socket connection from the CAD Simulator. When a
     * connection has been established the method exits. The input and output
     * streams are created on the new socket.
     *
     * @param socketPort Socket port to use for establishing Socket
     * communication.
     * @throws IOException if there is an exception in establishing Socket
     * communication.
     */
    private void initializeSockets(Integer socketPort) throws IOException
    {

        boolean waiting = true;

        try
        {
            serverSocket = new ServerSocket(socketPort);
            //delay for accept timeout(milliseconds)
            serverSocket.setSoTimeout(10 * 1000);
        } catch (IOException ioe)
        {
            throw new IOException("Exception in creating "
                    + "the server socket on port " + socketPort);
        }

        while (waiting)
        {
            try
            {
                paramicsSocket = serverSocket.accept();
                waiting = false;
            } catch (SocketTimeoutException ste)
            {
                System.out.println("...waiting...");
                try
                {
                    Thread.sleep(2000);
                    paramLogger.logp(Level.INFO, "ParamicsCommunicator",
                            "initializeSockets", "sleeping.");
                } catch (InterruptedException ex)
                {
                    paramLogger.logp(Level.INFO, "ParamicsCommunicator",
                            "initializeSockets", "Exception exiting for socket.", ex);
                }
            } catch (IOException ioe)
            {
                throw new IOException("Exception in creating "
                        + "the receiving socket on port " + socketPort);
            }

        }


        //** out must be performed before in to unlock for connecting socket **//
        try
        {
            out = new ObjectOutputStream(paramicsSocket.getOutputStream());
            in = new ObjectInputStream(paramicsSocket.getInputStream());
        } catch (IOException ioe)
        {
            throw new IOException("Exception in creating input "
                    + "and output streams on socket.");
        }

    }

    /**
     * Construct the ParamicsCommunicator with the properties file path, either
     * from the command line arguments or default.
     *
     * @param args Command line arguments.
     */
    public static void main(String[] args) {
    	if(System.getProperty("CONFIG_DIR") == null){
        	System.setProperty("CONFIG_DIR", "config");
        }
    	
        try{

        	UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            new Thread(new ParamicsCommunicator(System.getProperty("CONFIG_DIR") + System.getProperty("file.separator") + CONFIG_FILE_NAME)).start();
        
        } catch (Exception e) {
            paramLogger.logp(Level.SEVERE, "ParamicsCommunicator", "Main",
                    "Error occured initializing application", e);

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

            System.exit(-1);
        }


    }
}
