package nxm.audio.prim;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.SourceDataLine;

import nxm.sys.inc.DataTypes;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.StringUtil;

/**
  Simple audio player using javax.sound utilities. Currently plays
  8- or 16-bit PCM (linear) WAV or Type 1000 audio files only.

  @author Wayne Dengel - original hwplayer library
  @author Jeff Schoen - simple primitive
  @author Neon Ngo - 2011-08-31: added docs for set/get gain and pan;  added listControls()
                     2011-08-02: added support to mute audio
                     2009-12-16: fixed to play WAVE format that is 8-bit unsigned PCM (codec=1)
  @since NeXtMidas 2.1.1
*/
public class sinkaudio extends Primitive {

  private DataFile hi;
  private Data data;
  private boolean wrap,ulaw;
  private float rate;
  private int bits, channels, frameSize=1, bufsize=8000;
  private double gain=0, pan=0;
  private boolean bige=false, signed=true;
  private BooleanControl muteControl = null;

  private SourceDataLine audioLine;

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

  public int open () {

    hi = MA.getDataFile ("IN","1000","SF,SB,SI,CB,CI",0);
    hi.open();

    bits = hi.bps*8;
    rate = (float)Math.round(1/hi.getDelta());
    channels = hi.spa;

    bige = MA.getState("/BIGE");
    ulaw = MA.getState("/ULAW");
    gain = MA.getD("/GAIN",gain);
    pan  = MA.getD("/PAN",pan);

    if (hi.getFormatType()==DataTypes.BYTE && !ulaw) {
      // since 2.7.3: BUG 1860: 8-bit WAVE are always unsigned for codec=1 (PCM)
      String src = StringUtil.toString(hi.getKeyWord("SOURCE"));
      if (src.contains("WAV") && src.contains("codec=1")) signed = false;
    }
    signed = !MA.getState("/UNSIGNED",!signed); // since 2.7.3 for BUG 1860: allow specify unsigned integers (override)

    openAudio();

    xfer = MA.getL("/TL", (int)(0.25*rate) );
    xfer = (xfer/frameSize) * frameSize;
    data = hi.getDataBuffer(xfer);
    wrap = MA.getState("/WRAP");

    return (NORMAL);
  }

  public int process () {
    int avail = audioLine.available();
    //if (avail<xfer) return NOOP;
    int n = hi.read(data);
    if (n>0) playAudio (data.buf,n*data.bpe);
    else if (n==0) return (NOOP);
    else if (wrap && (hi.isFile() || hi.isJarFile())) hi.seek(0.0);
    else if (avail<bufsize) return NOOP; // wait for buffer to empty
    else return FINISH;
    return NORMAL;
  }

  public int close () {
    closeAudio();
    hi.close();
    return NORMAL;
  }

  private void openAudio () {
    try {
      AudioFormat audioFormat;
      if (ulaw) audioFormat = new AudioFormat (AudioFormat.Encoding.ULAW,
        rate, bits, channels, frameSize, rate/frameSize, bige);
      else audioFormat = new AudioFormat (rate, bits, channels, signed, bige);
      frameSize = audioFormat.getFrameSize();
      if (verbose) M.info("Audio format = "+audioFormat+", FS="+frameSize);
      DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class,audioFormat);
      if (!AudioSystem.isLineSupported(lineInfo)) M.error("Audio "+lineInfo+" not supported");
      audioLine = (SourceDataLine)AudioSystem.getLine(lineInfo);
      audioLine.open(audioFormat,bufsize);
      setGain(gain);
      //setPan(pan);
      audioLine.start();
    } catch (Exception e) { M.error(e); }
  }

  private void playAudio (byte[] buffer, int bytes) {
    int status = audioLine.write(buffer,0,bytes);
    if (status != bytes) M.warning("Bad playAudio status "+status+" of "+bytes);
  }

  private void closeAudio() {
    audioLine.drain();
    audioLine.stop();
    audioLine.close();
  }

  /** Set master gain (master volume) on audio line.
      @param valueDB master gain (volume)
   */
  public void setGain (double valueDB) {
    try {
      FloatControl fc = (FloatControl) audioLine.getControl(FloatControl.Type.MASTER_GAIN);
      if (verbose) M.info("Gain = "+valueDB+"  ["+fc.getMinimum()+","+fc.getMaximum()+"]");
      gain = Math.max(fc.getMinimum(),Math.min(fc.getMaximum(),valueDB));
      fc.setValue((float)gain);
    } catch (Exception e) { e.printStackTrace(); }
  }
  /** Get the master gain
   *  @return master gain (master volume) from audio line */
  public double getGain() { return gain; }

  /** Set the relative pan (left-right positioning) of the signal. The signal may be mono;
      the pan setting affects how it is distributed by the mixer in a stereo mix.
      The valid range of values is -1.0 (left channel only) to 1.0 (right channel only).
      The default is 0.0 (centered).
      @param valueLR relative pan of (left-right positioning) of the signal
   */
  public void setPan (double valueLR) {
    try {
      FloatControl fc = (FloatControl) audioLine.getControl(FloatControl.Type.PAN);
      if (verbose) M.info("Pan = "+valueLR+"  ["+fc.getMinimum()+","+fc.getMaximum()+"]");
      pan = Math.max(fc.getMinimum(),Math.min(fc.getMaximum(),valueLR));
      fc.setValue((float)pan);
    } catch (Exception e) { e.printStackTrace(); }
  }
  /** Get the relative pan
   *  @return the relative pan (left-right positioning) of the signal. */
  public double getPan() { return pan; }

  /** Get Mute control from audioLine.
      @return BooleanControl for mute control of audio line, null if no audio line
              or when control not supported.
      @since NeXtMidas 3.1.0
   */
  private BooleanControl getMuteControl () {
    if (audioLine == null) return null;
    if (muteControl == null) {
      try {
        muteControl = (BooleanControl) audioLine.getControl(BooleanControl.Type.MUTE);
      } catch (IllegalArgumentException ex) {
        M.warning("Unable to get mute control for audio line: "+ex);
      }
    }
    return muteControl;
  }

  /** Mute (true) / unmute (false) audioLine.
      @param newval true to mute, false to un-mute audio line
      @since NeXtMidas 3.0.0
   */
  public void setMute (boolean newval) {
    BooleanControl mute = getMuteControl();
    if (mute != null) {
      if (verbose) M.info("Set mute = "+newval+" previous = "+muteControl.getValue());
      muteControl.setValue(newval);
    }
  }

  /** Is audio line muted
   *  @return true if audio line is muted, false otherwise */
  public boolean isMute () {
    BooleanControl mute = getMuteControl();
    if (mute != null) return mute.getValue();
    return false;
  }

  /** List all the controls associated with this audio line.
      Some controls may only be available when the line is open.
      @since NeXtMidas 3.1.0
   */
  public void listControls () {
    if (audioLine == null) return;
    int ii = 0;
    for (Control control : audioLine.getControls()) {
      M.info("control["+ (ii++) + "]: "+control);
    }
  }
}
