/*
 * @(#)AudioRenderer.java	1.70 02/08/21
 *
 * Copyright (c) 1996-2002 Sun Microsystems, Inc.  All rights reserved.
 */
package com.sun.media.renderer.audio;

import java.util.*;
import java.io.*;
import java.lang.reflect.Constructor;
import javax.media.*;
import javax.media.format.*;
import javax.media.format.AudioFormat;
import javax.media.control.BufferControl;
import com.sun.media.*;
import com.sun.media.util.*;
import com.sun.media.controls.*;
//import com.sun.media.jmf.audio.*;
import com.sun.media.renderer.audio.device.AudioOutput;


/**
 * AudioRenderer
 * @version 1.23, 98/12/23
 */

public abstract class AudioRenderer extends BasicPlugIn implements Renderer, Prefetchable, Drainable, Clock {

    javax.media.Format supportedFormats[];
    protected javax.media.format.AudioFormat inputFormat;
    protected javax.media.format.AudioFormat devFormat;

    protected AudioOutput device = null;
    protected TimeBase timeBase = null;

    protected boolean started = false;
    protected boolean prefetched = false;
    protected boolean resetted = false;
    protected boolean devicePaused = true;

    protected GainControl gainControl;
    protected BufferControl bufferControl;
    protected Control     peakVolumeMeter = null;

    protected long bytesWritten = 0;
    protected int bytesPerSec;
    
    private Object writeLock = new Object();

    public AudioRenderer() {
        timeBase = new AudioTimeBase(this);
	bufferControl = new BC(this);
    }


    public javax.media.Format [] getSupportedInputFormats() {
	return supportedFormats;
    }

    public javax.media.Format setInputFormat(javax.media.Format format) {
	for (int i = 0; i < supportedFormats.length; i++) {
	    if (supportedFormats[i].matches(format)) {
		inputFormat = (AudioFormat)format;
		return format;
	    }
	}
	return null;
    }


    public void close() {
	stop();
	if (device != null) {
	    pauseDevice();
	    device.flush();
	    mediaTimeAnchor = getMediaNanoseconds();
	    ticksSinceLastReset = 0;
	    device.dispose();
	}
	device = null;
    }

    public void reset() {
	resetted = true;

	// Mark the media time before reset.
	mediaTimeAnchor = getMediaNanoseconds();

	if (device != null) {
	    device.flush();
	    ticksSinceLastReset = device.getMediaNanoseconds();
	} else
	    ticksSinceLastReset = 0;

	prefetched = false;
    }


    synchronized void pauseDevice() {
	if (!devicePaused && device != null) {
	    device.pause();
	    devicePaused = true;
	}
	if (timeBase instanceof AudioTimeBase)
	    ((AudioTimeBase)timeBase).mediaStopped();
    }


    synchronized void resumeDevice() {
	if (timeBase instanceof AudioTimeBase)
	    ((AudioTimeBase)timeBase).mediaStarted();
	if (devicePaused && device != null) {
	    device.resume();
	    devicePaused = false;
	}
    }


    public void start() {
	syncStart(getTimeBase().getTime());
    }

    public synchronized void drain() {
	if (started && device != null)
	    device.drain();
	prefetched = false;
    }

    public int process(Buffer buffer) {
	int rtn = processData(buffer);
	if (buffer.isEOM() && rtn != INPUT_BUFFER_NOT_CONSUMED) {
	    // EOM.
	    drain();
	    pauseDevice();
	}
	return rtn;
    }

    protected boolean checkInput(Buffer buffer) {

	javax.media.Format format = buffer.getFormat();

	// Here comes the real JavaSound processing.

	// Initialize the device if it's not already initialized; or if the
	// input format changes.
	if (device == null || devFormat == null || !devFormat.equals(format)) {
	    if (!initDevice((javax.media.format.AudioFormat)format)) {
		// failed to initialize the device.
		buffer.setDiscard(true);
		return false;
	    }
	    devFormat = (AudioFormat)format;
	}

	return true;
    }

    protected int processData(Buffer buffer) {

	if (!checkInput(buffer))
	    return BUFFER_PROCESSED_FAILED;

	return doProcessData(buffer);
    }

    protected int doProcessData(Buffer buffer) {

	byte data[] = (byte [])buffer.getData();
	int remain = buffer.getLength();
	int off = buffer.getOffset();
	int len = 0;

	synchronized (this) {
	  if (!started) {

	    // If we are not marked as started, we'll pause the device here.
	    if (!devicePaused)
		pauseDevice();

	    // This is in the prefetching state
	    // We'll try to write as much as needed so we won't
	    // block.

	    // We are in prefetching cycle now.  Turn of resetted.
	    resetted = false;

	    int available = device.bufferAvailable();

	    if (available > remain)
		available = remain;

	    if (available > 0) {
		len = device.write(data, off, available);
		bytesWritten += len;
	    }

	    buffer.setLength(remain - len);
	    if (buffer.getLength() > 0 || buffer.isEOM()) {
		buffer.setOffset(off + len);
		prefetched = true;
		return INPUT_BUFFER_NOT_CONSUMED;
	    } else {
		return BUFFER_PROCESSED_OK;
	    }
	  }
	}

	// Guard against pausing the device in the middle of a write
	// thus blocking the entire thread.
	synchronized (writeLock) {

	    if (devicePaused)
		return PlugIn.INPUT_BUFFER_NOT_CONSUMED;

	    try {
		while (remain > 0 && !resetted) {
		    // device.write is blocking.  If the device
		    // has not been started and the device's
		    // internal buffer is filled, then it will block.
		    len = device.write(data, off, remain);
		    bytesWritten += len;
		    off += len; remain -= len;
		}
	    } catch (NullPointerException e) {
		return BUFFER_PROCESSED_OK;
	    }
	}

	buffer.setLength(0);
	buffer.setOffset(0);

	return BUFFER_PROCESSED_OK;
    }

    protected boolean initDevice(AudioFormat format) {

	if (format == null) {
	    System.err.println("AudioRenderer: ERROR: Unknown AudioFormat");
	    return false;
	}

	if (format.getSampleRate() == AudioFormat.NOT_SPECIFIED ||
	    format.getSampleSizeInBits() == AudioFormat.NOT_SPECIFIED) {
	    Log.error("Cannot initialize audio renderer with format: " + format);
	    return false;
	}

	// Close the old device.
	if (device != null) {

	    device.drain();
	    pauseDevice();

	    // Adjust for the media time since the device as well as the
	    // sample count is re-initialized.
	    mediaTimeAnchor = getMediaNanoseconds();
	    ticksSinceLastReset = 0;

	    device.dispose();
	    device = null;
	}

	/*
	System.out.println("sampleRate is " + format.getSampleRate());
	System.out.println("sampleSize is " + sampleSize);
	System.out.println("SamplePerUnit is " + SamplePerUnit);
	System.out.println("channels is " + format.getChannels());
	System.out.println("encoding is " + format.getEncoding());
	System.out.println("bigendian is " + format.isBigEndian());
	System.out.println("signed is " + format.isSigned());
	*/

	// Create AudioPlay based on the format and the current platform.
	AudioFormat audioFormat = new
	    AudioFormat(
		   format.getEncoding(),
		   format.getSampleRate(),
		   format.getSampleSizeInBits(),
		   format.getChannels(),
		   format.getEndian(),
		   format.getSigned()
		 );

	device = createDevice(audioFormat);

	if (device == null || 
	    !device.initialize(audioFormat, computeBufferSize(audioFormat))) {
	    device = null;
	    return false;
	}

        device.setMute(gainControl.getMute());
	device.setGain(gainControl.getDB());

	if (rate != 1.0f) {
	    if (rate != device.setRate(rate)) {
		System.err.println("The AudioRenderer does not support the given rate: " + rate);
		device.setRate(1.0f);
	    }
	}

	if ( started )
	    resumeDevice();

	bytesPerSec = (int)(format.getSampleRate() * format.getChannels() * 
			format.getSampleSizeInBits() / 8);

	return true;
    }

    protected abstract AudioOutput createDevice(AudioFormat format);

    protected void processByWaiting(Buffer buffer) {

	synchronized (this) {
	    // If not yet started, it's in the prefetching state,
	    // do not consume the data bits.
	    if (!started) {
		prefetched = true;
		return;
	    }
	}

	javax.media.format.AudioFormat format =
		(javax.media.format.AudioFormat)buffer.getFormat();

	int sampleRate = (int)format.getSampleRate();
	int sampleSize = format.getSampleSizeInBits();
	int channels = format.getChannels();
	int timeToWait;
	long duration;

	duration = buffer.getLength() * 1000 /
			((sampleSize/8) * sampleRate * channels);
	timeToWait = (int)((float)duration/getRate());
	/*
	System.err.println("sampleSize = " + sampleSize +
		" sampleRate = " + sampleRate +
		" channels = " + channels +
		" length = " + buffer.getLength() +
		" offset = " + buffer.getOffset() +
		" timeToWait = " + timeToWait);
	*/
	try {
	    Thread.currentThread().sleep(timeToWait);
	} catch (Exception e) {}

	buffer.setLength(0);
	buffer.setOffset(0);

	mediaTimeAnchor += duration * 1000000;
    }

    public Object [] getControls() {
	Control c[] = new Control[] { 
				gainControl,
				bufferControl
				};
	return c;
    }



    /////////////////////////
    //
    // Prefetchable methods
    /////////////////////////
    public boolean isPrefetched() {
	return prefetched;
    }



    /////////////////////////
    //
    // Clock methods.
    /////////////////////////

    long mediaTimeAnchor = 0;
    long startTime = Long.MAX_VALUE;
    long stopTime = Long.MAX_VALUE;
    long ticksSinceLastReset = 0;
    float rate = 1.0f;

    // This would be the time base if there's a master set.
    TimeBase master = null;  


    public void setTimeBase(TimeBase master) throws IncompatibleTimeBaseException {
	if (!(master instanceof AudioTimeBase)) {
	    Log.warning("AudioRenderer cannot be controlled by time bases other than its own: " + master);
	   /**
	    Silently allows the time base to be set to make
	    addController slightly more useful.
	    --ivg
	    throw new IncompatibleTimeBaseException();
	    */
	}

	this.master = master;
    }

    public synchronized void syncStart(Time at) {
	// It doesn't really do syncStart right now.  It just starts
	// it right away.
	started = true;
	prefetched = true;
	resetted = false;
	resumeDevice();
	startTime = at.getNanoseconds();
    }

    public synchronized void stop() {
	started = false;
	prefetched = false;

	// Guard against pausing in the middle of a write.
	synchronized (writeLock) {
	    pauseDevice();
	}
    }

    public void setStopTime(Time t) {
	stopTime = t.getNanoseconds();
    }

    public Time getStopTime() {
	return new Time(stopTime);
    }

    public void setMediaTime(Time now) {
	mediaTimeAnchor = now.getNanoseconds();
    }

    public Time getMediaTime() {
	return new Time(getMediaNanoseconds());
    }

    public long getMediaNanoseconds() {
	return mediaTimeAnchor + 
		(device != null ? device.getMediaNanoseconds() : 0) - 
		ticksSinceLastReset;
    }

    public long getLatency() {
	long ts = bytesWritten * 1000/bytesPerSec * 1000000;
	return ts - getMediaNanoseconds();
    }

    public Time getSyncTime() {
	return new Time(0);
    }

    public TimeBase getTimeBase() {
	if (master != null)
	    return master;
	else
	    return timeBase;
    }

    public Time mapToTimeBase(Time t) throws ClockStoppedException {
	return new Time((long)((t.getNanoseconds() - mediaTimeAnchor)/rate) + startTime);
    }

    public float getRate() {
	return rate;
    }

    public float setRate(float factor) {
	if (device != null)
	    rate = device.setRate(factor);
	else
	    rate = 1.0f;
	return rate;
    }


    public int computeBufferSize(javax.media.format.AudioFormat f) {

	long bytesPerSecond = (long)(f.getSampleRate() * f.getChannels() * 
					f.getSampleSizeInBits() / 8);
	long bufSize;
	long bufLen;   // in millsec.

	// System.out.println("bytesPerSecond is " + bytesPerSecond);

	if (bufLenReq < DefaultMinBufferSize)
	    bufLen = DefaultMinBufferSize;
	else if (bufLenReq > DefaultMaxBufferSize)
	    bufLen = DefaultMaxBufferSize;
	else
	    bufLen = bufLenReq;

	float r = bufLen/1000f;

	bufSize = (long)(bytesPerSecond * r);

	//System.out.println("Render buffer size: " + bufSize);

	return (int)bufSize;
    }



    //////////////////////////////////////////////////////////////////////////
    //
    // INNER CLASSES
    //////////////////////////////////////////////////////////////////////////

    class AudioTimeBase extends MediaTimeBase {
	AudioRenderer renderer;

	AudioTimeBase(AudioRenderer r) {
	    renderer = r;
	}

	public long getMediaTime() {

	    // The usual does not require division.
	    if (rate == 1.0f || rate == 0.0f)
		return (device != null ? device.getMediaNanoseconds() : 0);

	    return (long)((device != null ? device.getMediaNanoseconds() : 0) / rate);
	}
    }


    static int DefaultMinBufferSize = 62;	// millisecs.
    static int DefaultMaxBufferSize = 4000;	// millisecs.
    long bufLenReq = 200;

    /**
     * BufferControl for the renderer.
     */
    class BC implements BufferControl, Owned {

	AudioRenderer renderer;

	BC(AudioRenderer ar) {
	   renderer = ar;
	} 
  
	public long getBufferLength() {
	    return bufLenReq;
	} 

	public long setBufferLength(long time) {
	    if (time < DefaultMinBufferSize)
		bufLenReq = DefaultMinBufferSize;
	    else if (time > DefaultMaxBufferSize)
		bufLenReq = DefaultMaxBufferSize;
	    else
		bufLenReq = time;
	    //System.err.println("Render buffer length set: " + bufLenReq);
	    return bufLenReq;
	}
  
	public long getMinimumThreshold() {
	    return 0;
	}

	public long setMinimumThreshold(long time) {
	    return 0;
	}
  
	public void setEnabledThreshold(boolean b) {
	}
  
	public boolean getEnabledThreshold() {
	    return false;
	}

	public java.awt.Component getControlComponent() {
	    return null;
	}

	public Object getOwner() {
	    return renderer;
	}
    }
}
