/***************************************************************************
    Copyright          : (C) 2002 by Neoworks Limited. All rights reserved
    Copyright          : Portions copyright the original author.
    URL                : http://www.neoworks.com
 ***************************************************************************/
/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
package com.neoworks.mpeg;

import java.io.InputStream;
import java.io.IOException;
import java.io.EOFException;
import java.io.ByteArrayInputStream;
import java.util.Arrays;
import com.neoworks.util.PushBackInputStream;
import org.apache.log4j.Category;

/**
 * This class parses an MPEG audio bitstream and represents the resulting MPEG data "file".
 *
 * @author Nigel Atkinson (<a href="mailto:nigel@neoworks.com">nigel@neoworks.com</a>)
 * @author     micah
 */
public final class MPEGStream {
	private static final Category log = Category.getInstance(MPEGStream.class.getName());
	private static final boolean logDebugEnabled = log.isDebugEnabled();
	private static final boolean logInfoEnabled = log.isInfoEnabled();

	private static final int msPerFrame = 26;
	static final byte INITIAL_SYNC = 0;
	static final byte STRICT_SYNC = 1;
	private final static int BUFFER_INT_SIZE = 433;
	private final static int bitmask[] =   {0,
						0x00000001, 0x00000003, 0x00000007, 0x0000000F,
						0x0000001F, 0x0000003F, 0x0000007F, 0x000000FF,
						0x000001FF, 0x000003FF, 0x000007FF, 0x00000FFF,
						0x00001FFF, 0x00003FFF, 0x00007FFF, 0x0000FFFF,
						0x0001FFFF};

	private PushBackInputStream source;
	private final int[] framebuffer = new int[BUFFER_INT_SIZE];
	private int framesize = 0;
	private final byte[] frame_bytes = new byte[BUFFER_INT_SIZE * 4];
	private int wordpointer = -1;
	private int bitindex = -1;
	private int syncword = 0;
	private boolean single_ch_mode = false;
	private final byte syncbuf[] = new byte[4];
	private final byte headbuf[] = new byte[4];
	private boolean firstFrame = true;
	private XingVBRHeader vbrHeader = null;


	/**
	 * Public constructor
	 *
	 * @param in The InputStream to read data from
	 */
	public MPEGStream(InputStream in) {
		source = new PushBackInputStream(in);
		Arrays.fill(headbuf,(byte)0);
		closeFrame();
	}

	/**
	 * Get the playing time in milliseconds for this MPEG Stream
	 *
	 * @return The playing time in milliseconds
	 */
	public long getPlayingTime()
	{
		long retVal = 0;
		MPEGFrame f = null;
		try {
			while (true)
			{
				f = readFrame();
				if (f != null)
				{
					if (f.getFrameSize() < 1)
					{
						log.info("frame length is 0, stopping");
						break;
					}
					retVal += msPerFrame;
				} else {
					log.info("frame is null, stopping");
					break;
				}
			}
			log.info("NULL frame " + (retVal/msPerFrame) + " frames");
		} catch (IOException e) {
			log.warn("IOException encountered while getting the playing time", e);
		}
		return retVal;
	}

	private long getStreamPos()
	{
		return source.getPos();
	}

	/**
	 * Reads and parses the next frame from the input source.
	 *
	 * @return the Header describing details of the frame read, or null if the end of the stream has been reached.
	 * @exception IOException
	 */
	public MPEGFrame readFrame() throws IOException
	{
		int headerstring = -1;
		boolean sync = false;
		byte syncmode = INITIAL_SYNC;
		MPEGFrame retVal = null;
		short checksum;
		int unreadCount = 0;

		do {
			headerstring = syncHeader(syncmode);
			if (headerstring == -1)
			{
				if (logInfoEnabled)log.info("bad headerstring");
				break;
			} else {
				try {
					retVal = new MPEGFrame(headerstring);
					//if (logDebugEnabled) log.debug("getting frame data (" + (retVal.getFrameSize() - 4) + " bytes)");
					read_frame_data(retVal.getFrameSize() - 4);
					retVal.setFrameData(getFrameData());

					if (firstFrame)
					{
						getVBRHeader(retVal);
					}
					
					if (isSyncCurrentPosition(syncmode)) {
						// only reset first frame flag when we have actually verified that it is an actual frame
						firstFrame = false;
						if (syncmode == INITIAL_SYNC) {
							syncmode = STRICT_SYNC;
							// mask out anything that is likely to change in the header
							// mask pattern: 11111111111010000000110011000000
							//              |framesync  |version|sr |channel |
							// Leave stereo mode, sample rate and version flag
							set_syncword(headerstring & 0xFFE80CC0);
						}
						sync = true;
						parse_frame();
					} else {
						if (unreadCount++ > 15)
						{
							log.warn("Failed to sync on next frame after 15 attempts");
							retVal = null;
							break;
						}
						if (logInfoEnabled) log.info("Out of sync, trying to resync");
						unreadFrame();
						syncmode = INITIAL_SYNC;
					}
				} catch (CorruptMPEGHeaderException e) {
					log.warn("Could not parse header",e);
					break;
				} catch (IOException ioe) {
					if (logInfoEnabled) log.info("IOException while reading frame", ioe);
					retVal = null;
					break;
				} catch (Exception ex) {
					if (logInfoEnabled) log.info("Exception while reading frame", ex);
					retVal = null;
					break;
				}
			}
		} while (!sync);

		if (retVal != null && retVal.isCRCProtected())
		{
			// frame contains a crc checksum
			if (logDebugEnabled) log.debug("getting checksum");
			try {
			 	checksum = (short)readbits(16);
				retVal.setCRCChecksum(checksum);
			} catch (ArrayIndexOutOfBoundsException e) {
				log.warn("Tried to read checksum", e);
			}
		}
		return retVal;
	}

	/**
	 * Close the source stream
	 *
	 * @exception  IOException
	 */
	public void close() throws IOException
	{
		source.close();
		closeFrame();
	}

	/**
	 *
	 */
	private final boolean isSyncCurrentPosition(int syncmode) throws IOException {
		int read = 0;
		int headerstring = 0;

		if ((read = source.read(syncbuf, 0, 4)) > 0)
		{
			source.unread(syncbuf, 0, read);
 			if (read == 4)
			{
				headerstring = ((syncbuf[0] << 24) & 0xFF000000) | ((syncbuf[1] << 16) & 0x00FF0000) | ((syncbuf[2] << 8) & 0x0000FF00) | (syncbuf[3] & 0x000000FF);
				return isSyncMark(headerstring, syncmode, syncword);
			}
		}
		return false;
  	}

	/**
	 * Gets the syncMark attribute of the Bitstream object]
	 *
	 *@param headerstring  Description of Parameter
	 *@param syncmode      Description of Parameter
	 *@param word          Description of Parameter
	 *@return               The syncMark value
	 */
	private final boolean isSyncMark(int headerstring, int syncmode, int word) {
		boolean sync = false;
		if (syncmode == INITIAL_SYNC) {
			// NEA modified to allow version 2.5 frames
			sync = ((headerstring & 0xFFE00000) == 0xFFE00000);
		} else {
			sync = ((headerstring & 0xFFE80C00) == word) && (((headerstring & 0x000000C0) == 0x000000C0) == single_ch_mode);
		}
		if(sync)
		{
			// filter out invalid sample rate
			//if(sync = (((headerstring >> 10) & 3) != 3))
			if(sync = (((headerstring >>> 10) & 3) != 3))
			{
				// filter out invalid layer
				sync = (((headerstring >> 17) & 3) != 0);
				if (!sync)
				{
					if (logDebugEnabled) log.debug("Sync FAILED: bad layer      : (" + getStreamPos() + ")\t" + Integer.toBinaryString(headerstring));
				}
				/*if(sync = (((headerstring >>> 17) & 3) != 0))
				{
					// filter out invalid version
					//sync = (((headerstring >> 19) & 3) != 1);
					sync = (((headerstring >>> 19) & 3) != 1);
				}*/
			} else {
				if (logInfoEnabled) log.info("Sync FAILED: bad sample rate: (" + getStreamPos() + ")\t" + Integer.toBinaryString(headerstring));
			}
		} else {
			if (logDebugEnabled) log.debug("Sync FAILED: headerstring   : (" + getStreamPos() + ")\t" + Integer.toBinaryString(headerstring));
		}

		return sync;
	}

	/**
	 * Read the specified number of bits from the frameBuffer
	 *
	 * @param num The number of bits to read
	 * @return An int value representing the bits read
	 */
	private final int readbits(int num) {
		int returnvalue = 0;
		int sum = bitindex + num;

		if (sum <= 32) {
			returnvalue = (framebuffer[wordpointer] >>> (32 - sum)) & bitmask[num];
			if ((bitindex += num) == 32) {
				bitindex = 0;
				wordpointer++;
			}
		} else {
			returnvalue = (((framebuffer[wordpointer++] & 0x0000FFFF) << 16) & 0xFFFF0000) | (((framebuffer[wordpointer] & 0xFFFF0000) >>> 16) & 0x0000FFFF);
			returnvalue >>>= 48 - sum;
			returnvalue &= bitmask[num];
			bitindex = sum - 32;
		}
		return returnvalue;
	}

	/**
	 * Extract any VBR header from the MPEG frame
	 */
	private void getVBRHeader(MPEGFrame frame)
	{
		if (logDebugEnabled) log.debug("First frame, checking for VBR header");
		///Work out whether there is an XING VBR header
		try {
			vbrHeader = new XingVBRHeader(getFrameData(),0,frame.getMPEGLayer(), frame.getMPEGVersion(),frame.sample_frequency_index(),frame.mode());

			if (vbrHeader.headerExists())
			{
				if (logInfoEnabled) log.info(vbrHeader.toString());
				//unreadFrame();
				//if (logDebugEnabled) log.debug("Reading VBR header frame (" + (vbrHeader.getLength() + vbrHeader.getHeaderOffset() + 4) + " bytes)");
				//frame.setFrameSize(vbrHeader.getLength() + vbrHeader.getHeaderOffset() + 4);
				//read_frame_data(frame.getFrameSize() - 4);
			} else {
				if (logInfoEnabled) log.info("No VBR header");
				if (logDebugEnabled){
					byte [] frameData = frame.getFrameData();
					StringBuffer debugBuf = new StringBuffer();
					for (int i = 0; i < frameData.length; i++)
					{
						debugBuf.append(Byte.toString(frameData[i]));
						debugBuf.append(" ");
						if (i%7 == 1)
							debugBuf.append("\n");
					}
					log.debug(debugBuf.toString());
				}
			}
		} catch(Exception e) {
			log.warn("Exception encountered while checking for VBR header", e);
		}
	}

	/**
	 * Unreads the bytes read from the frame.
	 *
	 * @exception  IOException      Description of Exception
	 * @throws  BitstreamException
	 */
	// REVIEW: add new error codes for this.
	private final void unreadFrame() throws IOException
	{
		if (wordpointer == -1 && bitindex == -1 && (framesize > 0)) {
			if (logInfoEnabled) log.info("unreading " + framesize + " bytes");
			source.unread(frame_bytes, 0, framesize);
		}
	}

	/**
	 * Close this frame.
	 */
	private void closeFrame() {
		framesize = wordpointer = bitindex = -1;
	}

	/**
	 * Set the word we want to sync the header to. In Big-Endian byte order
	 * @param syncword0 The word to sync the header to.
	 */
	private final void set_syncword(int syncword0) {
		// mask out channel mode bits
		syncword = syncword0 & 0xFFFFFF3F;
		// set single channel mode flag
		single_ch_mode = ((syncword0 & 0x000000C0) == 0x000000C0);
	}

	/**
	 * Synchronise the stream with the next header block
	 *
	 * @param syncmode The synchronisation mode
	 * @return The header block (as an int)
	 * @exception IOException
	 */
	private int syncHeader(byte syncmode) throws IOException
	{
		int headerstring = 0;

		// read 3 bytes into the syncbuffer
		if(source.read(syncbuf, 0, 3) != 3)
		{
			// ran out of data
			if (logInfoEnabled) log.info("Failed to read header");
			return -1;
		}

		// pack the bytes into an int
		headerstring = ((syncbuf[0] << 16) & 0xFF0000) | ((syncbuf[1] << 8) & 0xFF00) | ((syncbuf[2] << 0) & 0xFF);

		int slides = 0;

		do {
			slides++;
			headerstring <<= 8;
			if(source.read(syncbuf, 3, 1) != 1)
			{
				// ran out of data
				return -1;
			}
			headerstring |= syncbuf[3] & 0xff;
			if (logDebugEnabled) log.debug("syncing header: " + Integer.toBinaryString(headerstring));
		} while (!isSyncMark(headerstring, syncmode, syncword));

		if (slides > 1)
		{
			if (logInfoEnabled) log.info("header synced  on " + Integer.toBinaryString(headerstring) + " after " + slides + " attempts");
		} else {
			if (logDebugEnabled) log.debug("header synced on " + Integer.toBinaryString(headerstring));
		}
		headbuf[0] = (byte)((headerstring >> 24) & 0xFF);
		headbuf[1] = (byte)((headerstring >> 16) & 0xFF);
		headbuf[2] = (byte)((headerstring >> 8) & 0xFF);
		headbuf[3] = (byte)(headerstring & 0xFF);

		return headerstring;
	}

	/**
	 * Reads the data for the next frame. The frame is not parsed until parse frame is called.
	 * @param	bytesize	The size in bytes of the frame
	 * @exception	IOException
	 */
	private final void read_frame_data(int bytesize) throws IOException
	{
		if(bytesize >= 0)
		{
			framesize = bytesize;
			wordpointer = bitindex = -1;
			if (source.read(frame_bytes, 0, bytesize) != bytesize)
			{
				unreadFrame();
				if (logInfoEnabled) log.info("Failed to read frame data.");
				throw new IOException("Failed to read frame data");
			}
		}
	}

	/**
	 * Get the entire frame (header and everything) as a byte array
	 *
	 * @return A byte array containing the complete frame
	 */
	private byte[] getFrameData()
	{
		//if (logDebugEnabled) log.debug("Getting " + (framesize + 4) + " bytes of frame data");
		byte[] retVal = new byte[framesize + 4];
		ByteArrayInputStream bais = new ByteArrayInputStream(frame_bytes);
		try {
			bais.read(retVal, 4, framesize);
		} catch (Exception e) {
			log.warn("Exception encountered while getting frame data", e);
			log.warn("Frame size = " + framesize);
		}
		retVal[0] = headbuf[0];
		retVal[1] = headbuf[1];
		retVal[2] = headbuf[2];
		retVal[3] = headbuf[3];
		return retVal;
	}

	/**
	 * Parses the data previously read with read_frame_data().
	 */
	private final void parse_frame() {
		int b, k;
		byte b0;
		byte b1 = 0;
		byte b2 = 0;
		byte b3 = 0;
		// Convert Bytes read to int
		for (k = 0, b = 0; k < framesize; k += 4) {
			b0 = frame_bytes[k];
			if (k + 3 < framesize) {
				b3 = frame_bytes[k + 3];
				b2 = frame_bytes[k + 2];
				b1 = frame_bytes[k + 1];
			} else if (k + 2 < framesize) {
				b3 = 0;
				b2 = frame_bytes[k + 2];
				b1 = frame_bytes[k + 1];
			} else if (k + 1 < framesize) {
				b2 = b3 = 0;
				b1 = frame_bytes[k + 1];
			}
			framebuffer[b++] = ((b0 << 24) & 0xFF000000) | ((b1 << 16) & 0x00FF0000) | ((b2 << 8) & 0x0000FF00) | (b3 & 0x000000FF);
		}
		wordpointer = bitindex = 0;
	}
}
