/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */

package freenet.client.filter;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;

import freenet.l10n.NodeL10n;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;

/** Represents a single page of an Ogg bitstream
 * @author sajack
 */
public class OggPage {
	boolean logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
	static final byte[] magicNumber = new byte[] {0x4f, 0x67, 0x67, 0x53};
	/*This CRC lookup table was taken from libogg. These values
	 * are XORed with 
	 * See: http://www.ross.net/crc/download/crc_v3.txt
	 */
	static final private int crc_lookup[]= new int[] {
		0x00000000,0x04c11db7,0x09823b6e,0x0d4326d9,
		0x130476dc,0x17c56b6b,0x1a864db2,0x1e475005,
		0x2608edb8,0x22c9f00f,0x2f8ad6d6,0x2b4bcb61,
		0x350c9b64,0x31cd86d3,0x3c8ea00a,0x384fbdbd,
		0x4c11db70,0x48d0c6c7,0x4593e01e,0x4152fda9,
		0x5f15adac,0x5bd4b01b,0x569796c2,0x52568b75,
		0x6a1936c8,0x6ed82b7f,0x639b0da6,0x675a1011,
		0x791d4014,0x7ddc5da3,0x709f7b7a,0x745e66cd,
		0x9823b6e0,0x9ce2ab57,0x91a18d8e,0x95609039,
		0x8b27c03c,0x8fe6dd8b,0x82a5fb52,0x8664e6e5,
		0xbe2b5b58,0xbaea46ef,0xb7a96036,0xb3687d81,
		0xad2f2d84,0xa9ee3033,0xa4ad16ea,0xa06c0b5d,
		0xd4326d90,0xd0f37027,0xddb056fe,0xd9714b49,
		0xc7361b4c,0xc3f706fb,0xceb42022,0xca753d95,
		0xf23a8028,0xf6fb9d9f,0xfbb8bb46,0xff79a6f1,
		0xe13ef6f4,0xe5ffeb43,0xe8bccd9a,0xec7dd02d,
		0x34867077,0x30476dc0,0x3d044b19,0x39c556ae,
		0x278206ab,0x23431b1c,0x2e003dc5,0x2ac12072,
		0x128e9dcf,0x164f8078,0x1b0ca6a1,0x1fcdbb16,
		0x018aeb13,0x054bf6a4,0x0808d07d,0x0cc9cdca,
		0x7897ab07,0x7c56b6b0,0x71159069,0x75d48dde,
		0x6b93dddb,0x6f52c06c,0x6211e6b5,0x66d0fb02,
		0x5e9f46bf,0x5a5e5b08,0x571d7dd1,0x53dc6066,
		0x4d9b3063,0x495a2dd4,0x44190b0d,0x40d816ba,
		0xaca5c697,0xa864db20,0xa527fdf9,0xa1e6e04e,
		0xbfa1b04b,0xbb60adfc,0xb6238b25,0xb2e29692,
		0x8aad2b2f,0x8e6c3698,0x832f1041,0x87ee0df6,
		0x99a95df3,0x9d684044,0x902b669d,0x94ea7b2a,
		0xe0b41de7,0xe4750050,0xe9362689,0xedf73b3e,
		0xf3b06b3b,0xf771768c,0xfa325055,0xfef34de2,
		0xc6bcf05f,0xc27dede8,0xcf3ecb31,0xcbffd686,
		0xd5b88683,0xd1799b34,0xdc3abded,0xd8fba05a,
		0x690ce0ee,0x6dcdfd59,0x608edb80,0x644fc637,
		0x7a089632,0x7ec98b85,0x738aad5c,0x774bb0eb,
		0x4f040d56,0x4bc510e1,0x46863638,0x42472b8f,
		0x5c007b8a,0x58c1663d,0x558240e4,0x51435d53,
		0x251d3b9e,0x21dc2629,0x2c9f00f0,0x285e1d47,
		0x36194d42,0x32d850f5,0x3f9b762c,0x3b5a6b9b,
		0x0315d626,0x07d4cb91,0x0a97ed48,0x0e56f0ff,
		0x1011a0fa,0x14d0bd4d,0x19939b94,0x1d528623,
		0xf12f560e,0xf5ee4bb9,0xf8ad6d60,0xfc6c70d7,
		0xe22b20d2,0xe6ea3d65,0xeba91bbc,0xef68060b,
		0xd727bbb6,0xd3e6a601,0xdea580d8,0xda649d6f,
		0xc423cd6a,0xc0e2d0dd,0xcda1f604,0xc960ebb3,
		0xbd3e8d7e,0xb9ff90c9,0xb4bcb610,0xb07daba7,
		0xae3afba2,0xaafbe615,0xa7b8c0cc,0xa379dd7b,
		0x9b3660c6,0x9ff77d71,0x92b45ba8,0x9675461f,
		0x8832161a,0x8cf30bad,0x81b02d74,0x857130c3,
		0x5d8a9099,0x594b8d2e,0x5408abf7,0x50c9b640,
		0x4e8ee645,0x4a4ffbf2,0x470cdd2b,0x43cdc09c,
		0x7b827d21,0x7f436096,0x7200464f,0x76c15bf8,
		0x68860bfd,0x6c47164a,0x61043093,0x65c52d24,
		0x119b4be9,0x155a565e,0x18197087,0x1cd86d30,
		0x029f3d35,0x065e2082,0x0b1d065b,0x0fdc1bec,
		0x3793a651,0x3352bbe6,0x3e119d3f,0x3ad08088,
		0x2497d08d,0x2056cd3a,0x2d15ebe3,0x29d4f654,
		0xc5a92679,0xc1683bce,0xcc2b1d17,0xc8ea00a0,
		0xd6ad50a5,0xd26c4d12,0xdf2f6bcb,0xdbee767c,
		0xe3a1cbc1,0xe760d676,0xea23f0af,0xeee2ed18,
		0xf0a5bd1d,0xf464a0aa,0xf9278673,0xfde69bc4,
		0x89b8fd09,0x8d79e0be,0x803ac667,0x84fbdbd0,
		0x9abc8bd5,0x9e7d9662,0x933eb0bb,0x97ffad0c,
		0xafb010b1,0xab710d06,0xa6322bdf,0xa2f33668,
		0xbcb4666d,0xb8757bda,0xb5365d03,0xb1f740b4
		};

	//Page header contained here
	final byte version;
	final byte headerType;
	final byte[] granuelPosition;
	final byte[] bitStreamSerial;
	final byte[] pageSequenceNumber;
	byte[] checksum = new byte[4];
	byte segments;
	byte[] segmentTable;
	byte[] payload;

	public OggPage(DataInputStream input) throws IOException {
		version=input.readByte();
		Logger.minor(this, "Version: "+version);
		headerType = input.readByte();
		Logger.minor(this, "Headertype: "+headerType);
		this.granuelPosition = new byte[8];
		this.bitStreamSerial = new byte[4];
		this.pageSequenceNumber = new byte[4];
		input.readFully(granuelPosition);
		input.readFully(bitStreamSerial);
		input.readFully(pageSequenceNumber);
		input.readFully(checksum);
		Logger.minor(this, "Checksum: "+Integer.toHexString(byteToUnsigned(checksum[0]))+Integer.toHexString(byteToUnsigned(checksum[1]))+Integer.toHexString(byteToUnsigned(checksum[2]))+Integer.toHexString(byteToUnsigned(checksum[3])));
		segments = intToUnsignedByte(input.readUnsignedByte());
		segmentTable = new byte[byteToUnsigned(segments)];
		input.readFully(segmentTable);
		int payloadSize = 0;
		for(int i = 0; i < byteToUnsigned(segments); i++) {
			payloadSize += byteToUnsigned(segmentTable[i]);
		}
		payload = new byte[payloadSize];
		input.readFully(payload);
		Logger.minor(this, "Created page with "+segments+" segments");
	}

	public OggPage(OggPage oldPage, Collection<CodecPacket> packets) throws IOException {
		this.version = oldPage.version;
		this.headerType = oldPage.headerType;
		Logger.minor(this, "Header type: "+Integer.toBinaryString(this.headerType));
		this.granuelPosition = oldPage.granuelPosition;
		this.bitStreamSerial = oldPage.bitStreamSerial;
		this.pageSequenceNumber = oldPage.pageSequenceNumber;
		this.segments = 0;
		ArrayList<Byte> segmentSizes = new ArrayList<Byte>();
		ByteArrayOutputStream payloadStream = new ByteArrayOutputStream();
		for(CodecPacket packet : packets) {
			int wholeSegments = packet.payload.length / 255;
			int concludingPartialSegment = packet.payload.length % 255;
			Logger.minor(this, "Whole segments: "+wholeSegments+" Partial: "+concludingPartialSegment);
			for(int i = 0; i < wholeSegments; i++) {
				segmentSizes.add(intToUnsignedByte(255));
			}
			if(concludingPartialSegment != 0) {
				segmentSizes.add(intToUnsignedByte(concludingPartialSegment));
			}
			Logger.minor(this, "Writing packet sized: "+packet.payload.length);
			payloadStream.write(packet.payload);
		}
		this.segments = intToUnsignedByte(segmentSizes.size());
		this.segmentTable = new byte[byteToUnsigned(segments)];
		Logger.minor(this, "SegmentSizes len: "+segmentSizes.size()+" SegmentTable size: "+segmentTable.length+" Segments: "+segments);
		for(int i = 0; i < segmentSizes.size(); i++) {
			this.segmentTable[i] = segmentSizes.get(i);
		}

		payloadStream.close();
		this.payload = payloadStream.toByteArray();
		Logger.minor(this, "Payload size: "+this.payload.length+"Made of "+packets.size()+" packets");
		this.checksum = calculateCRC();
	}

	/**Extracts the Ogg page from a physical bitstream
	 * @param input a stream of data containing a physical bitstream
	 * @return the next Ogg page in bitstream
	 * @throws IOException
	 */
	public static void seekToPage(DataInputStream input) throws IOException {
		while(true) {
			//Seek for magic number
			if(input.readByte() != magicNumber[0]) continue;
			if(input.readByte() != magicNumber[1]) continue;
			if(input.readByte() != magicNumber[2]) continue;
			if(input.readByte() != magicNumber[3]) continue;
			return;
		}
		//If we've found all of the previous magic numbers, we've probably found a page
	}
	/**Extracts the Ogg page from a physical bitstream
	 * @param input a stream of data containing a physical bitstream
	 * @return the next Ogg page in bitstream
	 * @throws IOException
	 */
	public static OggPage readPage(DataInputStream input) throws IOException {
		seekToPage(input);
		return new OggPage(input);
	}

	/**Checks some header values for sanity, and verifies this Page's
	 * <code>checksum</code> field.
	 * @return whether or not the page is valid
	 */
	public boolean headerValid() {
		if(version != 0) return false;
		if(!Arrays.equals(checksum, calculateCRC())) return false;
		return true;
	}

	public boolean isPacketContinued() {
		if(logMINOR) Logger.minor(this, "Packet continued: "+(headerType & 0x1));
		return (headerType & 0x01) == 1;
	}

	public boolean isFinalPacket() {
		return (headerType & 0x04) == 4;
	}

	public byte[] toArray() {
		ByteBuffer bb = ByteBuffer.allocate(27+byteToUnsigned(segments)+payload.length);
		bb.put(magicNumber);
		bb.put(version);
		bb.put(headerType);
		bb.put(granuelPosition);
		bb.put(bitStreamSerial);
		bb.put(pageSequenceNumber);
		bb.put(checksum);
		bb.put(segments);
		bb.put(segmentTable);
		bb.put(payload);
		return bb.array();
	}

	public int getSerial() {
		ByteBuffer bb = ByteBuffer.wrap(bitStreamSerial);
		return bb.getInt();
	}

	public int getPageNumber() {
		ByteBuffer bb = ByteBuffer.wrap(pageSequenceNumber);
		return Integer.reverseBytes(bb.getInt());
	}

	/**Calculates this page's 32 bit CRC checksum.*/
	public byte[] calculateCRC() {
		byte[] array = toArray();
		//Strip out the checksum bytes
		array[22] = 0;
		array[23] = 0;
		array[24] = 0;
		array[25] = 0;
		int crc_reg = 0;
		for(int i=0;i<array.length;i++) {
			/*Ugly, no? This line was taken from jorbis, which, I'd bet money, adapted it to java from libogg,
			 * which in turn took it from http://www.ross.net/crc/download/crc_v3.txt */
			crc_reg=(crc_reg<<8) ^ crc_lookup[((crc_reg>>>24) & 0xff) ^ (array[i] & 0xff)];
		}
		return new byte[] { (byte)crc_reg,
				(byte) (crc_reg>>>8),
				(byte) (crc_reg>>>16),
				(byte) (crc_reg>>>24)};
	}

	/**Rewrites the stored sizes of this page's segments.
	 * @param packetSizes The sizes of any packets inside of this page's payload.
	 * If not null, a segment will be prematurely closed after each packet's size
	 * number of bytes have been read.
	 */
	public void recalculateSegmentLacing(LinkedList<Integer> packetSizes) {
		/*Will packets ever need to be expanded? Right now we're just cutting
		 * stuff away, but if we need to write stuff, we run the risk of overflowing
		 * past the hard limit of 255 packets, and will need to create a continuing page
		 */
		if(packetSizes == null) {
			packetSizes = new LinkedList<Integer>();
			packetSizes.push(payload.length);
		}
		segments = 0;
		for(int packet : packetSizes) {
			segments += packet / 255 + (packet % 255 == 0 ? 0 : 1);
			if(logMINOR) Logger.minor(this, "Size of current packet: "+packet+" Current number of segments: "+segments+" Number of whole segments belonging to this packet: "+packet/255+ " Remaining bytes "+packet%255);
		}
		if(logMINOR) Logger.minor(this, "Segments "+segments);
		segmentTable = new byte[segments];
		int segment = 0;
		for(int packet : packetSizes) {
			if(logMINOR) Logger.minor(this, "Setting segments for packet sized "+packet);
			for(int packetSegment = 0; packetSegment < packet / 255; packetSegment++) {
				if(logMINOR) Logger.minor(this, "Setting segment "+segment+" to full.");
				segmentTable[segment] = intToUnsignedByte(255);
				segment++;
			}
			int remainder = packet % 255;
			if(remainder != 0) {
				if(logMINOR) Logger.minor(this, "Partially filling segment "+segment);
				segmentTable[segment] = intToUnsignedByte(remainder);
				segment++;
			}
		}
	}

	public Collection<CodecPacket> asPackets() {
		Logger.minor(this, "Creating packets for "+byteToUnsigned(segments)+ " segments");
		ArrayList<CodecPacket> packets = new ArrayList<CodecPacket>();
		int bytesParsed = 0;
		int packetSize = 0;
		for(int i = 0; i < segmentTable.length; i++) {
			if((byteToUnsigned(segmentTable[i]) % 255 != 0) || byteToUnsigned(segmentTable[i]) == 0 || i == segmentTable.length-1) {
				packetSize += byteToUnsigned(segmentTable[i]);
				byte[] packetPayload = new byte[packetSize];
				try {
					System.arraycopy(payload, bytesParsed, packetPayload, 0, packetSize);
				} catch(java.lang.ArrayIndexOutOfBoundsException e) {
					Logger.error(this, "Error, Out of Bounds."+"Page sequence number: "+byteToUnsigned(this.pageSequenceNumber[0])+" "+byteToUnsigned(this.pageSequenceNumber[1])+" "+byteToUnsigned(this.pageSequenceNumber[2])+" "+byteToUnsigned(this.pageSequenceNumber[3])+" bytesParsed: "+bytesParsed+" packetSize: "+packetSize+" Payload length: "+payload.length, e);
					throw e;
				}

				bytesParsed += packetSize;
				packets.add(new CodecPacket(packetPayload));
				packetSize = 0;
			} else {
				packetSize += 255;
			}
		}
		return packets;
	}

	static private int byteToUnsigned(byte input) {
		return (input & 0xff);
	}

	static private byte intToUnsignedByte(int input) {
		return (byte) (input & 0xff);
	}

	private String l10n(String key) {
		return NodeL10n.getBase().getString("OggPage." + key);
	}
}
