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

import java.io.ByteArrayOutputStream;
import java.io.IOException;

import org.etsi.common.ByteHelper;

/**
 * This class provides TTCN-3 to network encoding context functionalities
 */
public class EncodingContext {

	/**
	 * String pattern for char to hexa conversion
	 */
	private static String _hexaString = "0123456789ABCDEF";
	
	/**
	 * The buffer to store the built network message
	 */
	private ByteArrayOutputStream _buffer;
	
	/**
	 * Current bit position in the encoding buffer
	 */
	private int _bitpos;
	
	/**
	 * Remaining bits for not byte aligned encoding
	 */
	private byte _theByte;
	
	/**
	 * Codec encoding identifier
	 */
	private String _valueEncoding;
	
	/**
	 * Mark to modify value later during encoding operations
	 * See Common header for GeoNetworking - plLength field
	 */
	private int _markForUpdate;
	
	/**
	 * Mark to store value of nextHeader field for checksum calculation
	 * See Ipv6Header - nextHeader field
	 */
	private int _markForNextHeader;
	
	/**
	 * Mark to store value of source IP address for checksum calculation
	 * See Ipv6Header - sourceAddress field
	 */
	private int _markForIPv6SrcAddress;
	
	/**
	 * Mark to store value of destination IP address for checksum calculation
	 * See Ipv6Header - destinationAddress field
	 */
	private int _markForIPv6DstAddress;
	
	/**
	 * Mark to modify value later during encoding operations
	 * See RouterAdvertisementMsg - checksum field
	 */
	private int _markForIPv6ChecksumCalculation;
	
	/**
	 * Accessor for <code>_valueEncoding</code> field
	 * @return The value of <code>_valueEncoding</code> field
	 */
	public String getValueEncoding() { return _valueEncoding; }
	
	/**
	 * Constructor
	 */
	public EncodingContext() {
		this(null);
	}
	
	/**
	 * Constructor
	 * @param valueEncoding The 'variant' value of the TTCN-3 message
	 */
	public EncodingContext(final String valueEncoding) {
		_valueEncoding = valueEncoding;
		_buffer = new ByteArrayOutputStream();
		_bitpos = 0;
		_markForUpdate = -1;
		_markForNextHeader = -1;
		_markForIPv6SrcAddress = -1;
		_markForIPv6DstAddress = -1;
		_markForIPv6ChecksumCalculation = -1;
	}
	
	/**
	 * Finalyses the encoding operation
	 * It shall be called to terminate encoding operation
	 */
	public void finalyze() {
		if ((_bitpos % 8) != 0) {
			_buffer.write(_theByte);
			_theByte = (byte) 0x00;
		}
	}
	
	/**
	 * Finalyses not byte aligned encoding
	 */
	public void finalizeBitstringValueEncoding() {
		if ((_bitpos % 8) != 0) {
			_buffer.write((byte)(_theByte >>> (Byte.SIZE - (_bitpos % Byte.SIZE))));
			_theByte = 0x00;
		}
	}
	
	/**
	 * Indicates is the current encoding operation is byte aligned
	 * @return true if the encoding operation is byte aligned, false otherwise
	 */
	public boolean isPositionAligned() {
		return _bitpos % 8 == 0;
	}
	
	/**
	 * Indicates the current size, in bytes, of the encoding buffer
	 * @return The current size, in bytes, of the encoding buffer
	 */
	public int size() {
		return _buffer.size();
	}
	
	/**
	 * @return Return the encoding buffer in <code>byte[]</code> format
	 */
	public byte[] toByteArray() {
		return _buffer.toByteArray();
	}
	
	/**
	 * Encodes a bit string value
	 * @param value The bit string value to encode
	 */
	public void setBitString(final String value) {
		// Sanity check
		if (value.isEmpty()) {
			return;
		}
		
		byte shiftByte = 0;
		for (int i = 1; i < value.length() - 2; i++) {
			if (value.charAt(i) == '1') {
				shiftByte = (byte) (1 << (7 - (_bitpos % 8)));
				_theByte |= shiftByte; // equals: theByte = theByte || shiftbyte
				shiftByte = 0;
			}
			_bitpos++;
			if ((_bitpos % 8) == 0) {
				_buffer.write(_theByte);
				_theByte = (byte)0x00;
			}
		}
	}
	
	/**
	 * Encodes a byte
	 * @param byteToEncode The byte to encode
	 * @throws IOException Signals that an I/O exception of some sort has occurred. This class is the general class of exceptions produced by failed or interrupted I/O operations
	 */
	public void setByte(final byte byteToEncode) throws IOException {
		if (_bitpos % 8 == 0) {
			_buffer.write(new byte[] { byteToEncode });
		} else {
			_buffer.write((byte)(_theByte >>> (8 - (_bitpos % 8))));
		}
		_bitpos += Byte.SIZE;
	}

	/**
	 * Encodes a byte array
	 * @param bytesToEncode The byte array to encode
	 * @throws IOException Signals that an I/O exception of some sort has occurred. This class is the general class of exceptions produced by failed or interrupted I/O operations
	 */
	public void setBytes(final byte[] bytesToEncode) throws IOException {
		// Sanity check
		if (bytesToEncode.length == 0) {
			return;
		}
		
		if (_bitpos % 8 == 0) {
			_buffer.write(bytesToEncode);
			_bitpos += bytesToEncode.length * Byte.SIZE;
		} else {
			throw new RuntimeException();
		}
	}

	/**
	 * Encodes an octet string ('CAFE'O -> 0xca 0xfe)
	 * @param value The octet string to encode
	 */
	public void setOctetString(final String value) {
		// Sanity check
		if (value.isEmpty()) {
			return;
		}
		
		byte shiftByte = 0;
		int lastBits = 0;
		for (int i = 1; i < value.length() - 2; i += 2) {
			byte theOct = (byte)((byte)(((byte)((byte)_hexaString.indexOf(value.charAt(i))) << 4) & 0xf0) | (byte)(((byte)_hexaString.indexOf(value.charAt(i + 1))) & 0x0f));
			if ((_bitpos % 8) > 0) {
				System.out.println("EncodingContext.setOctetString: The encoded octetstring is not byte aligned");
				lastBits = _bitpos % 8;
				shiftByte = (byte) (theOct >> lastBits);
				_theByte |= shiftByte;
				shiftByte = 0;
				_bitpos += (8 - lastBits);
				// The Byte should be filled
				if ((_bitpos % 8) == 0) {
					_buffer.write(_theByte);
					_theByte = 0x00;
				} else {
					throw new RuntimeException("EncodingContext.setOctetString: Bitposition is incorrect.");
				}
				// Shift the remaining bits in the next Byte
				for (int j = lastBits; j-- > 0;) {
					int lastBit = getBit(j - 1, (byte)theOct);
					if (lastBit == 1) {
						shiftByte |= (byte) (lastBit << (7 - (_bitpos % 8)));
						_theByte |= shiftByte;
						shiftByte = 0;
					}
					_bitpos++;
				}
			} else {
				_bitpos += 8;
				if ((_bitpos % 8) == 0) {
					_buffer.write(theOct);
				}
			}
		}
	}
	
	/**
	 * Encodes an hexadecimal string ('CAFFE'H -> 0xcaffe)
	 * @param value The hexadecimal string to encode
	 */
	public void setHexaString(final byte[] value) {
		// Sanity check
		if (value.length == 0) {
			return;
		}
		
		byte shiftByte = 0;
		int lastBits = 0;
		for (int i = 0; i < value.length; i++) {
			byte theHex = value[i];
		
			// hexstring is not byte aligned
			if ((_bitpos % 8) > 4) {
				System.out.println("EncodingContext.setHexaString: The encoded Hexstring is not byte aligned");
		
				lastBits = (_bitpos % 8) - 4;
				// Split the hexstring and fill up the byte
				shiftByte = (byte)(theHex >> lastBits);
				_theByte |= shiftByte;
				shiftByte = 0;
				_bitpos += (8 - (_bitpos % 8));
				// The Byte should be filled
				if ((_bitpos % 8) == 0) {
					_buffer.write(_theByte);
					_theByte = 0;
				} else {
					throw new RuntimeException("EncodingContext.setHexaString: Bitposition is incorrect.");
				}
				//shift the remaining bits in the next Byte
				for (int j = lastBits; j-- > 0;) {
					int lastBit = getBit(j - 1, (byte) theHex);
					if (lastBit == 1) {
						shiftByte |= (byte) (lastBit << (7 - (_bitpos % 8)));
						_theByte |= shiftByte;
						shiftByte = 0;
					}
					_bitpos++;
				}
			} else { // hexstring is byte aligned
				shiftByte = (byte) (theHex << (4 - (_bitpos % 8)));
				_theByte |= shiftByte;
				shiftByte = 0;
				_bitpos += 4;
				finalyze();
			}
		}
	}
	
	/**
	 * Return the current bit position
	 */
	@Override
	public String toString() {
		return String.format("EncodingContext: _bitpos:%d/%d - _bitpos/8:%d - _bitpos%%8:%d", _bitpos, _buffer.size() * 8, (int)(_bitpos / 8), (int)(_bitpos % 8));
	}
	
	
	/**
	 * 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;
		}
	}

	/**
	 * Set a mark to modify buffer values later during encoding operations
	 */
	public void markForUpdate() {
		_markForUpdate = _bitpos;
	}
	
	/**
	 * Set a mark to modify buffer values later during encoding operations
	 */
	public void markForNextHeader() {
		_markForNextHeader = _bitpos;
	}
	
	public byte[] getBytesAtMarkForNextHeader(int length) {
		if ((_markForNextHeader == -1) || (length <= 0)) {
			return null;
		}
		
		return ByteHelper.extract(_buffer.toByteArray(), _markForNextHeader / Byte.SIZE, length);
	}
	
	/**
	 * Set a mark to modify buffer values later during encoding operations
	 */
	public void markIPv6SrcAddress() {
		_markForIPv6SrcAddress = _bitpos;
	}
	
	public byte[] getBytesAtMarkForIPv6SrcAddress(int length) {
		if ((_markForIPv6SrcAddress == -1) || (length <= 0)) {
			return null;
		}
		
		return ByteHelper.extract(_buffer.toByteArray(), _markForIPv6SrcAddress / Byte.SIZE, length);
	}
	
	/**
	 * Set a mark to modify buffer values later during encoding operations
	 */
	public void markIPv6DstAddress() {
		_markForIPv6DstAddress = _bitpos;
	}
	
	public byte[] getBytesAtMarkForIPv6DstAddress(int length) {
		if ((_markForIPv6DstAddress == -1) || (length <= 0)) {
			return null;
		}
		
		return ByteHelper.extract(_buffer.toByteArray(), _markForIPv6DstAddress / Byte.SIZE, length);
	}
	
	/**
	 * Set a mark to modify buffer values later during encoding operations
	 */
	public void markForIPv6ChecksumCalculation() {
		_markForIPv6ChecksumCalculation = _bitpos;
	}
	
	public byte[] getBytesAtMarkForIPv6ChecksumCalculation(int length) {
		if ((_markForIPv6ChecksumCalculation == -1) || (length <= 0)) {
			return null;
		}
		
		return ByteHelper.extract(_buffer.toByteArray(), _markForIPv6ChecksumCalculation / Byte.SIZE, length);
	}
	
	public void setBytesAtMarkForIPv6ChecksumCalculation(byte[] value) throws IOException {
		// Sanity check
		if ((value == null) || (value.length == 0) || (_markForIPv6ChecksumCalculation == -1)) {
			return;
		}
		
		byte[] newBuffer = _buffer.toByteArray();
		int mark = _markForIPv6ChecksumCalculation / Byte.SIZE;
		for (int i = 0; i < value.length; i++) {
			newBuffer[mark++] = value[i];
		} // End of 'for statement'
		_buffer.reset();
		_buffer.write(newBuffer);
	}
	
	public byte[] getBytesAtMark(int length) {
		if ((_markForUpdate == -1) || (length <= 0)) {
			return null;
		}
		
		return ByteHelper.extract(_buffer.toByteArray(), _markForUpdate / Byte.SIZE, length);
	}
	
	/**
	 * Change buffer value at marked position
	 * @param value The new value to filled
	 * @throws IOException Signals that an I/O exception of some sort has occurred. This class is the general class of exceptions produced by failed or interrupted I/O operations
	 */
	public void setBytesAtMark(final byte[] value) throws IOException {
		// Sanity check
		if ((value == null) || (value.length == 0) || (_markForUpdate == -1)) {
			return;
		}

		byte[] newBuffer = _buffer.toByteArray();
		int mark = _markForUpdate / Byte.SIZE;
		for (int i = 0; i < value.length; i++) {
			newBuffer[mark++] = value[i];
		} // End of 'for statement'
		_buffer.reset();
		_buffer.write(newBuffer);
	}
	
} // End of class EncodingContext
