/**
 * @author	STF 424_ITS_Test_Platform
 * @version    $URL$
 *             $Id$
 */
package org.etsi.codec;

import org.etsi.common.ByteHelper;
import org.etsi.ttcn.tci.IntegerValue;
import org.etsi.ttcn.tci.RecordValue;

/**
 * This class provides Network to TTCN-3 decoding context functionalities
 */
public class DecodingContext {

	/**
	 * The buffer to be decoded
	 */
	private byte[] _buffer;
	
	/**
	 * Current bit position in the decoding buffer
	 */
	private int _bitpos;
	
	/**
	 * Codec encoding identifier
	 */
	private String _valueEncoding;

	/** 
	 * Contextual decoding value
	 * 
	 * See ETSI TS 102 636-4-1 - Table 4: Fields of the Common Header
	 */
	private RecordValue _commonHeader;

	/**
	 * Used to store the payload length during decoding
	 * 
	 * See ETSI TS 102 636-6-1 V1.1.1 - Clause 8.3 Frame format
	 */
	private IntegerValue _payloadLength;

	/**
	 * Used to store the 'next header' type during decoding
	 * 
	 * See ETSI TS 102 636-6-1 V1.1.1 - Clause 8.3 Frame format
	 */
	private IntegerValue _nextHeader;

	/**
	 * Constructor
	 * @param messageToDecode The network message to decode into TTCN-3
	 */
	public DecodingContext(final byte[] messageToDecode) {
		this(messageToDecode, null);
	}

	/**
	 * Constructor
	 * @param messageToDecode The network message to decode
	 * @param valueEncoding The value encoding
	 */
	public DecodingContext(final byte[] messageToDecode, final String valueEncoding) {
		_buffer = messageToDecode.clone();
		_bitpos = 0;
		_valueEncoding = valueEncoding;
		set_commonHeader(null);
		set_payloadLength(null);
		set_nextHeader(null);
	}

	/**
	 * Indicates is the current decoding operation is byte aligned
	 * @return true if decoding operation is byte aligned, false otherwise
	 */
	public boolean isPositionAligned() {
		return _bitpos % 8 == 0;
	}
	
	/**
	 * Indicates is the end of the decoding buffer is reached
	 * @return true if the end of the decoding buffer is reached, false otherwise
	 */
	public boolean isEndOfBufferReached() {
		return (_bitpos / 8) >= _buffer.length;
	}
	
	/**
	 * Indicates is the number of bits to be decoded
	 * @return true The number of bits to be decoded
	 */
	public int getCountOfRemaindingBits() {
		return _buffer.length * Byte.SIZE - _bitpos;
	}
	
	/**
	 * Extract <code>decodingLength</code> bits from the decoding buffer and incrementing the bit position index
	 * @param decodingLength The number of bits to decode
	 * @return The buffer containing the decoded bits, one item per bit
	 */
	public byte[] getBits(int decodingLength) {
		// Sanity check
		if (_buffer.length <= (int)(_bitpos / 8)) {
			return null;
		}

		byte[] bitArray = new byte[decodingLength];
		for (int i = 0; i < decodingLength; i++) {
			bitArray[i] = getBit((7 - ((_bitpos + i) % Byte.SIZE)), _buffer[(int)((_bitpos + i) >> 3)]);
		}
		_bitpos += decodingLength;
		
		return bitArray;
	}
	
	/**
	 * Extract one byte from the decoding buffer WITHOUT incrementing the bit position index
	 * @return The byte value
	 */
	public byte peekNextByte() {
		// Sanity check
		if (_buffer.length <= (int)(_bitpos / 8)) {
			return 0x00;
		}

		if (!((_bitpos % 8) == 0)) {
			throw new RuntimeException("DecodingContext.getNextByte: Trying to decoded a byte value not byte aligned or with a non integral number of bytes");
		}
		byte value = _buffer[(int)(_bitpos / Byte.SIZE)];
		
		return value;
	}

	/**
	 * Extract one byte from the decoding buffer and increment the bit position index
	 * @return The byte value on success, 0xff otherwise
	 */
	public byte getNextByte() {
		// Sanity check
		if (_buffer.length <= (int)(_bitpos / 8)) {
			return (byte) 0xff;
		}

		if (!((_bitpos % 8) == 0)) {
			throw new RuntimeException("DecodingContext.getNextByte: Trying to decoded a byte value not byte aligned or with a non integral number of bytes");
		}
		byte value = _buffer[(int)(_bitpos / Byte.SIZE)];
		_bitpos += Byte.SIZE;
		
		return value;
	}

	/**
	 * Retrieve next <code>countof</code> bytes from the decoding buffer and increment the bit position index
	 * @param countof Number of bytes to extract
	 * @return The extracted bytes on success, null otherwise
	 */
	public byte[] getNextBytes(final int countof) {
		// Sanity check
		if (_buffer.length <= (int)(_bitpos / 8)) {
			return null;
		}

		if (!((_bitpos % 8) == 0)) {
			throw new RuntimeException("DecodingContext.getNextBytes: Trying to decoded a byte value not byte aligned or with a non integral number of bytes");
		}
		byte[] value = ByteHelper.extract(_buffer, _bitpos / Byte.SIZE, countof);
		_bitpos += countof * Byte.SIZE;
		
		return value;
	}
	
	/**
	 * Extract all bytes from current <code>_bitpos</code> position
	 * @return The extracted bytes on success, null otherwise
	 */
	public byte[] getRemaindingBytes() {
		// Sanity check
		if (_buffer.length <= (int)(_bitpos / 8)) {
			return null;
		}

		byte[] value = ByteHelper.extract(_buffer, _bitpos / Byte.SIZE, _buffer.length - _bitpos / Byte.SIZE);
		_bitpos = Integer.MAX_VALUE;
		
		return value;
	}
	
	/**
	 * Extract <code>countof</code> bytes from the decoding buffer and increment the bit position index
	 * @param countof Number of bytes to extract
	 * @return The extracted bytes on success, null otherwise
	 */
	public byte[] extractBytes(final int countof) {
		// Sanity check
		if (_buffer.length <= (int)(_bitpos / 8)) {
			return null;
		}

		byte[] value = ByteHelper.extract(_buffer, _bitpos / Byte.SIZE, countof);
		_bitpos += countof * Byte.SIZE;
		
		return value;
	}
	
	/**
	 * Extract <code>countof</code> bytes from the decoding buffer and increment the bit position index
	 * @param countof An integer amount bits to decode
	 * @return The extracted bytes on success, null otherwise
	 */
	public byte[] getHexaString(final int countof) {
		// Sanity check
		if (_buffer.length <= (int)(_bitpos / 8)) {
			return null;
		}

		int decodingLength = countof;
		if ((_buffer.length << 1) < ((_bitpos >> 2) + (decodingLength >> 2))) {
			System.out.println("DecodingContext.getHexaString: Trying to decode past the end of received message.\nAbstractBaseCodec: Will decode until end of message, then aborting.\nAbstractBaseCodec: Being at bit position " + _bitpos + " trying to decode additional " + decodingLength + " bits for a hex digit.");
			decodingLength = (_buffer.length - (_bitpos >> 2)) << 2;
		}
		if (((_bitpos % 8) == 0) && ((decodingLength % 8) == 0)) {
			// Byte aligned and integral number of octets, just dump them out
			return getNextBytes(decodingLength >> 3);
		} else if (((_bitpos % 4) == 0)) {
			int nibblesToDecode = decodingLength >> 2;
			byte[] hexstring = new byte[nibblesToDecode];
			for (int i = 0; i < nibblesToDecode; i++) { 
				// The evaluation byte
				byte theEncodedByte = _buffer[_bitpos >> 3];
				// Check if the hex value is the first nibble from the byte
				if ((_bitpos % 8) == 0) {	
					hexstring[i] = (byte) ((theEncodedByte >> 4) & 0x0f);
				} else if ((_bitpos % 4) == 0) { // Check if the hex value is the second nibble nibble from the byte
					hexstring[i] = (byte) (theEncodedByte & 0x0f);
				}
				_bitpos += 4;
			}
			
			return hexstring;
		} else {
			throw new RuntimeException("DecodingContext.getHexaString: Can not decode hexstringstring at not nibble aligned position: " + _bitpos);
		}
	}
	
	/**
	 * Extract <code>countof</code> bytes from the decoding buffer and increment the bit position index
	 * @param countof An integer amount bits to decode
	 * @return The extracted bytes on success, null otherwise
	 */
	public byte[] getOctetString(final int countof) {
		// Sanity check
		if (_buffer.length <= (int)(_bitpos / 8)) {
			return null;
		}

		int decodingLength = countof;
		if (_buffer.length < ((_bitpos >> 3) + (decodingLength >> 3))) {
			decodingLength = (_buffer.length - (_bitpos >> 3)) << 3;
		}
		if (((_bitpos % 8) == 0) && ((decodingLength % 8) == 0)) {
			// Byte aligned and integral number of octets, just dump them out
			return getNextBytes(decodingLength >> 3);
		} else if (((_bitpos % 8) == 0) && ((decodingLength % 8) != 0)) {
			// Byte aligned but not integral number of octets, so we have to take care of the last octet
			// assumption: everything is right aligned
			final int blocks = decodingLength >> 3; // number of full blocks
			final int remainder = decodingLength & 7; // that is left
			final byte[] newMsg = new byte[blocks + 1];
			int octetpos = _bitpos >> 3;
			System.arraycopy(_buffer, octetpos, newMsg, 0, blocks);
			octetpos += blocks;
			newMsg[blocks] = (byte) (_buffer[octetpos] & (~((1 << (8 - remainder)) - 1))); // FIXME To be tested
			_bitpos += decodingLength;
			
			return newMsg;
		} else {
			throw new RuntimeException("DecodingContext.getOctetString: Can not decode octetstring at not bytealigned position: " + _bitpos);
		}
	}
	
	/**
	 * Return the current bit position
	 */
	@Override
	public String toString() {
		return String.format("DecodingContext: _bitpos:%d/%d - _bitpos/8:%d - _bitpos%%8:%d", _bitpos, _buffer.length * 8, (int)(_bitpos / 8), (int)(_bitpos % 8));
	}
	
	/**
	 * Accessor for <code>_valueEncoding</code> field
	 * @return The value of <code>_valueEncoding</code> field
	 */
	public String getValueEncoding() { return _valueEncoding; }
	
	/**
	 * @param _commonHeader the _commonHeader to set
	 */
	public void set_commonHeader(RecordValue _commonHeader) {
		this._commonHeader = _commonHeader;
	}

	/**
	 * @return the _commonHeader
	 */
	public RecordValue get_commonHeader() {
		return _commonHeader;
	}

	/**
	 * @param _payloadLength the _payloadLength to set
	 */
	public void set_payloadLength(IntegerValue _payloadLength) {
		this._payloadLength = _payloadLength;
	}

	/**
	 * @return the _payloadLength
	 */
	public IntegerValue get_payloadLength() {
		return _payloadLength;
	}

	/**
	 * @param _nextHeader the _nextHeader to set
	 */
	public void set_nextHeader(IntegerValue _nextHeader) {
		this._nextHeader = _nextHeader;
	}

	/**
	 * @return the _nextHeader
	 */
	public IntegerValue get_nextHeader() {
		return _nextHeader;
	}

	/**
	 * Return the bit at the position 'pos' from the byte 'b'.
	 *
	 * @param pos The position <code>int</code> of the bit to be return.
	 * @param theByte The <code>byte</code> where getting the bit.
	 *
	 * @return <code>int</code> that represent the bit (1 or 0)
	 */
	private byte getBit(int pos, byte theByte) {
		if ((((byte) 1) & (theByte >> pos)) == 1) {
			return 0x01;
		} else {
			return 0x00;
		}
	}

} // End of class DecodingContext
