/*
 * Copyright 2002-2015 Drew Noakes
 *
 * More information about this project is available at:
 *
 *    https://drewnoakes.com/code/exif/
 *    https://github.com/drewnoakes/metadata-extractor
 */

package tim.prune.jpeg.drew;


/**
 * Provides methods to read specific values from a byte array,
 * with a consistent, checked exception structure for issues.
 *
 * @author Drew Noakes https://drewnoakes.com
 */
public class ByteArrayReader
{
	private final byte[] _buffer;
	private boolean _isMotorolaByteOrder = true;

	public ByteArrayReader(byte[] buffer)
	{
		if (buffer == null)
			throw new NullPointerException();

		_buffer = buffer;
	}

	public void setMotorolaByteOrder(boolean motorolaByteOrder)
	{
		_isMotorolaByteOrder = motorolaByteOrder;
	}

	public long getLength()
	{
		return _buffer.length;
	}

	protected byte getByte(int index)
	{
		return _buffer[index];
	}

	protected void validateIndex(int index, int bytesRequested) throws ExifException
	{
		if (!isValidIndex(index, bytesRequested))
			throw new ExifException("Invalid index " + index);
	}

	private boolean isValidIndex(int index, int bytesRequested)
	{
		return bytesRequested >= 0
			&& index >= 0
			&& ((long)index + (long)bytesRequested) <= (long)_buffer.length;
	}

	public byte[] getBytes(int index, int count) throws ExifException
	{
		validateIndex(index, count);

		byte[] bytes = new byte[count];
		System.arraycopy(_buffer, index, bytes, 0, count);
		return bytes;
	}

	/**
	 * Returns an unsigned 8-bit int calculated from one byte of data at the specified index.
	 *
	 * @param index position within the data buffer to read byte
	 * @return the 8 bit int value, between 0 and 255
	 * @throws ExifException the buffer does not contain enough bytes to service the request, or index is negative
	 */
	public short getUInt8(int index) throws ExifException
	{
		validateIndex(index, 1);

		return (short) (getByte(index) & 0xFF);
	}

	/**
	 * Returns a signed 8-bit int calculated from one byte of data at the specified index.
	 *
	 * @param index position within the data buffer to read byte
	 * @return the 8 bit int value, between 0x00 and 0xFF
	 * @throws ExifException the buffer does not contain enough bytes to service the request, or index is negative
	 */
	public byte getInt8(int index) throws ExifException
	{
		validateIndex(index, 1);

		return getByte(index);
	}

	/**
	 * Returns an unsigned 16-bit int calculated from two bytes of data at the specified index.
	 *
	 * @param index position within the data buffer to read first byte
	 * @return the 16 bit int value, between 0x0000 and 0xFFFF
	 * @throws ExifException the buffer does not contain enough bytes to service the request, or index is negative
	 */
	public int getUInt16(int index) throws ExifException
	{
		validateIndex(index, 2);

		if (_isMotorolaByteOrder) {
			// Motorola - MSB first
			return (getByte(index    ) << 8 & 0xFF00) |
				   (getByte(index + 1)      & 0xFF);
		} else {
			// Intel ordering - LSB first
			return (getByte(index + 1) << 8 & 0xFF00) |
				   (getByte(index    )      & 0xFF);
		}
	}

	/**
	 * Returns a signed 16-bit int calculated from two bytes of data at the specified index (MSB, LSB).
	 *
	 * @param index position within the data buffer to read first byte
	 * @return the 16 bit int value, between 0x0000 and 0xFFFF
	 * @throws ExifException the buffer does not contain enough bytes to service the request, or index is negative
	 */
	public short getInt16(int index) throws ExifException
	{
		validateIndex(index, 2);

		if (_isMotorolaByteOrder) {
			// Motorola - MSB first
			return (short) (((short)getByte(index    ) << 8 & (short)0xFF00) |
			                ((short)getByte(index + 1)      & (short)0xFF));
		} else {
			// Intel ordering - LSB first
			return (short) (((short)getByte(index + 1) << 8 & (short)0xFF00) |
			                ((short)getByte(index    )      & (short)0xFF));
		}
	}

	/**
	 * Get a 32-bit unsigned integer from the buffer, returning it as a long.
	 *
	 * @param index position within the data buffer to read first byte
	 * @return the unsigned 32-bit int value as a long, between 0x00000000 and 0xFFFFFFFF
	 * @throws ExifException the buffer does not contain enough bytes to service the request, or index is negative
	 */
	public long getUInt32(int index) throws ExifException
	{
		validateIndex(index, 4);

		if (_isMotorolaByteOrder) {
			// Motorola - MSB first (big endian)
			return (((long)getByte(index    )) << 24 & 0xFF000000L) |
				   (((long)getByte(index + 1)) << 16 & 0xFF0000L) |
				   (((long)getByte(index + 2)) << 8  & 0xFF00L) |
				   (((long)getByte(index + 3))       & 0xFFL);
		} else {
			// Intel ordering - LSB first (little endian)
			return (((long)getByte(index + 3)) << 24 & 0xFF000000L) |
				   (((long)getByte(index + 2)) << 16 & 0xFF0000L) |
				   (((long)getByte(index + 1)) << 8  & 0xFF00L) |
				   (((long)getByte(index    ))       & 0xFFL);
		}
	}

	/**
	 * Returns a signed 32-bit integer from four bytes of data at the specified index the buffer.
	 *
	 * @param index position within the data buffer to read first byte
	 * @return the signed 32 bit int value, between 0x00000000 and 0xFFFFFFFF
	 * @throws ExifException the buffer does not contain enough bytes to service the request, or index is negative
	 */
	public int getInt32(int index) throws ExifException
	{
		validateIndex(index, 4);

		if (_isMotorolaByteOrder) {
			// Motorola - MSB first (big endian)
			return (getByte(index    ) << 24 & 0xFF000000) |
				   (getByte(index + 1) << 16 & 0xFF0000) |
				   (getByte(index + 2) << 8  & 0xFF00) |
				   (getByte(index + 3)       & 0xFF);
		} else {
			// Intel ordering - LSB first (little endian)
			return (getByte(index + 3) << 24 & 0xFF000000) |
				   (getByte(index + 2) << 16 & 0xFF0000) |
				   (getByte(index + 1) << 8  & 0xFF00) |
				   (getByte(index    )       & 0xFF);
		}
	}

	/**
	 * Creates a String from the _data buffer starting at the specified index,
	 * and ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
	 *
	 * @param index          The index within the buffer at which to start reading the string.
	 * @param maxLengthBytes The maximum number of bytes to read.  If a zero-byte is not reached within this limit,
	 *                       reading will stop and the string will be truncated to this length.
	 * @return The read string.
	 * @throws ExifException The buffer does not contain enough bytes to satisfy this request.
	 */
	public String getNullTerminatedString(int index, int maxLengthBytes) throws ExifException
	{
		// NOTE currently only really suited to single-byte character strings

		byte[] bytes = getBytes(index, maxLengthBytes);

		// Count the number of non-null bytes
		int length = 0;
		while (length < bytes.length && bytes[length] != '\0')
			length++;

		return new String(bytes, 0, length);
	}

	public String getString(int index, int bytesRequested) throws ExifException
	{
		// TODO: validate index
		return new String(getBytes(index, bytesRequested));
	}
}
