package jaron.flightgear;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;

import javax.swing.event.EventListenerList;

import jaron.gps.Direction;
import jaron.gps.Latitude;
import jaron.gps.Longitude;
import jaron.gps.NMEAListener;
import jaron.gps.Trackpoint;
import jaron.gps.TrackpointListener;

/**
 * The <code>FlightGearNMEAReceiver</code> class provides a TCP/IP connection to the
 * FlightGear flight simulator (FG) via the FG NMEA output interface.<br>
 * <code>FlightGearNMEAReceiver</code> runs as a thread and by instantiating it
 * waits for the incoming connection from FG (at port {@value DEFAULT_PORT} by default).<br>
 * The instantiating has to be done before FG starts up, else FG reports a
 * connection error and terminates.<br>
 * Be aware that running FG and a Java application that uses the
 * <code>FlightGearNMEAReceiver</code> needs a computer with sufficient performance.
 * The consequence of a lack of performance is, that some of the data sent by FG
 * could be lost because of timing issues. In this case you could run FG and the
 * Java application on two different machines via a network connection.<br>
 * <br>
 * Have a look at the {@link jaron.uavsim.UAVsim}. example on how to use the
 * <code>FlightGearNMEAReceiver</code>
 *   
 * @author      jarontec gmail com
 * @version     1.2
 * @since       1.1
 */
public class FlightGearNMEAReceiver extends Thread {
  /**
   * The default update frequency of the <code>FlightGearNMEAReceiver</code>
   * is {@value DEFAULT_UPDATE_FREQUENCY}Hz.
   */
  public static final float DEFAULT_UPDATE_FREQUENCY = 1f;
  
  /**
   * The default port that is used for the incoming connection. At the moment
   * this is port 5557 but this could change in the future.
   */
  public static final int DEFAULT_PORT = 5557;
  
  private float updateFrequency = DEFAULT_UPDATE_FREQUENCY;
  private ServerSocket server = null;
  private Socket client = null;
  private Boolean debug = false;
  private EventListenerList trackpathListeners = new EventListenerList();
  private EventListenerList nmeaListeners = new EventListenerList();

  /**
   * Creates a new <code>FlightGearNMEAReceiver</code> and starts a <code>Thread</code>
   * that listens for incoming connections.
   * 
   * @param port  the port the receiver listens to
   */
  public FlightGearNMEAReceiver(int port) {
    // start the socket server who waits for the connecting client
    try {
      server = new ServerSocket(port); // from now on the server listens to incomming connections
    } catch (IOException e) {
      System.out.println("IOException in FlightGearNMEAReceiver(): IOException: " + e.getMessage());
    }
    setDaemon(true);
    start();
  }

  /**
   * Creates a new <code>FlightGearNMEAReceiver</code> and starts a <code>Thread</code>
   * that listens for incoming connections. It listens on the default port
   * <code>DEFAULT_PORT</code>.
   * 
   * @see FlightGearNMEAReceiver#DEFAULT_PORT
   */
  public FlightGearNMEAReceiver() {
    this(DEFAULT_PORT);
  }
  
  /**
   * Adds a listener to the <code>EventListener</code> mechanism. So whenever
   * a set of NMEA data is received, the listener will be informed about
   * that.
   * 
   * @param listener  the listener to be added
   */
  public void addNMEAListener(NMEAListener listener) {
    // from now on the listener listens to NMEA data changes
    nmeaListeners.add(NMEAListener.class, listener);
  }
  
  /**
   * Adds a listener to the <code>EventListener</code> mechanism. So whenever
   * a trackpoint is added to the trackpath, the listener will be informed about
   * that event.
   * 
   * @param listener  the listener to be added
   */
  public void addTrackpointListener(TrackpointListener listener) {
    // from now on the listener listens to trackpoint changes
    trackpathListeners.add(TrackpointListener.class, listener);
  }

  /**
   * Disconnects the connection to the client.
   */
  private void disconnectClient() {
    try {
      if (client != null) {
        client.close();
        if (debug) System.out.println("FlightGearNMEAReceiver::disconnectClient(): Client is disconnected.");
      }
    } catch (IOException e) {
      System.out.println("IOException in FlightGearNMEAReceiver::disconnectClient(): " + e.getMessage());
    }
    finally {
      client = null;
    }
  }
  
  /**
   * Notifies all the listeners that added themselves to the <code>EventListener</code>
   * mechanism about the received NMEA data.
   * 
   * @param trackpoint  the new trackpoint
   */
  protected synchronized void notifyTrackpointListeners(Trackpoint trackpoint) {
    for (TrackpointListener l : trackpathListeners.getListeners(TrackpointListener.class)) 
      l.trackpointChanged(trackpoint);
  } 
  
  /**
   * Notifies all the listeners that added themselves to the <code>EventListener</code>
   * mechanism about the new NMEA data that was received.
   * 
   * @param trackpoint  the new trackpoint
   */
  protected synchronized void notifyNMEAListeners(HashMap<String, String> nmea) {
    for (NMEAListener l : nmeaListeners.getListeners(NMEAListener.class)) 
      l.nmeaReceived(nmea);
  } 
  
  /**
   * Sets the update frequency for the <code>FlightGearNMEAReceiver</code>.
   * The default update frequency is set to {@value DEFAULT_UPDATE_FREQUENCY}Hz.
   * 
   * @param updateFrequency the new frequency in Hz
   */
  public void setUpdateFrequency(float updateFrequency) {
    this.updateFrequency = updateFrequency;
  }
  
  /* (non-Javadoc)
   * @see java.lang.Thread#run()
   */
  @Override
  public void run() {
    while(true) {
      try {
        // check if there is a client connected to our server
        if (client != null) {
          if (debug) System.out.println("FlightGearNMEAReceiver::run(): Connection to client established, waiting for data");
          BufferedReader is = new BufferedReader(new InputStreamReader(client.getInputStream()));
          String s = new String();
          Trackpoint trackpoint = new Trackpoint();
          HashMap<String, String> nmea = new HashMap<String, String>();

          // FlightGear sends a stream of $GPRMC, $GPGGA and $GPGSA NMEA data (in that order)
          // this data is parsed and converted to a Trackpoint object
          // after that the Trackpoint object is sent to the EventListeners
          while ((s = is.readLine()) != null) {
            String[] params = s.split(",");
            if(params[0].equals("$GPRMC")) {
              nmea = new HashMap<String, String>();
              nmea.put("GPRMC", s);
              trackpoint.setLatitude(new Latitude(params[3], Direction.fromValue( params[4])));
              trackpoint.setLongitude(new Longitude(params[5], Direction.fromValue( params[6])));
              String time = params[1];
              String date = params[9];
              Calendar cal = GregorianCalendar.getInstance();
              cal.set(Calendar.HOUR, Integer.parseInt(time.substring(0, 2)));
              cal.set(Calendar.MINUTE, Integer.parseInt(time.substring(2, 4)));
              cal.set(Calendar.SECOND, Integer.parseInt(time.substring(4, 6)));
              cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(date.substring(0, 2)));
              cal.set(Calendar.MONTH, Integer.parseInt(date.substring(2, 4)));
              cal.set(Calendar.YEAR, Integer.parseInt(date.substring(4, 6)));
              trackpoint.setTimestamp(cal.getTime());
              trackpoint.setGroundSpeedKnots(Double.parseDouble(params[7]));
            }
            else if(params[0].equals("$GPGGA")) {
              nmea.put("GPGGA", s);
              trackpoint.setSatellites(Integer.parseInt(params[7]));
              trackpoint.setAltitude(Double.parseDouble(params[9]));
            }
            // $GPGSA implies the end of the NMEA data sequence
            else if(params[0].equals("$GPGSA")) {
              nmea.put("GPGSA", s);
              break;
            }
          }
          // FlightGear sends junk data at initialization and this is filtered out here
          if (trackpoint.getLatitude().getDegrees() != 0) {
            notifyTrackpointListeners(trackpoint);
            notifyNMEAListeners(nmea);
          }
          if (debug) System.out.println("FlightGearNMEAReceiver::run(): Data received");
        }
        // if no client is connected then check if there is a client waiting for a connection
        else if (server != null) {
          if (debug) System.out.println("FlightGearNMEAReceiver::run(): Waiting for client");
          client = server.accept(); // waits until a connection is established
        }
      } catch (IOException e) {
          System.out.println("IOException in FlightGearNMEAReceiver::run(): IOException: " + e.getMessage());
          disconnectClient();
      }
      try { sleep((long )(1000 / updateFrequency)); } catch(InterruptedException e) {} 
    }
  }

  /**
   * Sets the debugging flag which determines if the debugging informations should
   * be printed to the console. This is for debugging purpose only.
   * 
   * @param debug    set to <code>true</code> if additional debugging information
   *                 should be printed
   */
  public void setDebug(Boolean debug) {
    this.debug = debug;
  }

  /**
   * Shuts this receiver down and disconnects the client.
   */
  public void shutDown() {
    disconnectClient();
    if (debug) System.out.println("FlightGearNMEAReceiver::shutDown(): Receiver is shut down.");
  }
}