/*
 * FSSound.java
 * Transform
 * 
 * Copyright (c) 2001-2008 Flagstone Software Ltd. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution.
 *  * Neither the name of Flagstone Software Ltd. nor the names of its contributors 
 *    may be used to endorse or promote products derived from this software 
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.flagstone.transform;

import java.util.*;

/**
 * The FSSound class identifies a sound (previously defined using The
 * FSDefineSound class) and controls how it is played.
 * 
 * <p>
 * FSSound defines how the sound fades in and out, whether it is repeated as
 * well as specifying an envelope that provides a finer degree of control over
 * the levels at which the sound is played.
 * </p>
 * 
 * <table class="datasheet">
 * 
 * <tr>
 * <th align="left" colspan="2">Attributes</th>
 * </tr>
 * 
 * <tr>
 * <td><a name="FSSound_0">identifier</a></td>
 * <td>The identifier, in the range 1..65535, of the FSDefineSound object that
 * contains the sound data.</td>
 * </tr>
 * 
 * <tr>
 * <td><a name="FSSound_1">mode</a></td>
 * <td>Controls how the sound is played, either: Start - start playing the
 * sound, Continue - start playing the sound if it is not already playing and
 * Stop - stop playing the sound.</td>
 * </tr>
 * 
 * <tr>
 * <td><a name="FSSound_2">inPoint</a></td>
 * <td>The sample number that the sound increases in volume to until full
 * volume is reached. Optional. Set to 0 if the sound does not fade in.</td>
 * </tr>
 * 
 * <tr>
 * <td><a name="FSSound_3">outPoint</a></td>
 * <td>The sample number at which the that the sound starts to fades until the
 * sound has finished playing. Optional. Set to 0 if the sound does not fade
 * out.</td>
 * </tr>
 * 
 * <tr>
 * <td><a name="FSSound_4">loopCount</a></td>
 * <td>The number of times the sound is repeated. Optional. Must be at least 1
 * if a sound is being played. Set to 0 if a sound is being stopped.</td>
 * </tr>
 * 
 * <tr>
 * <td><a name="FSSound_5">envelopes</a></td>
 * <td>An array of FSEnvelope objects that control how the sound is played.
 * Optional. May be set to null or an empty array if no envelope is defined.</td>
 * </tr>
 * 
 * </table>
 * 
 * <p>
 * Not all the attributes are required to play a sound. Only the identifier and
 * the mode is required. The other attributes are optional and may be added as a
 * greater degree of control is required. The inPoint and outPoint attributes
 * may be set to zero if the sound does not fade in or out respectively. The
 * loopCount may be set to zero if a sound is being stopped. The envelopes array
 * may be left empty if no envelope is defined for the sound. The class provides
 * different constructors to specify different sets of attributes.
 * </p>
 * 
 * <p>
 * The in and out point specify the sample number which marks the point in time
 * at which the sound stops increasing or starts decreasing in volume
 * respectively. Sounds are played by the Flash player at 44.1KHz so the sample
 * number also indicates the time when the total number of samples in the sound
 * is taken into account.
 * </p>
 * 
 * <h1 class="datasheet">History</h1>
 * 
 * <p>
 * The FSSound class is based on the SoundInfo data structure from the
 * Macromedia Flash (SWF) File Format Specification. However it also contains
 * the identifier for the sound which is a separate attribute in the data
 * structures defined in the specification that contain SoundInfo data.
 * </p>
 */
public class FSSound extends FSTransformObject
{
	/**
	 * Identifies an uncompressed pulse code modulated sound. The byte-order for
	 * 16-bit sound samples is dependent on the host platform on which the sound
	 * is played.
	 */
	public static final int NATIVE_PCM = 0;

	/** Identifies an adaptive pulse code modulated sound. */
	public static final int ADPCM = 1;

	/** Identifies an MP3 format sound. */
	public static final int MP3 = 2;

	/**
	 * Identifies an uncompressed pulse code modulated sound, little-endian byte
	 * order.
	 */
	public static final int PCM = 3;

	// Flash 6
	// / Identifies a Nellymoser Asao encoded sound.
	public static final int NELLYMOSER = 6;

	// End Flash 6
	/** Start playing the sound. */
	public static final int Start = 0;

	/** Start playing the sound or continues if it is already playing. */
	public static final int Continue = 1;

	/** Stop playing the sound. */
	public static final int Stop = 2;

	private int identifier = 0;

	private int mode = 0;

	private int inPoint = 0;

	private int outPoint = 0;

	private int loopCount = 0;

	private ArrayList envelopes = null;

	/**
	 * Construct an FSSound object, initalizing it with values decoded from an
	 * encoded object.
	 * 
	 * @param coder
	 *            an FSCoder containing the binary data.
	 */
	public FSSound(FSCoder coder)
	{
		decode(coder);
	}

	/**
	 * Constructs an FSSound object specifying how the sound is played.
	 * 
	 * @param anIdentifier
	 *            the unique identifier of the object that contains the sound
	 *            data.
	 * @param aMode
	 *            how the sound is synchronised when the frames are displayed:
	 *            Start - start playing the sound, Continue - do not play the
	 *            sound if it is already playing and Stop - stop playing the
	 *            sound.
	 */
	public FSSound(int anIdentifier, int aMode)
	{
		setIdentifier(anIdentifier);
		setMode(aMode);
	}

	/**
	 * Constructs and FSSound object specifying how the sound is played and the
	 * number of times the sound is repeated.
	 * 
	 * @param anIdentifier
	 *            the unique identifier of the object that contains the sound
	 *            data.
	 * @param aMode
	 *            how the sound is synchronised when the frames are displayed:
	 *            Play - do not play the sound if it is already playing and Stop -
	 *            stop playing the sound.
	 * @param aCount
	 *            the number of times the sound is repeated. May be set to zero
	 *            if the sound will not be repeated.
	 */
	public FSSound(int anIdentifier, int aMode, int aCount)
	{
		setIdentifier(anIdentifier);
		setMode(aMode);
		setLoopCount(aCount);
	}

	/**
	 * Constructs and FSSound object specifying how the sound is played and the
	 * points at which the sound fades in and out.
	 * 
	 * @param anIdentifier
	 *            the unique identifier of the object that contains the sound
	 *            data.
	 * @param aMode
	 *            how the sound is synchronised when the frames are displayed:
	 *            Play - do not play the sound if it is already playing and Stop -
	 *            stop playing the sound.
	 * @param anInPoint
	 *            the sample number which the sound fades in to. May be set to
	 *            zero if the sound does not fade in.
	 * @param anOutPoint
	 *            the sample number at which the sound starts to fade. May be
	 *            set to zero if the sound does not fade out.
	 */
	public FSSound(int anIdentifier, int aMode, int anInPoint, int anOutPoint)
	{
		setIdentifier(anIdentifier);
		setMode(aMode);
		setInPoint(anInPoint);
		setOutPoint(anOutPoint);
	}

	/**
	 * Constructs and FSSound object specifying how the sound is played, the
	 * point at which the sound fades in and out and the number of times the
	 * sound is repeated.
	 * 
	 * @param anIdentifier
	 *            the unique identifier of the object that contains the sound
	 *            data.
	 * @param aMode
	 *            how the sound is synchronised when the frames are displayed:
	 *            Play - do not play the sound if it is already playing and Stop -
	 *            stop playing the sound.
	 * @param anInPoint
	 *            the sample number which the sound fades in to. May be set to
	 *            zero if the sound does not fade in.
	 * @param anOutPoint
	 *            the sample number at which the sound starts to fade. May be
	 *            set to zero if the sound does not fade out.
	 * @param aCount
	 *            the number of times the sound is repeated. May be set to zero
	 *            if the sound will not be repeated.
	 */
	public FSSound(int anIdentifier, int aMode, int anInPoint, int anOutPoint,
					int aCount)
	{
		setIdentifier(anIdentifier);
		setMode(aMode);
		setInPoint(anInPoint);
		setOutPoint(anOutPoint);
		setLoopCount(aCount);
	}

	/**
	 * Constructs and FSSound object specifying how the sound is played, the
	 * point at which the sound fades in and out, the number of times the sound
	 * is repeated and the envelopes used to control how the sound is played.
	 * 
	 * @param anIdentifier
	 *            the unique identifier of the object that contains the sound
	 *            data.
	 * @param aMode
	 *            how the sound is synchronised when the frames are displayed:
	 *            Play - do not play the sound if it is already playing and Stop -
	 *            stop playing the sound.
	 * @param anInPoint
	 *            the sample number which the sound fades in to. May be set to
	 *            zero if the sound does not fade in.
	 * @param anOutPoint
	 *            the sample number at which the sound starts to fade. May be
	 *            set to zero if the sound does not fade out.
	 * @param aCount
	 *            the number of times the sound is repeated. May be set to zero
	 *            if the sound will not be repeated.
	 * @param anArray
	 *            an array of FSEnvelope objects that define the levels at which
	 *            a sound is played over the duration of the sound. May be set
	 *            to NULL if no envelope is defined.
	 */
	public FSSound(int anIdentifier, int aMode, int anInPoint, int anOutPoint,
					int aCount, ArrayList anArray)
	{
		setIdentifier(anIdentifier);
		setMode(aMode);
		setInPoint(anInPoint);
		setOutPoint(anOutPoint);
		setLoopCount(aCount);
		setEnvelopes(anArray);
	}

	/**
	 * Constructs an FSSound object by copying values from an existing object.
	 * 
	 * @param obj
	 *            an FSSound object.
	 */
	public FSSound(FSSound obj)
	{
		identifier = obj.identifier;
		mode = obj.mode;
		inPoint = obj.inPoint;
		outPoint = obj.outPoint;
		loopCount = obj.loopCount;

		envelopes = new ArrayList();

		for (Iterator i = obj.envelopes.iterator(); i.hasNext();)
			envelopes.add(((FSEnvelope) i.next()).clone());
	}

	/**
	 * Add a FSEnvelope object to the array of envelope objects.
	 * 
	 * @param anEnvelope
	 *            an FSEnvelope object.
	 */
	public void add(FSEnvelope anEnvelope)
	{
		envelopes.add(anEnvelope);
	}

	/**
	 * Gets the identifier of the sound to the played.
	 * 
	 * @return the sound identifier.
	 */
	public int getIdentifier()
	{
		return identifier;
	}

	/**
	 * Gets the synchronisation mode: SyncNoMultiple - do not play the sound if
	 * it is already playing and SyncStop - stop playing the sound.
	 * 
	 * @return the synchronisation mode of the sound.
	 */
	public int getMode()
	{
		return mode;
	}

	/**
	 * Gets the sample number at which the sound reaches full volume when fading
	 * in.
	 * 
	 * @return the in point
	 */
	public int getInPoint()
	{
		return inPoint;
	}

	/**
	 * Gets the sample number at which the sound starts to fade.
	 * 
	 * @return the out point
	 */
	public int getOutPoint()
	{
		return outPoint;
	}

	/**
	 * Gets the number of times the sound will be repeated.
	 * 
	 * @return the Loop Count.
	 */
	public int getLoopCount()
	{
		return loopCount;
	}

	/**
	 * Gets the array of FSEnvelope objects that control the levels the sound is
	 * played.
	 * 
	 * @return an array of FSEnvelope objects.
	 */
	public ArrayList getEnvelopes()
	{
		return envelopes;
	}

	/**
	 * Sets the identifier of the sound to the played.
	 * 
	 * @param anIdentifier
	 *            the identifier for the sound to be played.
	 */
	public void setIdentifier(int anIdentifier)
	{
		identifier = anIdentifier;
	}

	/**
	 * Sets how the sound is synchronised when the frames are displayed: Start -
	 * start playing the sound, Continue - do not play the sound if it is
	 * already playing and Stop - stop playing the sound.
	 * 
	 * @param aMode
	 *            how the sound is played.
	 */
	public void setMode(int aMode)
	{
		mode = aMode;
	}

	/**
	 * Sets the sample number at which the sound reaches full volume when fading
	 * in. May be set to zero if the sound does not fade in.
	 * 
	 * @param aNumber
	 *            the sample number which the sound fades in to.
	 */
	public void setInPoint(int aNumber)
	{
		inPoint = aNumber;
	}

	/**
	 * Sets the sample number at which the sound starts to fade. May be set to
	 * zero if the sound does not fade out.
	 * 
	 * @param aNumber
	 *            the sample number at which the sound starts to fade.
	 */
	public void setOutPoint(int aNumber)
	{
		outPoint = aNumber;
	}

	/**
	 * Sets the number of times the sound is repeated. May be set to zero if the
	 * sound will not be repeated.
	 * 
	 * @param aNumber
	 *            the number of times the sound is repeated.
	 */
	public void setLoopCount(int aNumber)
	{
		loopCount = aNumber;
	}

	/**
	 * Sets the array of FSEnvelope objects that define the levels at which a
	 * sound is played over the duration of the sound. May be set to null if no
	 * envelope is defined.
	 * 
	 * @param anArray
	 *            an array of FSEnvelope objects.
	 */
	public void setEnvelopes(ArrayList anArray)
	{
		envelopes = anArray;
	}

	public Object clone()
	{
		FSSound anObject = (FSSound) super.clone();

		anObject.envelopes = new ArrayList();

		for (Iterator i = envelopes.iterator(); i.hasNext();)
			anObject.envelopes.add(((FSEnvelope) i.next()).clone());

		return anObject;
	}

	public boolean equals(Object anObject)
	{
		boolean result = false;

		if (super.equals(anObject))
		{
			FSSound typedObject = (FSSound) anObject;

			result = identifier == typedObject.identifier;
			result = result && mode == typedObject.mode;
			result = result && inPoint == typedObject.inPoint;
			result = result && outPoint == typedObject.outPoint;
			result = result && loopCount == typedObject.loopCount;

			if (envelopes != null)
				result = result && envelopes.equals(typedObject.envelopes);
			else
				result = result && envelopes == typedObject.envelopes;
		}
		return result;
	}

	public void appendDescription(StringBuffer buffer, int depth)
	{
		buffer.append(name());

		if (depth > 0)
		{
			buffer.append(": { ");
			Transform.append(buffer, "identifier", identifier);
			Transform.append(buffer, "mode", mode);
			Transform.append(buffer, "inPoint", inPoint);
			Transform.append(buffer, "outPoint", outPoint);
			Transform.append(buffer, "loopCount", loopCount);
			Transform.append(buffer, "envelopes", envelopes, depth);
			buffer.append("}");
		}
	}

	public int length(FSCoder coder)
	{
		boolean _containsInPoint = containsInPoint();
		boolean _containsOutPoint = containsOutPoint();
		boolean _containsLoopCount = containsLoopCount();
		boolean _containsEnvelopes = containsEnvelopes();

		int length = 3;

		length += (_containsInPoint) ? 4 : 0;
		length += (_containsOutPoint) ? 4 : 0;
		length += (_containsLoopCount) ? 2 : 0;
		length += (_containsEnvelopes) ? 1 : 0;
		length += (_containsEnvelopes) ? envelopes.size() * 8 : 0;

		return length;
	}

	public void encode(FSCoder coder)
	{
		boolean _containsInPoint = containsInPoint();
		boolean _containsOutPoint = containsOutPoint();
		boolean _containsLoopCount = containsLoopCount();
		boolean _containsEnvelopes = containsEnvelopes();

		coder.writeWord(identifier, 2);
		coder.writeBits(mode, 4);
		coder.writeBits(_containsEnvelopes ? 1 : 0, 1);
		coder.writeBits(_containsLoopCount ? 1 : 0, 1);
		coder.writeBits(_containsOutPoint ? 1 : 0, 1);
		coder.writeBits(_containsInPoint ? 1 : 0, 1);

		if (_containsInPoint)
			coder.writeWord(inPoint, 4);
		if (_containsOutPoint)
			coder.writeWord(outPoint, 4);
		if (_containsLoopCount)
			coder.writeWord(loopCount, 2);
		if (_containsEnvelopes)
		{
			coder.writeWord(envelopes.size(), 1);

			for (int i = 0; i < envelopes.size(); i++)
				((FSTransformObject) envelopes.get(i)).encode(coder);
		}
	}

	public void decode(FSCoder coder)
	{
		boolean _containsInPoint = false;
		boolean _containsOutPoint = false;
		boolean _containsLoopCount = false;
		boolean _containsEnvelopes = false;

		envelopes = new ArrayList();

		int envelopeCount = 0;

		identifier = coder.readWord(2, false);
		mode = coder.readBits(4, false);
		_containsEnvelopes = coder.readBits(1, false) != 0 ? true : false;
		_containsLoopCount = coder.readBits(1, false) != 0 ? true : false;
		_containsOutPoint = coder.readBits(1, false) != 0 ? true : false;
		_containsInPoint = coder.readBits(1, false) != 0 ? true : false;

		if (_containsInPoint)
			inPoint = coder.readWord(4, false);

		if (_containsOutPoint)
			outPoint = coder.readWord(4, false);

		if (_containsLoopCount)
			loopCount = coder.readWord(2, false);

		if (_containsEnvelopes)
		{
			envelopeCount = coder.readWord(1, false);

			for (int i = 0; i < envelopeCount; i++)
				envelopes.add(new FSEnvelope(coder));
		}
	}

	private boolean containsInPoint()
	{
		return inPoint != 0;
	}

	private boolean containsOutPoint()
	{
		return outPoint != 0;
	}

	private boolean containsLoopCount()
	{
		return loopCount != 0;
	}

	private boolean containsEnvelopes()
	{
		return envelopes != null && envelopes.size() > 0;
	}
}
