package nxm.audio.lib;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;

import nxm.audio.net.RemotePlayerDelegate;

/**
  Plays 16 bit linear, 1 channel, 8000 Samples/second PCM Audio to sound card

  @author Wayne G. Dengel II  (Zeta Associates)
  @author Modified by MSZ
  @author Modified by Lisa Tursky
  @since NeXtMidas 2.1.0 (June 2005)
*/
public class hwplayer implements Serializable {
  private static final long serialVersionUID = -8957109363449453351L;
  private transient Mixer mixer = null;
  private transient javax.sound.sampled.AudioFormat format = null;
  private transient DataLine.Info lineInfo = null;
  private transient SourceDataLine[] lines = null;

  /** The current SourceDataLine */
  private int curline=-1;
  /** The maximum number of supported SourceDataLines */
  private int maxlines=0;
  /** The default maximum number of bytes that can be written */
  private int defaultBufferSize=16000; //for 16 bit lin, 1 channel, 8000/sec this is 1 second
  /** The sample size */
  private int sampleSizeInBits=16;
  /** The number of channels */
  private int channels=1;
  /** The sample rate */
  private float sampleRate=8000;
  /** Is the data signed? */
  private boolean signed = true;
  /** Is the byte order big-endian? */
  private boolean bigEndian = false;

  /** Is debug mode being used? */
  private boolean DEBUG=false;

  /** The number of bytes of data available to play */
  private int playla;
  /** The offset in which to begin playing data from */
  private int playoffset;
  /** The amount of data to play */
  private int playoutsize;

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

  /** Instantiate a new hwplayer */
  public hwplayer() {}
  /**
     Ensures that the audio format of the data is supported by the audio resources of the workstation
     on which the hwplayer is running.
   */
  public void open() {
    try {
      Mixer.Info[] minfo = javax.sound.sampled.AudioSystem.getMixerInfo(); //gets set of mixer objects available
      if (DEBUG) {
        System.out.println("Mixer count="+minfo.length);
        for (int i=0;i<minfo.length;i++) {
          System.out.println("Mixer"+i+" ="+minfo[i]);
        }
      }
      mixer = javax.sound.sampled.AudioSystem.getMixer(minfo[0]); //get first one for now
      try {
        format = new javax.sound.sampled.AudioFormat(sampleRate,sampleSizeInBits,channels,signed,bigEndian);
        if (DEBUG) {
          System.out.println("AudioFormat: " + format);
          System.out.println("channels: " + format.getChannels());
          System.out.println("sampleRate: " + sampleRate);
          System.out.println("sampleSizeInBits: " + sampleSizeInBits);
          System.out.println("signed: " + signed);
          System.out.println("bigEndian: " + bigEndian);
          System.out.println(" framesize: " + format.getFrameSize());
          System.out.println(" framerate: " + format.getFrameRate());
          System.out.println("***********************************");
        }
      } catch (Exception e) {
        System.out.println("Error opening format:"+format+". Exception="+e);
        e.printStackTrace();
      }
      if (DEBUG) System.out.println("Default Buffsize="+defaultBufferSize);

      lineInfo = new DataLine.Info(SourceDataLine.class,format);
      maxlines = mixer.getMaxLines(lineInfo);
      if (maxlines>0) lines = new SourceDataLine[maxlines];
      if (DEBUG) {
        System.out.println("Max lines="+mixer.getMaxLines(lineInfo)+". Max buffer="+lineInfo.getMaxBufferSize()+
                           ". Min Buffer="+lineInfo.getMinBufferSize()+". No limit="+javax.sound.sampled.AudioSystem.NOT_SPECIFIED);
      }

      if (!javax.sound.sampled.AudioSystem.isLineSupported(lineInfo)) System.out.println("Line matching "+lineInfo+" not supported.");

    } catch (Exception e) {
      System.out.println("HWPlayer Open Failed: " + e);
      e.printStackTrace();
    }
  }

  /**
     Sets the sample rate used in determining the audio format.
     @param psampleRate the sample rate (the number of samples played per second)
   */
  public void setSampleRate(float psampleRate) { sampleRate=psampleRate; }
  /**
     Returns the sample rate
     @return the sample rate
   */
  public float getSampleRate() { return sampleRate; }
  /**
     Sets the number of audio chanenls
     @param pchannels The number of audio channels in the audio format (1 for mono, 2 for stereo).
   */
  public void setChannels(int pchannels) { channels=pchannels; }
  /**
     Returns the number of channels in the audio format
     @return the number of channels (1 for mono, 2 for stereo)
   */
  public int getChannels() { return channels; }
  /**
     Sets the audio format's signed flag
     @param psigned indicates whether the data is signed or unsigned
   */
  public void setSigned(boolean psigned) { signed=psigned; }
  /**
     returns the signed flag value
     @return the signed flag value (true if the audio data is signed, false otherwise)
   */
  public boolean getSigned() { return signed; }
  /**
     Sets the bigEndian flag
     @param pbigEndian indicates whether the data for a single sample is stored in big-endian byte
     order (<CODE>false</CODE> means little-endian)
   */
  public void setBigEndian(boolean pbigEndian) { bigEndian=pbigEndian; }
  /**
     returns the value of the bigEndian flag
     @return true if the byte order is big-endian, false if it is little-endian
   */
  public boolean getBigEndian() { return bigEndian; }
  /**
     Sets the default buffer size
     @param pbufferSize the desired buffer size in bytes
   */
  public void setDefaultBufferSize(int pbufferSize) { defaultBufferSize=pbufferSize; }
  /**
     Returns the default buffer size
     @return the default buffer size, in bytes
   */
  public int getDefaultBufferSize() { return defaultBufferSize; }
  /**
     Sets the default audio format
     @param pformat the desired audio format
   */
  public void setDefaultFormat(javax.sound.sampled.AudioFormat pformat) { format=pformat; }
  /**
     Sets the default audio format
     @param psampleRate the number of samples per second
     @param psampleSizeInBits the number of bits in each sample
     @param pchannels the number of channels (1 for mono, 2 for stereo)
     @param psigned indicates whether the data is signed or unsigned
     @param pbigEndian indicates whether the data for a single sample is stored in big-endian byte order
     (false means little-endian)
   */
  public void setDefaultFormat(float psampleRate,int psampleSizeInBits,int pchannels,boolean psigned,boolean pbigEndian) {
    format = new javax.sound.sampled.AudioFormat(psampleRate,psampleSizeInBits,pchannels,psigned,pbigEndian);
  }

  /**
     Sets the RemotePlayerDelegate for this hwplayer. (For use with RMI interface with mixer--TBD).
     @param delegate The RemotePlayerDelegate
   */
  public void setPlayerDelegate(RemotePlayerDelegate delegate) { playerDelegate=delegate; }
  /**
     Sets the debug flag for this player.
     @param pdebug if true, debug statements are printed to standard out.
   */
  public void setDebug(boolean pdebug) { DEBUG=pdebug; }
  /**
     Gets the maximum number of SourceDataLines that is supported by the audio device on the current workstation.
     @return The maximum number of lines.
   */
  public int getMaxLines() { return maxlines; }

  /**
     Obtains the maximum number of bytes of data that can be written to the first SourceDataLine.
     This is a convenience method equivalent to <code>getBufferSize(0)</code>.
     @return the size of the buffer in bytes
   */
  public int getBufferSize() { return getBufferSize(0); }
  /**
     Obtains the maximum number of bytes of data that can be written to the specified SourceDataLine.
     @param linenum the line number of the SourceDataLine
     @return the size of the buffer in bytes
   */
  public int getBufferSize(int linenum) { return lines[linenum].getBufferSize(); }
  /**
     Obtains the SourceDataLine specified by the given line number.
     @param linenum the line number
     @return the SourceDataLine
   */
  public SourceDataLine getSourceDataLine(int linenum) { return lines[linenum]; }

  /**
     Determines whether the indicated line number is open.
     @param linenum the line number
     @return true if the line is open, otherwise false
   */
  public boolean isOpen(int linenum) {
      if (linenum>=0 && linenum<lines.length && lines[linenum]!=null)
          return lines[linenum].isOpen();
      else return false;
  }

  /**
     Obtains a string representation of this hwplayer.
     @return the string representation of this hwplayer.
   */
  public String toString() {
      return "hwplayer-Left/Right=PCM_SIGNED-"+sampleRate+"-"+
             sampleSizeInBits+"-"+channels+"-"+ signed+"-"+bigEndian+"-"+defaultBufferSize;
  }

  /**
     Add another line with the specified line number
     @param linenum the line number
     @return the new SourceDataLine
   */
  public SourceDataLine addChannel(int linenum) { return addChannel(linenum,null,defaultBufferSize); }
  /**
     Add another line with the specified line number
     @param linenum the line number
     @param bufsize the buffer size
     @return the SourceDataLine
   */
  public SourceDataLine addChannel(int linenum,int bufsize) { return addChannel(linenum,null,bufsize); }

  /**
     Add another line with the specified line number
     @param linenum the line number
     @param pformat the audio format
     @param bufsize the buffer size
     @return the SourceDataLine
   */
  public SourceDataLine addChannel(int linenum,javax.sound.sampled.AudioFormat pformat,int bufsize) {
    try {
      if (DEBUG) System.out.println("addChannel: linenum="+linenum+". bufsize="+bufsize+". Format="+pformat);
      if (linenum>maxlines) {
        System.out.println("Too many Audio Lines. Max="+maxlines+" and requested="+linenum);
        return null;
      }
      if (pformat==null) lineInfo = new DataLine.Info(SourceDataLine.class,format,bufsize);
      else lineInfo = new DataLine.Info(SourceDataLine.class,pformat,bufsize);
      if (DEBUG) System.out.println("Bufsize="+lineInfo.getMinBufferSize()+". Max="+lineInfo.getMaxBufferSize()+".linenum="+linenum);
      lines[linenum]=(SourceDataLine)mixer.getLine(lineInfo);
      if (lines[linenum]!=null) lines[linenum].open(format,bufsize);
      if (lines[linenum].isOpen()) lines[linenum].start();
      else System.out.println(linenum+" line not opened");
      if (DEBUG) {
        System.out.println("Buffer after start="+lines[linenum].getBufferSize());
        System.out.println("available after start="+lines[linenum].available());
      }
    } catch (LineUnavailableException e) {
      System.out.println("Unable to open the line: " + e);
    }
    return lines[linenum];
  }

  /**
     Closes all audio lines and then closes the hwplayer.
   */
  public void shutdown() {
    closeAll();
    System.exit(0);
  }

  /** Close
      @param line source data line to close
   */
  public void close(SourceDataLine line) {
    try {
      if (DEBUG) System.out.println("Releasing AudioLine");
      if (line!=null && line.isOpen()) {
        line.stop();
        line.close();
      }
    } catch (Exception e) {
      System.out.println("close Failed: " + e);
      e.printStackTrace();
    }
  }

  /** Close
      @param linenum source data line number to close
   */
  public void close(int linenum) {
    try {
      if (DEBUG) System.out.println("Releasing AudioLine:"+linenum);
      close(lines[linenum]);
    } catch (Exception e) {
      System.out.println("close Failed: " + e);
      e.printStackTrace();
    }
  }

  /** Play
      @param line source data line to play data to
      @param inBuf input buffer of data to play
      @param n length (size) of data to play
   */
  public void play(SourceDataLine line, byte[] inBuf, int n) {
    if (line==null) { System.out.println("Null Line to play"); return; }
    try {
      playla = line.available();
      if (playla<n) {
        playoffset=n-playla;
        playoutsize=playla;
      } else {
        playoffset=0;
        playoutsize=n;
      }
      if (DEBUG && playoutsize<n) System.out.println("Truncated data. Playoutsize="+playoutsize+". Len="+n);
      line.write(inBuf,playoffset,playoutsize);
    } catch (Exception e) {
       System.out.println("Play Failed: " + e);
       e.printStackTrace();
    }
  }

  /** Play
      @param inBuf input buffer of data to play
      @param n length (size) of data to play
      @param linenum source data line number to play audio to
   */
  public void play(byte[] inBuf, int n, int linenum){
    try {
      play(lines[linenum],inBuf,n);
    } catch (Exception e) {
       System.out.println("Play Failed: " + e);
       e.printStackTrace();
    }
  }

  /** Close all data lines */
  public void closeAll() {
    try {
       for (int i=0;i<curline;i++) {
         if (lines[i]!=null && lines[i].isOpen()) {
           lines[i].stop();
           lines[i].close();
         }
       }
     } catch (Exception e) {
         e.printStackTrace();
     }
  }

  /** Set Volume on all lines
      @param volume desired volume to set on all lines
   */
  public void setVolumeAll(int volume) {
    try {
       FloatControl gainControl=null;
       for (int i=0;i<curline;i++) {
         gainControl = (FloatControl) lines[i].getControl(FloatControl.Type.MASTER_GAIN);
         gainControl.setValue(volume);
       }
     } catch (Exception e) {
         e.printStackTrace();
     }
  }

  /** Set Volume
      @param line source data line to set volume
      @param volume desired volume to set to
   */
  public void setVolume(SourceDataLine line, int volume) {
    setVolume(line, (float) volume);
  }

  /** Set Volume
      @param line source data line to set volume
      @param volume desired volume to set to
   */
  public void setVolume(SourceDataLine line, float volume) {
     try {
       if (line!=null) {
         FloatControl gainControl = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN);
         gainControl.setValue(volume);
       }
     } catch (Exception e) {
        e.printStackTrace();
     }
  }

  /** Set Volume
      @param volume desired volume to set to
      @param linenum source data line number to adjust volume
   */
  public void setVolume(int volume, int linenum) {
    setVolume((float) volume, linenum);
  }

  /** Set Volume
      @param volume desired volume to set to
      @param linenum source data line number to adjust volume
   */
  public void setVolume(float volume, int linenum) {
     try {
        FloatControl gainControl = (FloatControl) lines[linenum].getControl(FloatControl.Type.MASTER_GAIN);
        gainControl.setValue(volume);
     } catch (Exception e) {
        e.printStackTrace();
     }
  }

  /** Get volume
      @param linenum source data line number get volume
      @return Gain control value or 0 if unable to determine gain control value
   */
  public float getVolume(int linenum) {
     try {
        FloatControl gainControl = (FloatControl)lines[linenum].getControl(FloatControl.Type.MASTER_GAIN);
        return gainControl.getValue();
     } catch (Exception e) {
        e.printStackTrace();
     }
     return 0;
  }

  /** Set desired pan control value
      @param line source data line to set pan
      @param pan desired pan control value
   */
  public void setPan(SourceDataLine line, int pan) {
    setPan(line, (float) pan);
  }

  /** Set desired pan control value
      @param line source data line to set pan
      @param pan desired pan control value
   */
  public void setPan(SourceDataLine line, float pan) {
     try {
       if (line!=null) {
         FloatControl panControl = (FloatControl)line.getControl(FloatControl.Type.PAN);
         panControl.setValue(pan);
       }
     } catch (Exception e) {
       System.out.println("Error setting Balance for line:"+line);
       e.printStackTrace();
     }
  }

  /** Set desired pan control value
      @param pan desired pan control value
      @param linenum source data line number to adjust pan
   */
  public void setPan(int pan, int linenum) {
    setPan((float) pan, linenum);
  }

  /** Set desired pan control value
      @param pan desired pan control value
      @param linenum source data line number to adjust pan
   */
  public void setPan(float pan, int linenum) {
    try {
      FloatControl panControl = (FloatControl)lines[linenum].getControl(FloatControl.Type.PAN);
      panControl.setValue(pan);
    } catch (Exception e) {
      System.out.println("Error setting Balance for linenum:"+linenum);
      e.printStackTrace();
    }
  }

  /** Get Pan control for given line number
      @param linenum source data line number to get pan
      @return Pan control or 0 if unable to determine pan control
   */
  public float getPan(int linenum) {
     try {
          FloatControl panControl = (FloatControl) lines[linenum].getControl(FloatControl.Type.PAN);
          return panControl.getValue();
     } catch (Exception e) {
        System.out.println("Error getting Pan for linenum:"+linenum);
        e.printStackTrace();
     }
     return 0;
  }

  /** SourceDataLine Constructor
      @param off true to stop/close
      @param linenum source data line number to turn off
      @return SourceDataLine or null in any error condition
   */
  public SourceDataLine setOff(boolean off, int linenum) {
     try {
        if (off) {
           lines[linenum].stop();
           lines[linenum].close();
           return null;
        } else {
           lines[linenum]=(SourceDataLine)mixer.getLine(lineInfo);
           javax.sound.sampled.AudioFormat pformat=lines[linenum].getFormat();
           int pbufsize=lines[linenum].getBufferSize();
           if (DEBUG) System.out.println("In setOnOff: bufsize="+pbufsize+". format="+pformat);
           lines[linenum].open(pformat,pbufsize);
           lines[linenum].start();
           return lines[linenum];
        }
     } catch (LineUnavailableException e) {
        System.out.println("Unable to set On/Off: " + e);
     } catch (Exception e) {
          System.out.println("Error setting On/Off for linenum:"+linenum);
          e.printStackTrace();
     }
     return null;
  }

  /** Get ON OFF status for given line number
      @param linenum source data line number to get off status
      @return true OFF, false if ON
   */
  public boolean getOff(int linenum) {
     try {
         if (lines[linenum]==null || !lines[linenum].isOpen()) return true;
         return false;
     } catch (Exception e) {
       System.out.println("Error getting Off for linenum:"+linenum);
       e.printStackTrace();
     }
     return false;
  }

  /** 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();
    Mixer.Info[] minfo = javax.sound.sampled.AudioSystem.getMixerInfo(); //gets set of mixer objects available
    mixer = javax.sound.sampled.AudioSystem.getMixer(minfo[0]); //get first one
    format = new javax.sound.sampled.AudioFormat(sampleRate,sampleSizeInBits,channels,signed,bigEndian);
    lineInfo = new DataLine.Info(SourceDataLine.class,format);
    maxlines = mixer.getMaxLines(lineInfo);
    if (maxlines>0) lines = new SourceDataLine[maxlines];
  }
}