package sivantoledo.nxt;
import java.util.concurrent.locks.*;

public class SerialInterface implements CommunicationInterface {
  
  /* returns the last Windows error message */
  native private static String nlasterror();

  /* returns the last Windows error message */
  native private static String nenumerateserialports();

  /* returns a windows handle */
  private static int handle;
  native private static int nopen(String name);

  /* returns a windows handle to an abort event */
  private static int abortHandle;
  native private static int ngetaborthandle();
  native private static void nabort(int h);

  /* closes a windows handle */
  native private static int nclose(int handle, int abort_handle);

  /* returns an invalid handle */
  native private static int ninvalid();

  /* 
   * returns the size of a handle;
   * if not 4, the code will not work!
   */
  native private static int nhandlesize();

  /*
   *  NXT messages are limited to 59 bytes, so buffer must be
   *  at least as long.
   */
  
  native private static int nreceive(int handle, byte[] buffer, int abort_handle);
  
  /*
   * The buffer may be longer than the message that we want to send.
   */
  
  native private static int nsend   (int handle, byte[] buffer, int length, int abort_handle); 

  /*
   * Native-library loader
   */
  
  static { 
    java.io.FileOutputStream dll_f;
    java.io.InputStream      dll_r;
    try {
      System.loadLibrary("SerialInterface"); 
    } catch (UnsatisfiedLinkError ule) {
      try {
        dll_r = SerialInterface.class.getClassLoader().getResourceAsStream("native/win32/NXTCommunicationChannel/release/SerialInterface.dll"); 
        dll_f = new java.io.FileOutputStream("SerialInterface.dll");
        byte[] buffer = new byte[8192];
        int    bytes_read = 0;
        do {
          dll_f.write(buffer, 0, bytes_read);
          bytes_read = dll_r.read(buffer);
        } while (bytes_read != -1);
        dll_r.close();
        dll_f.close();
        System.loadLibrary("SerialInterface");
      } catch (java.io.IOException ioe) {
        System.err.println("Error encountered while trying to copy a DLL to the working directory: "
                           +ioe.getMessage());
      }
    }
    handle      = ninvalid();
    abortHandle = ninvalid();
  }
  
  /*
   * Constructor
   */
  
  public SerialInterface(String resource_name, String short_name, Listener l) 
    throws java.io.IOException {
    if (nhandlesize() != 4)
      throw new RuntimeException("Cannot use an int to store a Win32 HANDLE");
    
    for (int q=0; q<10; q++)
      sendQueue[q] = new java.util.LinkedList<Message>();

    handle = nopen("//./"+short_name);
    if (handle == ninvalid())
      throw new java.io.IOException(nlasterror());
    abortHandle = ngetaborthandle();
    if (abortHandle == ninvalid())
      throw new java.io.IOException(nlasterror());

    messageListener = l;
    listenerThread = new ListenerThread();    
    //sendThread = new SendThread();

    listenerThread.start();
    //sendThread.start();
  }
  
  /*
   * Sending messages.
<   * 
   * Sending uses a queue and a send thread.
   */
  
  /*
  */
  //private java.util.Queue sendQueue = new java.util.LinkedList();
  private java.util.Queue<Message>[] sendQueue = new java.util.Queue[10];
  private Lock      lock = new ReentrantLock();
  private Condition notEmpty = lock.newCondition(); 
  
  public void flushAllQueues() {
    lock.lock();
    for (int q=0; q<sendQueue.length; q++) sendQueue[q].clear();
    lock.unlock();
  }

  public void flushQueue(int q) {
    lock.lock();
    sendQueue[q].clear();
    lock.unlock();
  }

  public void send(Message m) {
    lock.lock();
    Log.println(">> GOING TO ENQUEUE A SEND ON QUEUE "+m.mailbox+" QUEUE SIZE "+sendQueue[m.mailbox].size());
    try {
      //Log.println(">> ENQUEUEING SEND to queue "+buffer[2]);
      sendQueue[m.mailbox].add(m);
      if (m.mailbox >=5) notEmpty.signal();
    } finally {
      lock.unlock();
    }
    //Log.println(">> DONE ENQUEUEING SEND");
  }

  /*
  private SendThread sendThread;
  private class SendThread extends Thread {
    private volatile boolean stopSignal = false;
    public void done() { stopSignal = true; }
    public void run() {
      Message m = null; // dummy initialization
      while (!stopSignal) {
        lock.lock();
        try {
          Log.println("IS THERE A MESSAGE TO SEND?");
          while (sendQueue[5].isEmpty()
                 && sendQueue[6].isEmpty()
                 && sendQueue[7].isEmpty()
                 && sendQueue[8].isEmpty()
                 && sendQueue[9].isEmpty()
                 ) {            
            //try {
              notEmpty.await();
            //} catch (InterruptedException e) {
            //  Log.println("send thread interrupted");
            //  if (stopSignal) return;
            //}
          }

          Log.println("DEQUEUEING BEFORE SEND");
          if      (! sendQueue[5].isEmpty()) m = sendQueue[5].poll();
          else if (! sendQueue[6].isEmpty()) m = sendQueue[6].poll();
          else if (! sendQueue[7].isEmpty()) m = sendQueue[7].poll();
          else if (! sendQueue[8].isEmpty()) m = sendQueue[8].poll();
          else if (! sendQueue[9].isEmpty()) m = sendQueue[9].poll();
        } catch (InterruptedException e) {
          Log.println("send thread interrupted");
          if (stopSignal) return;
        } finally {
          lock.unlock();
        }
        byte[] buffer = new byte[ m.payload.length + 3];
        buffer[0] = (byte) 0x80;
        buffer[1] = 0x09;
        buffer[2] = m.mailbox;
        for  (int k=0; k < m.payload.length; k++) 
          buffer[k+3] = m.payload[k];
        Log.println("CALLING SEND TO MAILBOX "+m.mailbox);
        int rc = nsend(handle, buffer, buffer.length, abortHandle);
        Log.println("CALLING SEND (DONE)");
        Log.println("send output = "+rc);
      } 
    }
  }
  */

  /*
   * Receiving messages.
   */

  private Listener messageListener;
  private ListenerThread listenerThread;

  private class ListenerThread extends Thread {
    private volatile boolean stopSignal = false;
    public void done() { stopSignal = true; }
    public void run() {
      byte[] buffer = new byte[256];
      while (!stopSignal) {
        int length = nreceive(handle, buffer, abortHandle);
        
        //System.out.printf("received bt message: ");
        //for  (int k=0; k<length; k++)
        //  System.out.printf("%02x ",buffer[k]);
        //System.out.printf("\n");
         
        if (buffer[1] == 0x13) {
          byte remote_mailbox     = buffer[2];
          byte local_mailbox      = buffer[3]; // the two seem to always match
          boolean remove_message = (buffer[4] != 0);
          //System.out.println("request for queue "+(remote_mailbox-10)+", "+local_mailbox+", queuesize="+sendQueue[remote_mailbox-10].size());
          byte[] send_buffer = null;
          lock.lock();
          if (!sendQueue[ remote_mailbox - 10 ].isEmpty()) {            
            Log.println("DEQUEUEING BEFORE SEND, MAILBOX "+(remote_mailbox-10)+" remove?="+remove_message);
            // drop old unimportant messages
            Message m = sendQueue[remote_mailbox-10].poll();
            while (sendQueue[remote_mailbox-10].size() >= 5 && m.important == false) {
              Log.println("dropping a message");
              m = sendQueue[remote_mailbox-10].poll();
            }
            /*
            if (remove_message) 
              queued = sendQueue[ remote_mailbox - 10 ].poll();
            else
              queued = sendQueue[ remote_mailbox - 10 ].peek();
            */
            send_buffer = new byte[ 64 ];
            send_buffer[0] = 0x02;
            send_buffer[1] = 0x13;
            send_buffer[2] = 0x00; // success status code
            send_buffer[3] = local_mailbox; 
            for  (int k=0; k < m.payload.length; k++) 
              send_buffer[k+4] = m.payload[k];
            
          } else {
            // If we send this reply, the NXT-G bt-receive block
            // gets stuck and repeatedly asks for a message.
            // Better to ignore, which causes the NXT-G block to
            // time out, and then to check in the NXT-G program
            // whether we received a message.
            
            //Log.println("SEND QUEUE IS EMPTY");
            send_buffer = new byte[3];
            send_buffer[0] = 0x02;
            send_buffer[1] = 0x13;
            send_buffer[2] = 0x40; // "specified mailbox is empty" status

            //send_buffer = new byte[64];
            //send_buffer[0] = 0x02;
            //send_buffer[1] = 0x13;
            //send_buffer[2] = 0x40; // "specified mailbox is empty" status
            //send_buffer[3] = local_mailbox; 
          }
            
          lock.unlock();
          if (send_buffer != null) {
            //Log.println("CALLING SEND");
            
            //System.out.printf("sending bt message: ");
            //for  (int k=0; k<send_buffer.length; k++)
            //  System.out.printf("%02x ",send_buffer[k]);
            //System.out.printf("\n");
            
            //try { Thread.sleep(200); } catch (Exception e) {}

            int rc = nsend(handle, send_buffer, send_buffer.length, abortHandle);
            //Log.println("CALLING SEND (DONE)");
            //Log.println("send output = "+rc);
          }
        }
        
        if (length > 0 && messageListener != null 
            && buffer[1] == 0x09)
          //System.out.printf("SerialInterface received a 0x09 message with a %02x header\n",buffer[0]);
          messageListener.process(buffer[2], /* mailbox */
                                  buffer, 
                                  3); /* offset */
      }
    }
  }

  /*
  public void receive(byte[] buffer) 
    throws java.io.IOException {
    int rc = nreceive(handle, buffer, abortHandle);
    Log.println("recv output = "+rc);
  }
  */
  
  /*
   * Closing the connection
   */

  public void close() {
    //int rc = nclose(handle);
    //Log.println("close output = "+rc);
    //if (rc != 0) throw new java.io.IOException();

    Log.println("calling done on threads");
    //sendThread.done();
    if (listenerThread != null) listenerThread.done();
    Log.println("setting the abort event");
    nabort(abortHandle);
    Log.println("done() returned");
    try {
      Log.println("calling join()");
      //sendThread.interrupt();
      if (listenerThread != null) listenerThread.interrupt();
      //sendThread.join();
      if (listenerThread != null) listenerThread.join();
    } catch ( InterruptedException ex ) {}
    Log.println("now closing the handles");
    nclose(handle, abortHandle);
    Log.println("close done");
  }
  
  
  /*
   * enumeration of potential commmunication channels
   */
  
  public static java.util.Map<String,String> enumerateInterfaces() {
    java.util.Map<String,String> map = new java.util.LinkedHashMap<String,String>();
    
    String s = nenumerateserialports();
    //Log.println(s);
    
    int eq = 0, semi = 0, current;
    String internal_name, external_name;
    
    current = 0;
    
    do {
      eq   = s.indexOf('=',current);
      semi = s.indexOf(';',current);
      
      //Log.println(current+","+eq+","+semi);

      if (eq != -1 && semi != -1) {
        internal_name = s.substring(current,eq);
        external_name = s.substring(eq+1,semi);
        map.put(internal_name, external_name);
        Log.println(internal_name+" ~ "+external_name);
      }
      current = semi+1;
    } while (semi != -1);
    
    return map;
  }

}
