/**
 * @file Com.java
 * 
 * @author Christos Choutouridis AEM:8997
 * @email  cchoutou@ece.auth.gr
 */
package net.hoo2.auth.vmodem;

/** @name imports */
/** @{ */
import java.util.Arrays;
import ithakimodem.*;
/** @} */

/**
 * @class Com
 * 
 * Wrapper object of the provided Modem class to provide
 * a convenient and common API for all the session functionalities
 */
class Com {
   static final int SPEED_DEFAULT   = 48000;    /** default communication speed [bps] */
   static final int TIMEOUT_DEFAULT = 2000;     /** default timeout values [sec] */
   static final String URL_DEFAULT  = "ithaki"; /** Default destination. Ithaki of-course */

   private Modem  modem_;     /** Ref to Modem */
   private int    speed_;     /** Communication speed [bps] */
   private int    timeout_;   /** communication timeout [sec] */

   /**
    * Basic constructor
    */
   Com () {
      modem_   = new Modem();
      speed_   = SPEED_DEFAULT;
      timeout_ = TIMEOUT_DEFAULT;
      modem_.setSpeed(speed_);
      modem_.setTimeout(timeout_);
   }
   /** A more fancy constructor
    * 
    * @param log     Reference to Log to use
    * @param speed   The desired speed [bps]
    * @param timeout The desired timeout [sec]
    * @note
    *    Currently not used
    */
   Com (Log log, int speed, int timeout) {
      modem_   = new Modem();
      speed_   = speed;
      timeout_ = timeout;
      modem_.setSpeed(speed_);
      modem_.setTimeout(timeout_);
   }

   /** @name Mutator interface */
   /**@{ */
   /** Get the current timeout value [sec] */
   int  timeout ()      { return timeout_; }
   /** Set the timeout value [sec] */
   void timeout (int timeout) {
      if (timeout_ != timeout) {
         timeout_ = timeout;
         modem_.setTimeout(timeout_);
      }
   }
   /** Get the current speed [bps] */
   int  speed () { return speed_;  }
   /** Set the speed [bps] */
   void speed (int speed) {
      if (speed_ != speed) {
         speed_ = speed;
         modem_.setSpeed(speed_);
      }
   }
   /**@} */

   /** @name Public API of the class */
   /**@{ */
   /** Open a communication channel with a URL */
   boolean open (String url) { return modem_.open(url); }
   /** Open a communication channel with Ithaki */
   boolean open ()           { return modem_.open(URL_DEFAULT); }
   /** Close the connection */
   boolean close()           { return modem_.close(); }

   /**
    * @grief The basic communication functionality
    * 
    * This function sends a request to server and return the response. In order to 
    * do that, the main reading loop seeks for the starting pattern and use the data
    * from that point up to the ending pattern. This is very helpful for binary incoming
    * data.
    * @note
    *    Blocking mode
    * @param data    Reference to Transaction data to use @see Transaction
    * @param code    The request code to send
    * @param begin   The starting pattern of the response data
    * @param end     The ending pattern of the response data
    * @param bin     A flag to indicate that the data are in binary form
    * @return        Reference to Transaction object with the response data. @see Transaction
    */
   Transaction request (Transaction data, byte[] code, byte[] begin, byte[] end, boolean bin) {
      int ch =0;
      boolean have_begin = (begin != null) ? false : true;
      boolean incoming   = false;

      // Clear the receive buffer first
      _clear (data.response, 0, data.response.length);
      if (code != null) {
         // if we have a request
         data.code = code;          // store the request code for the transaction
         modem_.write(data.code);   // send the code and mark the time
         modem_.write((int)'\r');
         data.departure = System.currentTimeMillis();// - (long)((8*(data.code.length+1))*(1000.0/speed_));
      }

      // main receive loop
      data.size =0;
      do {
         // escape guard for memory protection
         if (data.size >= data.response.length) {
            data.size =0;
            return data;
         }
         // read the data byte from server
         try {
            ch = modem_.read();
         }
         catch (Exception e) {
            // Oops!!!! something went wrong. Break and return :(
            System.err.println (e.getMessage());
            data.size =0;
            return data;
         }
         if (!incoming) {
            // first byte triggered. mark the time
            incoming = true;
            data.arrival = System.currentTimeMillis();// - (long)(8*(1000.0/speed_));
         }
         data.response [data.size++] = (byte)ch;   // store the byte
         
         if (!have_begin && (detect(data.response, begin, data.size) != -1)) {
            // If begin pattern detected, start over.
            _clear(data.response, 0, data.size);
            data.size = _copy (data.response, begin);
            have_begin = true;
         }
      } while (
         ch != -1 && (
            !have_begin || (
                  (bin || (detect (data.response, "\r\n\n\n".getBytes(), data.size) == -1))
               && (bin || (detect (data.response, "NO CARRIER".getBytes(), data.size) == -1))
               && (detect (data.response, end, data.size) == -1)
            )
         )
      );
      /*^
       * We loop until:
       *    1) server returns -1
       *    2) A end pattern arrives 
       *    3) A "\r\n\n\n" sequence arrives (in character mode, not for binary files)
       *    4) A "NO CARRIER" sequence arrives (in character mode, not for binary files)
       */
      return data;
   }

   /** 
    * @brief
    *    A pattern detect functionality.
    *    
    * Search in data input for the pattern and return its position if found.
    * @note
    *    O(n^2) in time.
    * @param data       The data buffer to search into
    * @param pattern    The pattern to search for
    * @param max        The maximum length to search
    * @return           The position of pattern in data, or -1 if not found
    */
   static int detect (byte[] data, byte[] pattern, int max) {
      if (pattern != null) {
         for (int i =0 ; i<max && i<data.length - pattern.length; ++i) {
            boolean detected = true;
            for (int j=0 ; j<pattern.length ; ++j) {
               if (data[i+j] != pattern[j]) {
                  detected = false;
                  break;
               }
            }
            if (detected)
               return i;
         }
      }
      return -1;
   }
   /**@} */

   /** @name Private helper api */
   /**@{*/
   /**
    * Clear a buffer segment
    * @param buffer     Buffer to clear
    * @param begin      The starting point
    * @param end        The ending point
    */
   private void _clear (byte[] buffer, int begin, int end) {
      for (int i=begin ; i<end && i<buffer.length ; ++i)
         buffer[i] = 0;
   }

   /**
    * Copy a buffer to another
    * @param dest    The destination buffer
    * @param src     The source buffer
    * @return        The number of bytes copied
    */
   private int _copy (byte[] dest, byte[] src) {
      if (dest.length >= src.length) {
         for (int i=0 ; i<src.length ; ++i)
            dest[i] = src[i];
         return src.length;
      }
      return 0;
   }
   /**@}*/
}

/**
 * @class
 *    Class to represent client-server transaction data
 */
class Transaction {
   byte[]   code;       /** request code from client */
   byte[]   response;   /** response data from server */
   long     departure;  /** request time */
   long     arrival;    /** response time */
   int      size;       /** size of response data in bytes */

   /**
    * Basic constructor
    * @param code       Code to use
    * @param response   Response buffer to use
    */
   Transaction (byte[] code, byte[] response) {
      this.code      = code;
      this.response  = response;
      departure      = arrival = 0;
      size           = 0;
   }

   /**
    * Get a copy of the response
    */
   byte[] getResponse() {
      return Arrays.copyOf(response, size);
   }
}

