/*
 * FSVideo.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.*;
import java.util.zip.*;
import java.io.*;

/**
 * FSVideo is a container class for the objects that represents the data
 * structures in a Flash video file. It is very similar to the FSMovie class for
 * flash (.swf) files and provides a simple API for decoding and encoding files
 * and accessing the objects that represent the different data structures used
 * for audio and video data.
 */
public final class FSVideo implements Cloneable
{
	/**
	 * Indicates that the video data was encoded using the Sorenson modified
	 * H263 format.
	 */
	public static final int H263 = 2;

	/**
	 * Indicates that the video data was encoded using Macromedia's ScreenVideo
	 * format.
	 */
	public static final int ScreenVideo = 3;

	/**
	 * Indicates a frame is a key frame in the video stream.
	 */
	public static final int KeyFrame = 1;

	/**
	 * Indicates a frame is a normal frame (one displayed between key frames) in
	 * the video stream.
	 */
	public static final int Frame = 2;

	/**
	 * Indicates a frame is an optional frame - one that may be deleted to
	 * maintain real-time display of the video stream. Used only with video
	 * encoded using the Sorenson modified H263 format.
	 */
	public static final int Optional = 3;

	/*
	 * Index values for the context array in FSVideoCoder.
	 */
	static final int Action = 0;

	static final int Version = 1;

	static final int Type = 2;

	static final int Delta = 3;

	static final int Identifier = 4;

	static final int Codec = 5;

	static FSVideoObject decodeObject(FSCoder coder)
	{
		FSVideoObject currentObject = null;

		int start = coder.getPointer();

		int type = coder.readWord(1, false);
		int length = coder.readWord(3, false) & 0x00FFFFFF;
		int next = start + ((11 + length) << 3);

		coder.setPointer(start);

		switch (type)
		{
			case FSVideoObject.AudioData:
				currentObject = new FSAudioData(coder);
				break;
			case FSVideoObject.VideoData:
				currentObject = new FSVideoData(coder);
				break;
			case FSVideoObject.MetaData:
				currentObject = new FSVideoMetaData(coder);
				break;
			default:
				break;
		}

		int delta = next - coder.getPointer();
		coder.context[FSVideo.Delta] = delta;
		coder.setPointer(next);

		return currentObject;
	}

	private String signature = "FLV";

	private int version = 1;

	private ArrayList objects = new ArrayList();

	/**
	 * Constructs an FSVideo object with no objects.
	 */
	public FSVideo()
	{
	}

	/**
	 * Constructs an FSVideo object and decodes the contents of the specified
	 * file to generate an array of objects representing the video file. If an
	 * error occurs while reading and parsing the file then an exception is
	 * thrown.
	 * 
	 * @param fileName
	 *            the path to the Flash Video file that will be parsed.
	 * @throws FileNotFoundException -
	 *             if an error occurs while reading the file.
	 * @throws DataFormatException -
	 *             if the file does not contain Flash data.
	 * @throws IOException -
	 *             if an error occurs while reading and decoding the file.
	 */
	public FSVideo(String fileName) throws FileNotFoundException, IOException,
					DataFormatException
	{
		decodeFromFile(fileName);
	}

	/**
	 * Constructs an FSVideo object and decodes the binary data presented in the
	 * byte array to generate an array of objects representing the Flash Video
	 * data. If an error occurs while parsing the data then an exception is
	 * thrown.
	 * 
	 * @param data
	 *            an array of bytes containing the Flash Video binary data.
	 * @throws DataFormatException -
	 *             if the file does not contain Flash data.
	 * @throws IOException -
	 *             if an error occurs while reading and decoding the data.
	 */
	public FSVideo(byte[] data) throws IOException, DataFormatException
	{
		decodeFromData(data);
	}

	/**
	 * Gets the number representing the version of Flash Video that the video
	 * represents.
	 * 
	 * @return an integer defining the Flash Video version number for the coder.
	 */
	public int getVersion()
	{
		return version;
	}

	/**
	 * Sets the Flash Video version.
	 * 
	 * @param aNumber
	 *            the version of the Flash Video file format that this object
	 *            utilises.
	 */
	public void setVersion(int aNumber)
	{
		version = aNumber;
	}

	/**
	 * Gets the array of video objects.
	 * 
	 * @return the array of objects.
	 */
	public ArrayList getObjects()
	{
		return objects;
	}

	/**
	 * Sets the array of objects contained in the Movie.
	 * 
	 * @param anArray
	 *            the array of objects that describe a coder.
	 */
	public void setObjects(ArrayList anArray)
	{
		objects = anArray;
	}

	/**
	 * Adds the object to the Movie.
	 * 
	 * @param anObject
	 *            the object to be added to the coder.
	 */
	public void add(FSVideoObject anObject)
	{
		objects.add(anObject);
	}

	/**
	 * Adds the array of object to the Video.
	 * 
	 * @param array
	 *            an array of FSVideoObjects that will be added to the video in
	 *            the order they are in the array.
	 */
	public void add(ArrayList array)
	{
		objects.addAll(array);
	}

	/**
	 * Decodes the contents of the specified file. An object for each tag
	 * decoded from the file is placed in the object array in the order they
	 * were read from the file. If an error occurs while reading and parsing the
	 * file then an exception is thrown.
	 * 
	 * @param fileName
	 *            the path to the Flash Video file that will be parsed.
	 * 
	 * @throws FileNotFoundException -
	 *             if an error occurs while reading the file.
	 * @throws DataFormatException -
	 *             if the file does not contain Flash data.
	 * @throws IOException -
	 *             if an error occurs while reading and decoding the file.
	 */
	public void decodeFromFile(String fileName) throws FileNotFoundException,
					DataFormatException, IOException
	{
		FileInputStream fileContents = null;
		File swfFile = new File(fileName);

		fileContents = new FileInputStream(swfFile);

		int fileLength = (int) swfFile.length();
		byte[] contents = new byte[fileLength];

		fileContents.read(contents);
		fileContents.close();

		decodeFromData(contents);
	}

	/**
	 * Decodes the binary Flash Video data stored in the byte array. If an error
	 * occurs while the data is being decoded an exception is thrown.
	 * 
	 * @param bytes
	 *            an array of bytes that contain the encoded Flash Video
	 *            objects.
	 * 
	 * @throws DataFormatException -
	 *             if the file does not contain Flash data.
	 * @throws IOException -
	 *             if an error occurs while reading and decoding the file.
	 */
	public void decodeFromData(byte[] bytes) throws DataFormatException,
					IOException
	{
		FSCoder coder = new FSCoder(FSCoder.BIG_ENDIAN, bytes);
		FSVideoObject object = null;

		boolean containsAudio = false;
		boolean containsVideo = false;

		isFlashVideo(bytes);

		signature = coder.readString(3, "UTF8");
		version = coder.readWord(1, false);
		coder.readBits(5, false);
		containsAudio = coder.readBits(1, false) != 0;
		coder.readBits(1, false);
		containsVideo = coder.readBits(1, false) != 0;

		int offset = coder.readWord(4, false);
		offset = coder.readWord(4, false);

		do
		{
			objects.add(decodeObject(coder));

			if (coder.context[Delta] != 0)
				throw new IOException();

			offset = coder.readWord(4, false);

		} while (!coder.eof());
	}

	/**
	 * Encodes the array of objects and writes the data to the specified file.
	 * If an error occurs while encoding the file then an exception is thrown.
	 * 
	 * @param fileName
	 *            the path to the file that the video will be encoded to.
	 * 
	 * @throws FileNotFoundException -
	 *             if an error occurs while reading the file.
	 * @throws IOException -
	 *             if an error occurs while encoding and writing the file.
	 */
	public void encodeToFile(String fileName) throws FileNotFoundException,
					IOException
	{
		FileOutputStream fileContents = new FileOutputStream(fileName);

		byte[] encodedData = encode();

		fileContents.write(encodedData);
		fileContents.close();
	}

	/**
	 * Returns the encoded representation of the array of objects that this
	 * Video contains. If an error occurs while encoding the file then an
	 * exception is thrown.
	 * 
	 * @return the array of bytes representing the encoded objects.
	 * @throws IOException
	 *             if an error occurs while the data is being decoded.
	 */
	public byte[] encode() throws IOException
	{
		FSCoder coder = new FSCoder(FSCoder.BIG_ENDIAN, 0);

		coder.context[FSVideo.Version] = version;

		int fileLength = length(coder);

		coder.setData(FSCoder.BIG_ENDIAN, new byte[fileLength]);

		boolean containsAudio = false;
		boolean containsVideo = false;

		for (Iterator i = objects.iterator(); i.hasNext();)
		{
			int type = ((FSVideoObject) (i.next())).getType();

			if (type == FSVideoObject.AudioData)
				containsAudio = true;
			else if (type == FSVideoObject.VideoData)
				containsVideo = true;
		}

		coder.writeString(signature, "UTF8");
		coder.writeWord(version, 1);
		coder.writeBits(0, 5);
		coder.writeBits(containsAudio ? 1 : 0, 1);
		coder.writeBits(0, 1);
		coder.writeBits(containsVideo ? 1 : 0, 1);
		coder.writeWord(9, 4);
		coder.writeWord(0, 4);

		for (Iterator i = objects.iterator(); i.hasNext();)
		{
			FSVideoObject object = (FSVideoObject) i.next();

			int length = object.getLength();
			int start = coder.getPointer();
			int next = start + (length << 3);

			object.encode(coder);
			coder.setPointer(next);

			if (((next - coder.getPointer()) >> 3) != 0)
				throw new IOException();

			coder.writeWord(length, 4);
		}
		return coder.getData();
	}

	/**
	 * Creates a deep copy of the entire video.
	 * 
	 * @return a copy of the object.
	 */
	public Object clone()
	{
		FSVideo anObject = null;

		try
		{
			anObject = (FSVideo) super.clone();

			anObject.objects = new ArrayList();

			for (Iterator i = objects.iterator(); i.hasNext();)
				anObject.objects.add(((FSVideoObject) i.next()).clone());
		} catch (CloneNotSupportedException e)
		{
			throw new InternalError();
		}

		return anObject;
	}

	/**
	 * Returns true if anObject is equal to this one. The comparison is
	 * performed on all the objects contained in the video. Objects are
	 * considered equal if they would generate identical binary data when they
	 * are encoded to a Flash Video file.
	 * 
	 * @return true if this object would be identical to anObject when encoded.
	 */
	public boolean equals(Object anObject)
	{
		boolean result = false;

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

			result = version == typedObject.version;
			result = result
							&& objects
											.equals(((FSVideo) anObject)
															.getObjects());
		}
		return result;
	}

	/**
	 * AppendDescription is used to present a string description of the object
	 * including all nested objects up to a specified depth.
	 * 
	 * The representation of the object is appended to the StringBuffer, showing
	 * the name of the class and values of the attributes it contains. If the
	 * object contains any attributes that are objects then the object graph
	 * will be traversed up to the specified depth. If objects are nested at a
	 * level less than specified depth then the full string representation of
	 * the object is displayed. For objects at the specified depth only the name
	 * of the class is displayed. Any objects below this depth are not
	 * displayed.
	 * 
	 * @param buffer
	 *            a StringBuffer to which the description of each object is
	 *            appended.
	 * @param depth
	 *            the maximum level of nesting up to which objects will be
	 *            displayed.
	 */
	public void appendDescription(StringBuffer buffer, int depth)
	{
		buffer.append("FSVideo");
		buffer.append(" : { ");
		Transform.append(buffer, "signature", signature);
		Transform.append(buffer, "version", version);
		Transform.append(buffer, "objects", objects, depth);
		buffer.append("}");
	}

	private int length(FSCoder coder)
	{
		int length = 13;

		for (Iterator i = objects.iterator(); i.hasNext();)
		{
			length += 4 + ((FSVideoObject) (i.next())).length(coder);
		}
		return length;
	}

	private void isFlashVideo(byte[] bytes) throws DataFormatException
	{
		if (bytes == null || bytes.length < 8)
			throw new DataFormatException();

		boolean isFlashVideo = bytes[0] == 0x46 && bytes[1] == 0x4C
						&& bytes[2] == 0x56;

		if (isFlashVideo == false)
			throw new DataFormatException();
	}
}
