package nxm.sys.lib;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;

import nxm.sys.inc.Debug;

/** Resource for interacting with a Socket.

    @author Jeff Schoen
 */
public class SocketResource extends IOResource implements Runnable {

  /** Socket action/state flags */
  public static final int UNTANGLE=1, CLOSED=-1, REOPEN=-2;
  InputStream  sis;         // input stream  (null=not open yet)
  OutputStream sos;         // output stream (null=not open yet)
  ServerSocket server;      // server socket for output streams
  Socket       socketToMe;  // ???
  String       host;        // host name for input streams
  int          port;        // port number
  long         skip;        // number of bytes to skip over (see seek(long) for details)
  int          timeout;     // timeout in ms
  long         soffset;     // actual stream offset

  /** default constructor */
  public SocketResource () {}

  /** Always assume the socket exists in the case of input and assume it doesn't in the case of output.
      @return true for input and false for output.
   */
  public boolean exists () { return (dir < DIR_INOUT); }

  public boolean isStream () { return true; }

  public void setURL (String str) {
    super.setURL(str);
    // peel off various header formats
    if (str.startsWith("stp://")) str = str.substring(6);
    else if (str.startsWith("stp:/")) str = str.substring(5);
    else if (str.startsWith("stp:")) str = str.substring(4);
    int i = str.indexOf(":");
    host = str.substring(0,i);
    port = Convert.s2l(str.substring(i+1));
  }

  public boolean open () {
    isOpen = false;

    double t = (qualTable == null)? 0.0 : qualTable.getD("TIMEOUT", 0.0);
    timeout = (int)(t * 1000); // convert sec -> ms
    if ((t > 0) && (timeout <= 0)) timeout = 1; // make sure it doesn't round down to 0

    try {
      if (dir > DIR_INOUT) { // output only
        server = new ServerSocket(port);
        server.setSoTimeout(timeout);
        sos = getOutputStream(false);
      } else if (dir <= DIR_INOUT) {
        sis = getInputStream(false);
      }
      isOpen = true;
    }
    catch (Exception e) {
      if (M.debug(Debug.TRACE)) M.warning("SocketResource - exception during open()" + e);
    }
    soffset = 0;
    return isOpen;
  }

  public boolean close () {
    try {
      if (sis    != null) { sis.close();    sis = null;    }
      if (sos    != null) { sos.close();    sos = null;    }
      if (server != null) { server.close(); server = null; }
      isOpen = false;
    }
    catch (IOException e) {
      isOpen = true;
    }
    return isOpen;
  }

  public InputStream  getInputStream ()  { return getInputStream(true);  }
  public OutputStream getOutputStream () { return getOutputStream(true); }

  /** Internal call to open input stream. */
  protected InputStream getInputStream (boolean indefinite)  {
    while (isOpen && (sis == null)) {
      try {
        Socket socket = new Socket(host,port);
        socket.setSoTimeout(timeout);
        sis = socket.getInputStream();
      }
      catch (SocketTimeoutException e) {
        if (!indefinite) return null;
      }
      catch (Exception e) {
        throw new MidasException("Error connecting to "+host+":"+port+": "+e,e);
      }
    }
    return sis;
  }

  /** Internal call to open output stream. */
  protected OutputStream getOutputStream (boolean indefinite) {
    while (isOpen && (sos == null)) {
      try {
        Socket socket = server.accept();
        socket.setSoTimeout(timeout);
        sos = socket.getOutputStream();
      }
      catch (SocketTimeoutException e) {
        if (!indefinite) return null;
      }
      catch (Exception e) {
        throw new MidasException("Error connecting to port "+port+": "+e,e);
      }
    }
    return sos;
  }

  /** Seek to the specified offset in socket. Sets the asking offset for the next read or write access.
      @param off The new seek offset.
   */
  public boolean seek (long off) {
    boolean ok = (off >= soffset);
    offset = off;
    return ok;
  }

  public long seek () {
    return offset;
  }

  public long avail () {
    if (sis==null) return 0L;
    try { return sis.available() + soffset - offset; }
    catch (IOException e) { return 0L; }
  }

  /** Read data from the socket. If the seek position is is set less than the number of bytes read
      so far (see {@link #seek(long)}), this will read 0's until it new bytes are readable.
      @param buf   The byte buffer to read into.
      @param boff  The byte offset into the byte array to start writing at.
      @param bytes The number of bytes to try to read.
      @return The number of bytes read or -1 at the end of the file.
   */
  public int read (byte[] buf, int boff, int bytes) {
    int togo=bytes;
    try {
      while (offset<soffset && togo>0) { 		// ask < stream offset - fill in zeros
        buf[boff++] = 0;
        togo--;
        offset++;
      }
      while (soffset<offset) {				// ask > stream offset - read off to null
        int skip = (int)Math.min(offset-soffset,bytes);
        int num  = getInputStream(true).read(buf,boff,skip);
        if (num>=0) soffset += num; else return -1;
      }
      while (togo>0) {
        int num = getInputStream(true).read(buf,boff,togo);
        if (num>=0) { togo-=num; boff+=num; offset+=num; soffset=offset; }
        else if (togo==bytes) return -1; // EOF no data
        else break;  // EOF with data
      }
    }
    catch (IOException e) { return -1; }
    return bytes-togo;
  }

  /** Writes data to the socket. If the seek position is is set less than the number of bytes written
      so far (see {@link #seek(long)}), this will discard data until until it new bytes are writable.
      @param buf   The byte buffer to write from.
      @param boff  The byte offset into the byte array to start writing from.
      @param bytes The number of bytes to try to write.
      @return The number of bytes written or -1 if none could be written (possibly a read-only source).
   */
  public int write (byte[] buf, int boff, int bytes) {
    int togo=bytes; byte[] fbuf=null;
    try {
      while (offset<soffset && togo>0) { 	// ask < stream offset - eat buffer
        boff++;
        togo--;
        offset++;
      }
      while (soffset<offset) {			// ask > stream offset - write zeros
        if (fbuf==null) fbuf = new byte[bytes];
        int fill = (int)Math.min(offset-soffset,bytes);
        int num=fill; getOutputStream(true).write(fbuf,0,fill);
        if (num>=0) soffset += num; else return -1;
      }
      while (togo>0) {
        int num=togo; getOutputStream(true).write(buf,boff,togo);
        if (num>=0) { togo-=num; boff+=num; offset+=num; soffset=offset; }
        else if (togo==bytes) return -1; // EOF no data
        else break;  // EOF with data
      }
    }
    catch (IOException e) { return -1; }
    return bytes-togo;
  }

  /** connect to multiple sockets with optional timeout and untangle
   *  @param host    address
   *  @param port    port
   *  @param nsock   number of sockets
   *  @param timeout time out value
   *  @param flags   flags (checks for UNTANGLE)
   *  @return        sockets
   */
  public Socket[] connectSockets (String host, int port, int nsock, int timeout, int flags) {
    this.host = host;
    this.port = port;
    Socket[] socks = new Socket[nsock];

    try {
      for (int i=0; i<nsock; i++) {
        Thread thread = new MidasThread(M,this);
        thread.start();
        thread.join(timeout);
        if (thread.isAlive()) {
          thread.interrupt();
          Thread.sleep(500);  // wait for thread to write err message
          return null;
        }
        socks[i] = socketToMe;
      } // end for loop
      if (nsock==2 && (flags&UNTANGLE)!=0) {
        byte[] bufi = new byte[16];
        byte[] bufo = new byte[16];
        socks[0].getInputStream().read(bufi);
        socks[1].getInputStream().read(bufo);
        if ((char)bufi[0] != 'S') {
          Socket stmp = socks[0];
          socks[0] = socks[1];
          socks[1] = stmp;
        }
      }
      return socks;
    }
    catch (Exception e) { throw new MidasException("Connect Sockets", e); }
  }

  /** Run method for executing the socket resource. */
  public void run () {
    try { socketToMe = new Socket (host,port); }
    catch (Exception e) { throw new MidasException("Socket connect", e); }
  }
}
