package nxm.audio.lib;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.rmi.ConnectException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;

import javax.sound.sampled.SourceDataLine;

import nxm.audio.net.CustomRMISocketFactory;
import nxm.audio.net.RemotePlayerDelegate;

/**
 * playerdaemon - this should be 16bit linear, 1 channel input
 * Stand-Alone or CLASS instantiated AudioPlayer
 * Now that it's in the NeXtMidas baseline, we need to test the RMI portion.
 * Make sure RMI is optional throughout : LMT 09/23/2005
 *
 * @author MSZ (Zeta)
 * @author Lisa Tursky (TMA)
 */
public class playerdaemon implements Runnable, Serializable {
  private static final long serialVersionUID = 4487655833304615810L;
  private static hwplayer hwp;
  /** A TreeMap of channels to IP addresses associated with DataReaders */
  private TreeMap<Integer,String> readerLookup = new TreeMap<>();
  private transient Map<Integer,DataReader> readers = Collections.synchronizedMap(new TreeMap<Integer,DataReader>());
  /** A TreeMap of all available channels */
  private TreeMap<Integer,String> openChannels = new TreeMap<>();
  private static int maxChannels;
  private static String rmiHost="127.0.0.1";
  private static int rmiPort = 1099;
  /** Is debug mode being used? */
  private boolean DEBUG=false;
  /** Run the player, enabled by default */
  private boolean run=true;
  /** The decode table used for mulaw-formatted audio data */
  private short[] decodeTable;
  private static Thread daemon = null;
  private static playerdaemon play = null;

  /** The associated Remote Player object */
  private RemotePlayerDelegate playerDelegate;

  /** Toggle verbose output
   *  @param ptemp whether or not to use verbose output
   */
  public void setVerbose(boolean ptemp) { DEBUG=ptemp; }
  /** Get the hardware player
   *  @return the hardware player
   */
  public static hwplayer getPlayer() { return hwp; }

  /** Toggle socket debugging for all channels
   *  @param ptemp whether or not to use socket debugging
   */
  public void setSocketDebug(boolean ptemp) {
    try {
      for (Integer key : readers.keySet()) {
        DataReader dr = readers.get(key);
        dr.setVerbose(ptemp);
        readers.put(key, dr);
      }
    } catch (Exception e) { e.printStackTrace(); }
  }

  /** Toggle socket debugging for a given channel
   *  @param ptemp   whether or not to use socket debugging
   *  @param channel the given channel
   */
  public void setSocketDebug(boolean ptemp,int channel) {
    try {
      Integer key = Integer.valueOf(channel);
      DataReader dr = readers.get(key);
      dr.setVerbose(ptemp);
      readers.put(key, dr);
    } catch (Exception e) { e.printStackTrace(); }
  }

  /** Toggle packet debugging for all channels
   *  @param ptemp whether or not to use packet debugging
   */
  public void setPacketDebug(boolean ptemp) {
    try {
      for (Integer key : readers.keySet()) {
        DataReader dr = readers.get(key);
        dr.setPacketDebug(ptemp);
        readers.put(key, dr);
      }
    } catch (Exception e) { e.printStackTrace(); }
  }

  /** Toggle packet debugging for a given channel
   *  @param ptemp   whether or not to use packet debugging
   *  @param channel the given channel
   */
  public void setPacketDebug(boolean ptemp,int channel) {
    try {
      Integer key = Integer.valueOf(channel);
      DataReader dr = readers.get(key);
      dr.setPacketDebug(ptemp);
      readers.put(key, dr);
    } catch (Exception e) { e.printStackTrace(); }
  }

  /** Toggle RTP for all channels
   *  @param ptemp whether or not to use RTP
   */
  public void setRTP(boolean ptemp) {
    try {
      for (Integer key : readers.keySet()) {
        DataReader dr = readers.get(key);
        dr.setRTP(ptemp);
        readers.put(key, dr);
      }
    } catch (Exception e) { e.printStackTrace(); }
  }

  /** Toggle RTP for a given channel
   *  @param ptemp   whether or not to use RTP
   *  @param channel the given channel
   */
  public void setRTP(boolean ptemp,int channel) {
    try {
      Integer key = Integer.valueOf(channel);
      DataReader dr = readers.get(key);
      dr.setRTP(ptemp);
      readers.put(key, dr);
    } catch (Exception e) { e.printStackTrace(); }
  }

  /** Set the timeout value for all channels
   *  @param ptimeout the timeout value in milliseconds
   */
  public void setTimeOut(int ptimeout) {
    try {
      for (Integer key : readers.keySet()) {
        DataReader dr = readers.get(key);
        dr.setTimeOut(ptimeout);
        readers.put(key, dr);
      }
    } catch (Exception e) { e.printStackTrace(); }
  }

  /** Set the timeout value for a given channel
   *  @param ptimeout the timeout value in milliseconds
   *  @param channel  the given channel
   */
  public void setTimeOut(int ptimeout,int channel) {
    try {
      Integer key = Integer.valueOf(channel);
      DataReader dr = readers.get(key);
      dr.setTimeOut(ptimeout);
      readers.put(key, dr);
    } catch (Exception e) { e.printStackTrace(); }
  }

  /** Set the host from which data is read for a given channel
   *  @param host    the host address
   *  @param channel the given channel
   */
  public void setHost(String host, int channel) {
    try {
      Integer key = Integer.valueOf(channel);
      DataReader dr = readers.get(key);
      dr.setHost(host); //only sets host at init
      readers.put(key, dr);
    } catch (Exception e) { e.printStackTrace(); }
  }

  /** Set the port from which data is read for a given channel
   *  @param port    the port number
   *  @param channel the given channel
   */
  public void setPort(int port, int channel) {
    try {
      Integer key = Integer.valueOf(channel);
      DataReader dr = readers.get(key);
      dr.setPort(port); //only sets port at init
      readers.put(key, dr);
    } catch (Exception e) { e.printStackTrace(); }
  }

  /** Set the host, port, audio type, and connection state for a given channel
   *  @param host      the host address
   *  @param port      the port number
   *  @param channel   the given channel
   *  @param audiotype the audio type
   *  @param on        whether or not to open a new connection or leave the current connection open
   */
  public void setHostPort(String host,int port,int channel,String audiotype,boolean on) {
    try {
      if (DEBUG) System.out.println("playerdaemon: (setHostPort) "+host+"/"+port+".type:"+audiotype+".chan:"+channel);
      Integer key = Integer.valueOf(channel);
      DataReader dr = readers.get(key);
      dr.setLine(null);
      dr.close(); //shuts down reading socket and run loop
      dr.setHost(host);
      dr.setPort(port);
      int audiotypenum=1;
      if (audiotype!=null && audiotype.compareToIgnoreCase("ULAW")==0) audiotypenum=0;
      dr.setAudioType(audiotypenum);
      readers.put(key,dr);
      if (on) setOff(false,channel);
      else setOff(true,channel);
    } catch (Exception e) { e.printStackTrace(); }
  }

  //for testing purposes, run it in its own thread
  /** Main driver to run playerdaemon as an instance
   *  @param args the commandline arguments
   */
  public static void main(String []args) {
    try {
      play = new playerdaemon();
      daemon = new Thread(play);
      daemon.setPriority(Thread.currentThread().getPriority() - 1);
      daemon.start();
    } catch (Exception e) { e.printStackTrace(); }
  }

  //if we don't always want to do this, we can add another command line arg
  //as a flag.
  private void registerPlayer() {
    /*************************************************************************
  This is not  necessary right now because we are using RMI locally only.
  When you do not install a security manager, all remote classes must be
  found on the local classpath. Once we go to remote RMI with the registry
  located on the web server, we will need to install a security manager.

       if(System.getSecurityManager() == null) {
           System.setSecurityManager(new RMISecurityManager());
       }
     *************************************************************************/

    //host & port in name represents host of RMI registry
    //String name = "//137.20.69.227:1099/HWPlayer:137.20.69.227";
    //String rmiHost = System.getProperty("java.rmi.hostname");
    //String rmiPort = System.getProperty("java.rmi.port");
    StringBuffer nameBuf = new StringBuffer("//");
    nameBuf.append(rmiHost);
    nameBuf.append(":");
    nameBuf.append(rmiPort);
    nameBuf.append("/HWPlayer:");
    nameBuf.append("localhost");
    //TESTING--LMT
    //String name = nameBuf.toString();
    String name = "HWPlayer";
    if (DEBUG) System.out.println("binding player with name: " + name);

    try {
      playerDelegate = new RemotePlayerDelegate(this);
      Registry reg = getRMIRegistry();
      reg.rebind(name, playerDelegate);

    } catch(Exception e) {
      e.printStackTrace();
    }
  }

  private Registry getRMIRegistry() {
    Registry reg = null;
    boolean retry = true;
    while (retry == true) {
      try {
        //get Registry
        reg = LocateRegistry.getRegistry(rmiHost,rmiPort,new CustomRMISocketFactory());
        //test communication with Registry
        reg.list();
        //If no exceptions thrown by above two methods, stop retrying
        retry = false;

      } catch (ConnectException ce) {
        //Registry has not yet initialized
        retry = true;
      } catch (RemoteException re) {}
    }
    return reg;
  }

   /** Adds a DataReader for each channel to the array.
    *  TBD -- the return value is not useful for adding multiple channels? Modify?
    *  @param ihost input host name/address
    *  @param iport input host port
    *  @param audiotype type of audio
    *  @param pbufsize size of buffer
    *  @param numChannels number of channels
    *  @return the index of the last channel added
   */
  public int addReader(String ihost, int iport, String audiotype, int pbufsize, int numChannels) {

    int index = -1;
    String val = ihost + ":" + iport;
    int currentSize = readers.size();
    if ((currentSize + numChannels) > maxChannels) {
      System.err.println("Cannot open " + ihost + ":" + iport + "...");
      System.err.println("Insufficient number of open channels remaining...");
    } else {
      int audiotypenum=1;
      if (audiotype!=null && audiotype.compareToIgnoreCase("ULAW")==0) audiotypenum=0;
      DataReader dataReader = null;
      if (DEBUG) System.out.println("addReader host: " + ihost+".port="+iport+".Type="+audiotype+". size="+pbufsize+".chans="+numChannels+". num="+audiotypenum);

      for (int i=0; i<numChannels; i++) {
        Integer key = openChannels.firstKey();

        //make sure channel is initialized
        SourceDataLine line = hwp.addChannel(key.intValue(), null, pbufsize); //null=AudioFormat to use default
        dataReader = new DataReader( key.intValue(), decodeTable, audiotypenum, line);
        dataReader.open(ihost,iport+i);
        dataReader.start();

        readers.put(key, dataReader);
        openChannels.remove(key);
        val = ihost + ":" + (iport+i);
        readerLookup.put(key, val);
        index = key.intValue();

      }
    }
    return index;
  }

  /** Get the host and port that data is being read from on a given channel
   *  @param channel the given channel
   *  @return the host and port that data is being read from
   */
  public String getHostPort(int channel) {
    DataReader dr = readers.get(Integer.valueOf(channel));
    StringBuffer hostPort = new StringBuffer(dr.getHost());
    hostPort.append(":");
    hostPort.append(dr.getPort());
    return hostPort.toString();
  }

  /** Constructor that will register player with RMI. */
  public playerdaemon() {
    this(true);
  }

  /** Use this constructor to set up the playerdaemon's audio type and set up
   *  DataReaders sufficient for the maximum number of channels
   *  the hwplayer can handle. Make subsequent calls to the addReader() method
   *  to add DataReaders for different hosts/ports.
   *  @param useRMI true to use RMI
   */
  public playerdaemon(boolean useRMI) {
    try {
      if (DEBUG) System.out.println("Opening hwplayer");
      hwp = new hwplayer();
      hwp.open();
      if (useRMI) registerPlayer();
      maxChannels = hwp.getMaxLines();
      readers = new TreeMap<>();

      initializeOpenChannels();
      decodeTable = build_mu_decode_table();

      if (hwp==null) {
        System.err.println("Error-Could not open audio player");
        return;
      }

    } catch(Exception e){
      System.err.println("Error creating:"+e);
      e.printStackTrace();
    }
    if (DEBUG) System.out.println("DONE Opening hwplayer");
  }

  /** Remove the DataReader on a given channel
   *  @param channel the given channel
   */
  public void removeReader(int channel) {
    if (readers==null) return;
    synchronized(readers) {
      Integer key = Integer.valueOf(channel);
      DataReader dr = readers.get(key);
      hwp.setOff(true,channel);
      dr.shutdown();
      readerLookup.remove(key);
      readers.remove(key);
      openChannels.put(key, "");
    }
  }

  private void initializeOpenChannels() {
    for (int i = 0; i < maxChannels; i++) {
      openChannels.put(Integer.valueOf(i), "");
    }
  }

  /** Shut down the audio player */
  public void shutdown() {
    close();
  }

  /** Close the audio player */
  public void close() {
    try {
      stopPlayer();
      hwp = null;
      if (readers!=null) readers.clear();
      readers = null;
      if (daemon!=null) {
        daemon.interrupt();
        // NTN: Thread.destroy() is deprecated and the method was never implemented.
        // daemon.destroy();
      }
      daemon = null;
      try {
        Registry reg = LocateRegistry.getRegistry(rmiHost,rmiPort,new CustomRMISocketFactory());
        reg.unbind("HWPlayer");
        if (DEBUG) System.out.println("HWPLAY unbound from:"+rmiHost+":"+rmiPort);
      } catch(Exception e) {
        System.err.println("Close Unbind Exception:"+e);
      }
    } catch(Exception e) {
      System.err.println("Close Exception:"+e);
    }
  }

  /** Get the hosts and port that data is being read from on each channel
   *  @return a listing of the hosts and ports that data is being from on each channel
   */
  public String getPlayerChannelInfo() {
    StringBuffer buff = new StringBuffer();
    for (Integer key : readers.keySet()) {
      DataReader dr = readers.get(key);
      buff.append("channel: " + key + "\n");
      buff.append("host: " + dr.getHost() + "\n");
      buff.append("port: " + dr.getPort() + "\n\n");
    }
    return buff.toString();
  }

  /** Close the connections for each DataReader across all channels */
  public void stopPlayer(){
    try {
      run = false;  // Set GLOBAL run to FALSE...
      try {
        hwp.closeAll();
      } catch(Exception e) {
        System.err.println("Error stopping hwp:"+e);
        e.printStackTrace();
      }
      for (DataReader dr : readers.values()) {
        hwp.setOff(true,dr.getLineNumber());
        dr.shutdown();
      }
    } catch(Exception e) {
      System.err.println("Error stopping:"+e);
      e.printStackTrace();
    }
    if (DEBUG) System.out.println("Stopped Player");
  }

  public void run() {
    while (run) {
      try {
        Thread.sleep(200);
      } catch(InterruptedException ie) {}
    }
  }

  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));
    }
    return convbuf;
  }

  /** Toggle open connections on a given channel
   *  @param off     whether or not to close connections
   *  @param channel the given channel
   */
  public void setOff(boolean off, int channel) {
    try {
      Integer key = Integer.valueOf(channel);
      DataReader dr = readers.get(key);
      if (dr == null) {
        System.err.println("playerdaemon: setOff: no channel exits for:"+channel);
        return;
      }
      SourceDataLine line = hwp.setOff(off,channel);
      if (!off) {
        hwp.setVolume(line,dr.getVolume());
        hwp.setPan(line,dr.getPan());
        dr.setLine(line);
        dr.open(dr.getHost(),dr.getPort());
        readers.put(key,dr);
      } else {
        dr.setLine(null);
        dr.close(); //shuts down reading socket but leaves the run loop
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /** Set the pan on a given channel
   *  @param pan     the desired pan
   *  @param channel the given channel
   */
  public void setPan(float pan, int channel) {
    Integer key = Integer.valueOf(channel);
    DataReader dr = readers.get(key);
    if (dr == null) return;
    dr.setPan(pan);
    readers.put(key,dr);
    hwp.setPan(dr.getLine(),pan);
  }

  /** Set the volume on a given channel
   *  @param volume  the desired volume
   *  @param channel the given channel
   */
  public void setVolume(int volume, int channel) {
    Integer key = Integer.valueOf(channel);
    DataReader dr = readers.get(key);
    if (dr == null) return;
    dr.setVolume(volume);
    readers.put(key,dr);
    hwp.setVolume(dr.getLine(),volume);
  }

  /** Overwrite default serialization
   *
   *  @param oos Object Output Stream
   *  @throws IOException Exception thrown by writeObject
   */
  private void writeObject (ObjectOutputStream oos) throws IOException {
    oos.defaultWriteObject();
    oos.writeObject(readers);
  }

  /** Overwrite default deserialization
   *
   *  @param ois Object Input Stream
   *  @throws ClassNotFoundException Exception thrown by readObject
   *  @throws IOException Exception thrown by readObject
   */
  @SuppressWarnings("unchecked")
  private void readObject (ObjectInputStream ois) throws ClassNotFoundException, IOException {
    ois.defaultReadObject();
    readers = (Map<Integer,DataReader>)ois.readObject();
  }
}