package nxm.sys.prim;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Random;

import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.Time;

/**
  Streams data to a device supporting Real-Time Protocol for audio.

  Currently, accepts linear 8-bit or mulaw formatted input and always
  outputs linear 16-bit data to the audio driver.

  @author Jeff Schoen
*/
public class sinkrtp extends Primitive {

  private String rthost;
  private int port,rtport,prange;
  private byte[] rtbuf;
  private short[] outbuf;
  private DataFile hin, hout;
  private Data outdata;
  private DatagramPacket rtpack;
  private DatagramSocket dsock;
  private int reopen=-1;
  private int reopen_tries=0;
  private int seq=0xabcd, ts=0x00123456, src;
  private int mode, chan=3, tl, twotl;
  private double time, tdelay, tlast=0;
  private static String modeList="LEFT,RIGHT,BOTH,NONE,RESYNC";
  private boolean sockisok=false, selfpace=false;
  private double scale=1.0;
  private int type;
  private short[] decodeTable;
  private byte[] inbuf;
  private Data inint;
  private short[] inshort;

  private static final String typeList = "LIN8,MULAW"; //LIN16 ???
  private static final int T_LIN8=1, T_MULAW=2;
  private static final int RTP_PT_LIN8=11, RTP_PT_LIN16=13, RTP_PT_PCMU=0;
  private static final int RTP_VERSION=2, RTP_BYE=203, RTP_CC=1;
  private static final int VER=0, SEQ=2, TS=4, SSRC=8, CHAN=12, DATA=16;
  private static final int PLEN=2048;

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

  public int open() {
   try {
    // get parameters
    rthost = MA.getS("HOST").toLowerCase();
    rtport = MA.getL("PORT");

    // get switches
    tl = MA.getL("/TL",PLEN);
    twotl = tl*2;
    port = MA.getL("/PORT",7101);
    prange = MA.getL("/PRANGE",8);

///    reopen = MA.getState("/REOPEN",false);
    // Handle legacy behavior of sinkrtp/reopen without a value
    String tmpStr = null;
    if (MA.isPresent("/REOPEN")) {
      tmpStr = MA.getS("/REOPEN");
      if (tmpStr.equals("")) { reopen = 5; }
      else                   { reopen = MA.getL("/REOPEN", reopen); }
    }

    Random rng = new Random();
    src = Math.abs(rng.nextInt());
    seq = src & 0xFFFF;

    // make the static part of the header only when socket is null
    if (dsock==null) {
      type = MA.getChoice("/TYPE",typeList,1);
      mode = MA.getChoice("/MODE",modeList,3);
    }

    // connect to Datagram port
    InetAddress rtaddr = null;
    while (dsock==null) {
     try {
      dsock = new DatagramSocket(port);
      M.info ("Connected to RTP Socket port");
      rtaddr = InetAddress.getByName(rthost);
     } catch (Exception e) {
      M.warning ("Opening socket: "+e.getMessage());
      if (--prange==0) return (FINISH);
      port++;
     }
    }

    // get input file
    hin = MA.getDataFile("IN","1000","S*",0); //MULAW in SB, RAW in SI
    if (hin==null) {
      M.info("Invalid Input File");
      return(NORMAL);
    }
    hin.open();
    tdelay = tl * hin.getXDelta();
    selfpace = hin.isFile();

    inbuf = new byte[tl];
    inint = hin.getDataBuffer(tl,INT);
    outbuf = new short[tl];
    decodeTable = build_mu_decode_table();
    rtbuf  = new byte[DATA+twotl];     //buffer must be big enough for packetized lin16 data as bytes
    rtpack = new DatagramPacket(rtbuf,rtbuf.length,rtaddr,rtport);
    makeRTPHeader();

    // get output file
    if (MA.find("OUT")) {
      hout = MA.getDataFile("OUT",hin,0);
      hout.setFormat("SI");
      hout.setType(1000);
      hout.open();
      outdata = new Data(hout.getFormat(),tl);
    }
   } catch (Exception e) {
     M.printStackTrace("Open error:",e);
   }
   return (NORMAL);
  }

  public int process() {
    // JC4: We may want to reconsider how exceptions are handled in this method, the three-deep
    // nesting of try/catch blocks seems unnecessary. If there is a good reason for it, we should
    // have a comment documenting that.
   try {
    int n;
    short newval=0,adjval;
    if (hin==null) { M.info("Null Input"); return(NOOP); }

    if (type==T_MULAW) n=hin.read(inbuf,0,tl);
    else {
      if (inint==null) M.warning("Null in ints");
      n=hin.read(inint); //n is in element so n*2 bytes
    }

    if (n==0) return (NOOP);
    if (n<0) return (FINISH);

    if (type==T_MULAW) {
      for (int i=0,j=0; i<n; i++,j+=2) {
        newval = decodeTable[inbuf[i]+128];
        adjval = adjustVolume(newval,scale);
        pack(rtbuf,DATA+j,adjval,2);
        if (hout != null) outbuf[i] = adjval;
      }
    } else {
      inshort=inint.castI(false);
      for (int i=0,j=0; i<n; i++,j+=2) {
        adjval = adjustVolume(inshort[i],scale);
        pack(rtbuf,DATA+j,adjval,2);
        if (hout != null) outbuf[i] = adjval;
      }
      inint.uncast(inshort,true);
    }

    if (selfpace) {
      time=Time.current();
      while ( (time-tlast) < (tdelay-.001) ) {
        Time.sleep(0.001);
        time=Time.current();
      }
      tlast = time;
    }

    // send a new packet
    if (mode!=4 && sockisok) try {
      pack (rtbuf,SEQ,seq,2);
      pack (rtbuf,TS,ts,4);
      dsock.send(rtpack);
      ts+=tl; seq++;
    } catch (java.io.InterruptedIOException e) {
      // never return NOOP - were waiting in the receive() call
    } catch (Exception e) {
      // Don't print stack trace since we will be doing a retry
      M.warning("SinkRtp: problem in socket write: "+e.getMessage());
      sockisok = false;
      if ( (reopen > 0) && reopen_tries < reopen) {
        dsock.close();
        try {
          dsock = new DatagramSocket(port);
          sockisok = true;
          reopen_tries = 0;
        } catch (Exception ex) {
          // ignore exception, just count retries
          reopen_tries++;
        }
      }
    }

    if (hout != null) {
      outdata.fromArray(outbuf);
      hout.write(outdata);
    }
   } catch (Exception e) {
     M.printStackTrace("Process error:",e);
   }
   return (NORMAL);
  }

  public int close() {
    try {
      rtbuf[VER+0] = (byte)(RTP_VERSION<<6 | RTP_CC);
      rtbuf[VER+1] = (byte)(RTP_BYE);
      pack (rtbuf,2,2,2);
      pack (rtbuf,4,src,4);
      rtpack.setPort(rtport+1);
      rtpack.setLength(8);
      dsock.send(rtpack);
    } catch (Exception e) {
      M.printStackTrace("Close error", e);
    }
    if (dsock !=null) dsock.close();
    if (hin  != null) hin.close();
    if (hout !=null)  hout.close();
    return (NORMAL);
  }

  // JC4: We should consider replacing this with a call to Convert.packB(..) or java.util.Arrays.fill(..)
  private void pack (byte[] buf, int boff, int value, int bytes) {
    for (int i=bytes-1; i>=0; i--) {
      buf[boff+i] = (byte)(value&0xFF);
      value = (value>>8);
    }
  }

  private void makeRTPHeader() {
    rtbuf[VER+0] = (byte)(RTP_VERSION<<6 | RTP_CC);
    rtbuf[VER+1] = (byte)(RTP_PT_LIN16);
    rtpack.setLength(DATA+twotl);
    pack (rtbuf,SSRC,src,4);
    pack (rtbuf,CHAN,chan,4);
    setMode(mode);
  }

  /** Set input audio data type
   *  @param value the input audio data type
   */
  public void setType (String value) {
    setType(Parser.find(typeList,value,type,0,Parser.E_WARNING)); // Changed to throw warning for Bug 760
  }
  private void setType(int value) {
    this.type = value;
    sockisok = true;
  }

  /** Set RTP mode
   *  @param value the RTP mode
   */
  public void setMode (String value) {
    setMode(Parser.find(modeList,value,mode,0,Parser.E_WARNING)); // Changed to throw warning for Bug 760
  }
  private void setMode (int value) {
    if (value==5) { seq ^= 0x8000; return; }
    if (value==4) chan=0;
    else chan = value;
    this.mode = value;
    pack (rtbuf,CHAN,chan,4);
    sockisok = true;
  }
  /** Get RTP mode
   *  @return the RTP mode
   */
  public String getMode() { return Parser.get(modeList,mode); }

  /** Set RTP volume.
   *  Value is optimally set in the range of -10 to 10.
   *  @param value the RTP volume
   */
  public void setVolume (double value) {
    scale = Math.pow(10.0,(value/10.0));
  }

  private short adjustVolume (short val, double scale) {
    short adjval;
    double newvol;

    if (scale==1.0) adjval=val;
    newvol=(double)val*scale;
    if (newvol<-32768.0)
      adjval = -32768;
    else if (newvol>32767.0)
      adjval = 32767;
    else
      adjval = (short)newvol;

    return adjval;
  }

  private short[] build_mu_decode_table() {
    short[] convbuf = new short[256];

    for (int i=0; i<256; i++) {
      int j = i-128;
      int x = j^127;
      int S = 1-(2*((x&128)/128));
      int L = (x&112)/16;
      int V = x&15;
      convbuf[i] = (short) Math.round(((S*(Math.pow(2,L)*(2*V+33)-33)*32767.0)/8031.0));
      //System.out.println("VAL="+convbuf[i]);
    }

    return convbuf;
  }

}
