package tmcsim.paramicscommunicator;

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
{

    /**
     * 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");
        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 static ParamicsCommunicatorGUI theGUI;
    /**
     * The thread used to initialize the socket connection with the CADSimulator
     */
    Thread initThread;

    /**
     * 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)
    {
        writers = new TreeMap<String, ParamicsFileWriter>();
        readers = new TreeMap<String, ParamicsFileReader>();

        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);
        }

        Integer desiredPort = Integer.parseInt(paramicsCommProp.getProperty(
                PROPERTIES.SOCKET_PORT.name).trim());
        /* Start a thread to initialize the sockets that talk to CADsimulator */
        initThread = new Thread(new SocketStarter(desiredPort));
        initThread.start();
    }

    /**
     * For testing, we want to be able to provide a non-visible instance of the
     * GUI to be used. (As main won't be called).
     *
     * @param viewer
     */
    public void setGUI(ParamicsCommunicatorGUI viewer)
    {
        theGUI = viewer;
        addObserver(theGUI);
    }

    /**
     * Start the thread that does the main work of the communicator. This method
     * waits until the initial thread has opened the needed sockets to connect
     * to the CADSimulator, then it starts the socket reader thread, which runs
     * until the application is terminated. Usage: Normally this method is
     * called immediately after constructing this object, but for testing
     * purposes it is available to be invoked by the test harness after the
     * Simulation Manager initiates a connection.
     */
    public void startReading()
    {
        while (initThread.isAlive())
        {
            try
            {
                Thread.sleep(1000);
            } catch (InterruptedException ex)
            {
                paramLogger.logp(Level.SEVERE, "ParamicsCommunicator", "runstarter",
                        "Sleep interrupted.");
            }
        }
        Runnable sr = new SocketReader();
        new Thread(sr).start();
    }

    /**
     * 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);
        }
    }

    private class SocketReader implements Runnable
    {

        /**
         * 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
                {
                    // Sleep for a second while waiting in this infinite loop
                    // to yield to other threads run by test harness.
                    Thread.sleep(1000);
                } 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 the received XML document message.
     * 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;
        }
    }

    private class SocketStarter implements Runnable
    {

        Integer socketPort;

        /**
         * 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
        public SocketStarter(Integer socketPort)
        {
            this.socketPort = socketPort;
        }

        public void run()
        {

            boolean waiting = true;

            try
            {
                serverSocket = new ServerSocket(socketPort);
                //delay for accept timeout(milliseconds)
                serverSocket.setSoTimeout(10 * 1000);
            } catch (IOException ioe)
            {
                paramLogger.logp(Level.SEVERE, "ParamicsCommunicator",
                        "initializeSockets", "Exception in creating "
                        + "the server socket on port " + socketPort + ", terminating.");
                System.exit(1);
            }

            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)
                {
                    paramLogger.logp(Level.SEVERE, "ParamicsCommunicator",
                            "initializeSockets", "Exception in creating "
                            + "the receiving socket on port " + socketPort + ", terminating.");
                    System.exit(1);
                }

            }


            //** 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)
            {
                paramLogger.logp(Level.SEVERE, "ParamicsCommunicator",
                        "initializeSockets", "Exception in creating "
                        + "input and output streams on socket, terminating.");
                System.exit(1);
            }

        }
    }

    /**
     * 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)
    {
        System.setProperty("PARAMICS_COMM_PROPERTIES", "config/paramics_communicator_config.properties");

        try
        {
            if (System.getProperty("PARAMICS_COMM_PROPERTIES") != null)
            {
                ParamicsCommunicator pc = new ParamicsCommunicator(System.getProperty("PARAMICS_COMM_PROPERTIES"));
                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                theGUI = new ParamicsCommunicatorGUI();
                pc.addObserver(theGUI);
                theGUI.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                theGUI.setVisible(true);
                pc.startReading();

            }
            else
            {
                throw new Exception("PARAMICS_COMM_PROPERTIES system property not defined.");
            }
        } 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);
        }


    }
}
