package tv.porst.swfretools.parser;

import java.util.List;

import tv.porst.splib.binaryparser.AsciiString;
import tv.porst.splib.binaryparser.BinaryParserHelpers;
import tv.porst.splib.binaryparser.Bits;
import tv.porst.splib.binaryparser.Flag;
import tv.porst.splib.binaryparser.Float16;
import tv.porst.splib.binaryparser.Float32;
import tv.porst.splib.binaryparser.Float64;
import tv.porst.splib.binaryparser.IFileElement;
import tv.porst.splib.binaryparser.INT16;
import tv.porst.splib.binaryparser.INT24;
import tv.porst.splib.binaryparser.INT32;
import tv.porst.splib.binaryparser.UBits;
import tv.porst.splib.binaryparser.UINT16;
import tv.porst.splib.binaryparser.UINT32;
import tv.porst.splib.binaryparser.UINT8;
import tv.porst.swfretools.parser.structures.ByteArray;

/**
 * Contains helper functions for SWF parsing.
 * 
 * @author sp
 *
 */
public final class SWFParserHelpers {

	/**
	 * Throws an exception if less than a given number of bytes remain unparsed in the input stream.
	 * 
	 * @param parser The parser that provides the stream.
	 * @param numberOfBytes The number of remaining bytes to check for.
	 * @param errorCode The error code of the exception to be thrown.
	 * @param fieldName Name of the field to be parsed.
	 * 
	 * @throws SWFParserException Thrown if fewer than the given number of bytes are left in the input stream.
	 */
	private static void throwIf(final SWFBinaryParser parser, final long numberOfBytes, final int errorCode, final String fieldName) throws SWFParserException {
		if (!BinaryParserHelpers.hasBytesLeft(parser, numberOfBytes)) {
			throw new SWFParserException(errorCode, parser.getBytePosition(), String.format("Read beyond file while parsing %s (%08X)", fieldName, parser.getBytePosition()));
		}
	}

	/**
	 * Throws an exception if less than a given number of bits remain unparsed in the input stream.
	 * 
	 * @param parser The parser that provides the stream.
	 * @param numberOfBits The number of remaining bits to check for.
	 * @param errorCode The error code of the exception to be thrown.
	 * @param fieldName Name of the field to be parsed.
	 * 
	 * @throws SWFParserException Thrown if fewer than the given number of bits are left in the input stream.
	 */
	private static void throwIfB(final SWFBinaryParser parser, final int numberOfBits, final int errorCode, final String fieldName) throws SWFParserException {
		if (!BinaryParserHelpers.hasBitsLeft(parser, numberOfBits)) {
			throw new SWFParserException(errorCode, parser.getBytePosition(), String.format("Read beyond file while parsing %s (%08X)", fieldName, parser.getBytePosition()));
		}
	}

	/**
	 * Sums the bit lengths of the given file elements.
	 * 
	 * @param elements The file elements whose bit lengths are summed.
	 * 
	 * @return The summed bit lengths of the file elements.
	 */
	public static int addBitLengths(final IFileElement ... elements) {

		int length = 0;

		for (final IFileElement element : elements) {

			if (element != null) {
				length += element.getBitLength();
			}
		}

		return length;

	}

	/**
	 * Sums the bit lengths of a list of file elements.
	 * 
	 * @param elements The file elements whose bit lengths are summed.
	 * 
	 * @return The summed bit lengths of the file elements.
	 */
	public static int addBitLengths(final List<? extends IFileElement> elements) {

		int length = 0;

		for (final IFileElement element : elements) {

			if (element != null) {
				length += element.getBitLength();
			}
		}

		return length;

	}

	/**
	 * Throws an exception if an argument is null.
	 * 
	 * @param value The value to check.
	 * @param valueName The name of the value to check.
	 */
	public static void checkNull(final Object value, final String valueName) {
		if (value == null) {
			throw new IllegalArgumentException(String.format("%s argument must not be null"));
		}
	}

	/**
	 * Parses a byte array.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param numberOfBytes Number of bytes to add to the byte array.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed byte array.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static ByteArray parseByteArray(final SWFBinaryParser parser, final long numberOfBytes, final int errorCode, final String fieldName) throws SWFParserException {
		throwIf(parser, numberOfBytes, errorCode, fieldName);

		return new ByteArray(parser.getBytePosition() * 8 + parser.getBitPosition(), BinaryParserHelpers.readByteArray(parser, (int) numberOfBytes));
	}

	/**
	 * Parses a byte array if a condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param numberOfBytes Number of bytes to add to the byte array.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the array to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed byte array.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static ByteArray parseByteArrayIf(final SWFBinaryParser parser, final int numberOfBytes, final int errorCode, final boolean condition, final String fieldName) throws SWFParserException {
		return condition ? parseByteArray(parser, numberOfBytes, errorCode, fieldName) : null;
	}

	/**
	 * Parses a byte array if a condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param numberOfBytes Number of bytes to add to the byte array.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the array to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed byte array.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static ByteArray parseByteArrayIf(final SWFBinaryParser parser, final int numberOfBytes, final int errorCode, final Flag condition, final String fieldName) throws SWFParserException {

		checkNull(condition, "Condition");

		return condition.value() ? parseByteArray(parser, numberOfBytes, errorCode, fieldName) : null;
	}

	/**
	 * Parses a double.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed double.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static Float64 parseDouble(final SWFBinaryParser parser, final int errorCode, final String fieldName) throws SWFParserException {
		throwIf(parser, 8, errorCode, fieldName);

		return parser.readFloat64();
	}

	/**
	 * Parses a flag.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed flag.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static Flag parseFlag(final SWFBinaryParser parser, final int errorCode, final String fieldName) throws SWFParserException {
		throwIfB(parser, 1, errorCode, fieldName);

		return parser.readFlag();
	}

	/**
	 * Parses a flag if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the flag to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed flag.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static Flag parseFlagIf(final SWFBinaryParser parser, final int errorCode, final boolean condition, final String fieldName) throws SWFParserException {
		return condition ? parseFlag(parser, errorCode, fieldName) : null;
	}

	/**
	 * Parses a float.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed float.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static Float32 parseFloat(final SWFBinaryParser parser, final int errorCode, final String fieldName) throws SWFParserException {
		throwIf(parser, 4, errorCode, fieldName);

		return parser.readFloat();
	}

	/**
	 * Parses a short float.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed short float.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static Float16 parseFloat16(final SWFBinaryParser parser, final int errorCode, final String fieldName) throws SWFParserException {
		throwIf(parser, 2, errorCode, fieldName);

		return parser.readFloat16();
	}

	/**
	 * Parses a 16 bit integer.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 16 bit integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static INT16 parseINT16(final SWFBinaryParser parser, final int errorCode, final String fieldName) throws SWFParserException {
		throwIf(parser, UINT16.BYTE_LENGTH, errorCode, fieldName);
		return parser.readInt16();
	}

	/**
	 * Parses a 16 bit integer if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 16 bit integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static INT16 parseINT16If(final SWFBinaryParser parser, final int errorCode, final boolean condition, final String fieldName) throws SWFParserException {
		return condition ? parseINT16(parser, errorCode, fieldName) : null;
	}

	/**
	 * Parses a 16 bit integer if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 16 bit integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static INT16 parseINT16If(final SWFBinaryParser parser, final int errorCode, final Flag condition, final String fieldName) throws SWFParserException {
		return condition.value() ? parseINT16(parser, errorCode, fieldName) : null;
	}

	/**
	 * Parses a 24 bit integer.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 24 bit integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static INT24 parseINT24(final SWFBinaryParser parser, final int errorCode, final String fieldName) throws SWFParserException {
		throwIf(parser, 3, errorCode, fieldName);
		return parser.readInt24();
	}

	/**
	 * Parses a 32 bit integer.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 32 bit integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static INT32 parseINT32(final SWFBinaryParser parser, final int errorCode, final String fieldName) throws SWFParserException {
		throwIf(parser, INT32.BYTE_LENGTH, errorCode, fieldName);
		return parser.readInt32();
	}

	/**
	 * Parses a signed bit field.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param numberOfBits Number of bits to parse.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed signed bit field.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static Bits parseSBits(final SWFBinaryParser parser, final int numberOfBits, final int errorCode, final String fieldName) throws SWFParserException {
		throwIfB(parser, numberOfBits, errorCode, fieldName);

		return parser.readSBits(numberOfBits);
	}

	/**
	 * Parses a signed bit field if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param numberOfBits Number of bits to parse.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed signed bit field.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static Bits parseSBitsIf(final SWFBinaryParser parser, final int numberOfBits, final int errorCode, final boolean condition, final String fieldName) throws SWFParserException {
		return condition ? parseSBits(parser, numberOfBits, errorCode, fieldName) : null;
	}

	/**
	 * Parses an ASCII string of a given length.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param length The number of bytes to read.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed ASCII string.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static AsciiString parseString(final SWFBinaryParser parser, final int length, final int errorCode, final String fieldName) throws SWFParserException {
		return parser.readString(length);
	}

	/**
	 * Parses an ASCII string.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed ASCII string.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static AsciiString parseString(final SWFBinaryParser parser, final int errorCode, final String fieldName) throws SWFParserException {
		return parser.readString();
	}

	/**
	 * Parses an ASCII string if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed ASCII string.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static AsciiString parseStringIf(final SWFBinaryParser parser, final int errorCode, final boolean condition, final String fieldName) throws SWFParserException {
		return condition ? parseString(parser, errorCode, fieldName) : null;
	}

	/**
	 * Parses an ASCII string if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed ASCII string.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static AsciiString parseStringIf(final SWFBinaryParser parser, final int errorCode, final Flag condition, final String fieldName) throws SWFParserException {
		return condition.value() ? parseString(parser, errorCode, fieldName) : null;
	}

	/**
	 * Parses an unsigned bit field.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param numberOfBits Number of bits to parse.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed unsigned bit field.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static UBits parseUBits(final SWFBinaryParser parser, final int numberOfBits, final int errorCode, final String fieldName) throws SWFParserException {
		throwIfB(parser, numberOfBits, errorCode, fieldName);

		return parser.readBits(numberOfBits);
	}

	/**
	 * Parses an unsigned bit field if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param numberOfBits Number of bits to parse.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed unsigned bit field.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static UBits parseUBitsIf(final SWFBinaryParser parser, final int numberOfBits, final int errorCode, final boolean condition, final String fieldName) throws SWFParserException {
		return condition ? parseUBits(parser, numberOfBits, errorCode, fieldName) : null;
	}

	/**
	 * Parses an unsigned bit field if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param numberOfBits Number of bits to parse.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed unsigned bit field.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static UBits parseUBitsIf(final SWFBinaryParser parser, final int numberOfBits, final int errorCode, final Flag condition, final String fieldName) throws SWFParserException {
		return condition.value() ? parseUBits(parser, numberOfBits, errorCode, fieldName) : null;
	}

	/**
	 * Parses a 16 bit unsigned integer.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 16 bit unsigned integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static UINT16 parseUINT16(final SWFBinaryParser parser, final int errorCode, final String fieldName) throws SWFParserException {
		throwIf(parser, UINT16.BYTE_LENGTH, errorCode, fieldName);
		return parser.readUInt16();
	}

	/**
	 * Parses a 16 bit unsigned integer if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 16 bit unsigned integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static UINT16 parseUINT16If(final SWFBinaryParser parser, final int errorCode, final boolean condition, final String fieldName) throws SWFParserException {
		return condition ? parseUINT16(parser, errorCode, fieldName) : null;
	}

	/**
	 * Parses a 16 bit unsigned integer if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 16 bit unsigned integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static UINT16 parseUINT16If(final SWFBinaryParser parser, final int errorCode, final Flag condition, final String fieldName) throws SWFParserException {
		return condition.value() ? parseUINT16(parser, errorCode, fieldName) : null;
	}

	/**
	 * Parses a 32 bit unsigned integer.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 32 bit unsigned integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static UINT32 parseUINT32(final SWFBinaryParser parser, final int errorCode, final String fieldName) throws SWFParserException {
		throwIf(parser, UINT32.BYTE_LENGTH, errorCode, fieldName);
		return parser.readUInt32();
	}

	/**
	 * Parses a 32 bit unsigned integer if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 32 bit unsigned integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static UINT32 parseUINT32If(final SWFBinaryParser parser, final int errorCode, final Flag condition, final String fieldName) throws SWFParserException {
		return condition.value() ? parseUINT32(parser, errorCode, fieldName) : null;
	}

	/**
	 * Parses a 8 bit unsigned integer.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 8 bit unsigned integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static UINT8 parseUINT8(final SWFBinaryParser parser, final int errorCode, final String fieldName) throws SWFParserException {
		throwIf(parser, UINT8.BYTE_LENGTH, errorCode, fieldName);
		return parser.readUInt8();
	}

	/**
	 * Parses a 8 bit unsigned integer if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 8 bit unsigned integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static UINT8 parseUINT8If(final SWFBinaryParser parser, final int errorCode, final boolean condition, final String fieldName) throws SWFParserException {
		return condition ? parseUINT8(parser, errorCode, fieldName) : null;
	}

	/**
	 * Parses a 8 bit unsigned integer if a given condition is true.
	 * 
	 * @param parser The parser that provides the input stream.
	 * @param errorCode Error code in case parsing fails.
	 * @param condition The condition to be true for the integer to be parsed.
	 * @param fieldName Name of the field to parse.
	 * 
	 * @return The parsed 8 bit unsigned integer.
	 * 
	 * @throws SWFParserException Thrown if parsing failed.
	 */
	public static UINT8 parseUINT8If(final SWFBinaryParser parser, final int errorCode, final Flag condition, final String fieldName) throws SWFParserException {
		return condition.value() ? parseUINT8(parser, errorCode, fieldName) : null;
	}
}