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

import java.io.IOException;
import java.math.BigInteger;
import java.util.logging.Level;

import org.etsi.codec.DecodingContext;
import org.etsi.codec.EncodingContext;
import org.etsi.codec.TriMessageImpl;
import org.etsi.common.ByteHelper;
import org.etsi.its.codec.ItsCodecImpl;
import org.etsi.its.codec.TestAdapterMessageTypeEnum;
import org.etsi.ttcn.tci.IntegerValue;
import org.etsi.ttcn.tci.OctetstringValue;
import org.etsi.ttcn.tci.RecordValue;
import org.etsi.ttcn.tci.TciCDProvided;
import org.etsi.ttcn.tci.TciCDRequired;
import org.etsi.ttcn.tci.TciTypeClass;
import org.etsi.ttcn.tci.Type;
import org.etsi.ttcn.tci.UnionValue;
import org.etsi.ttcn.tci.Value;
import org.etsi.ttcn.tri.TriMessage;

/** 
 * This class provides the TTCN-3 codec implementation for LibItsCommon_TypesAndValues TTCN-3 module (ITS message primitives)
 * 
 * Note that it requires closely linked to LibItsGeoNetworking_TypesAndValues TTCN-3 codec
 * 
 * See 'ETSI TS 102 868-3 Clause 4.4.1 Primitives of the camPort' for message primitives on CAM port
 * See 'ETSI TS 102 870-3 Clause 4.4.1 Primitives of the btpPort' for message primitives on BTP port
 * See 'ETSI TS 102 869-3 Clause 4.4.1 Primitives of the denmPort' for message primitives on DENM port
 * See 'ETSI TS 102 871-3 Clause 4.4.1 Primitives of the geoNetworkingPort' for message primitives on geoNetworkingPort port
 * See 'ETSI TS 102 859-3 Clause 4.4.1 Primitives of the ipv6OverGeoNetworkingPort' for message primitives on ipv6OverGeoNetworkingPort port
 * 
 * Refer to ETSI ES 201 873-6 for TCI implementations
 * 
 * @see org.etsi.its.codec.ttcn.LibItsGeoNetworking_TypesAndValues.LibItsGeoNetworking_TypesAndValuesCodec
 * @see org.etsi.its.codec.ttcn.LibItsBtp_TypesAndValues.LibItsBtp_TypesAndValuesCodec
 * 
 * @category Codec
 */
public class LibIts_InterfaceCodec extends ItsCodecImpl {

	/**
	 * Constructor
	 * @param typeServer Reference to the tool specific codec implementation
	 * 
	 */
	public LibIts_InterfaceCodec(TciCDRequired typeServer) {
		super(typeServer);

		_logger.entering("LibIts_InterfaceCodec", "Constructor", String.format("version:%s", Version));
	} // End of Constructor

	/** 
	 * Returns an encoded TriMessage according to the encoding rules.
	 * This operation shall be called whenever the TE has to encode a Value
	 * 
	 * @param template The value to be encoded
	 * @return Returns an encoded TriMessage for the specified encoding rule
	 * 
	 * See ETSI ES 201 873-6 V4.2.1 - 7.3.2.2.2 encode
	 */
	@Override
	public TriMessage encode(final Value template) {
		_logger.entering("LibIts_InterfaceCodec", "encode", template.toString());

		// Set the _bitpos to 0 (this is needed to know the actual bit position from the byte array)
		try {
			TriMessage encoded = super.encode(template);
			if (_logger.isLoggable(Level.ALL)) ByteHelper.dump("LibIts_InterfaceCodec.encode: ", encoded.getEncodedMessage());
			return encoded;
		} catch (Exception e) {
			e.printStackTrace();
		}

		return null;
	} // End of method encode

	/** 
	 * This operation decodes message according to the encoding rules and returns a TTCN-3 value. 
	 * The decodingHypothesis shall be used to determine whether the encoded value can be decoded. 
	 * If an encoding rule is not self-sufficient, i.e. if the encoded message does not inherently 
	 * contain its type decodingHypothesis shall be used. If the encoded value can be decoded without 
	 * the decoding hypothesis, the distinct null value shall be returned if the type determined from 
	 * the encoded message is not compatible with the decoding hypothesis
	 * 
	 * @param message The encoded message to be decoded
	 * @param decodingHypothesis The hypothesis the decoding can be based on
	 * @return Returns the decoded value, if the value is of a compatible type as the decodingHypothesis, else the distinct value null
	 * 
	 * See ETSI ES 201 873-6 V4.2.1 - 7.3.2.2.1 decode
	 */
	@Override
	public Value decode(final TriMessage message, final Type decodingHypothesis) {
		_logger.entering("LibIts_InterfaceCodec", "decode", decodingHypothesis.getName());

		try {
			// Initialise the decodingContext
			byte[] encodedMsg = message.getEncodedMessage();
			if (encodedMsg.length == 0) {
				_logger.severe("encodedMsg is empty");
				return null;
			}
			if (_logger.isLoggable(Level.ALL)) ByteHelper.dump("LibIts_InterfaceCodec.decode: ", encodedMsg);
			DecodingContext decodingContext = new DecodingContext(encodedMsg, decodingHypothesis.getTypeEncoding());

			// Check which type class the decoding hypothesis is of
			Value value = decodeTLVLayer(decodingHypothesis, decodingContext);
			_logger.exiting("LibIts_InterfaceCodec", "decode", value.toString());
			return value;
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		
		return null;
	} // End of method decode

	/** 
	 * This method decodes the current TLV sequence
	 * @param decodingHypothesis Decoding hypothesis
	 * @param decodingContext The complete encoded message
	 * @return The decoded value contained in the TLV sequence
	 * @throws InstantiationException Thrown when an application tries to create an instance of a class using the newInstance method in class Class, but the specified class object cannot be instantiated
	 * @throws IllegalAccessException An IllegalAccessException is thrown when an application tries to reflectively create an instance (other than an array), set or get a field, or invoke a method, but the currently executing method does not have access to the definition of the specified class, field, method or construct
	 */
	public Value decodeTLVLayer(final Type decodingHypothesis, DecodingContext decodingContext) throws InstantiationException, IllegalAccessException {
		_logger.entering("LibIts_InterfaceCodec", "decodeTLVLayer", decodingHypothesis.getName());

		//Check which type class the decoding hypothesis is of
		Value value = null;
		switch (decodingHypothesis.getTypeClass()) {
			case TciTypeClass.RECORD:
				// Create a new instance
				final RecordValue recordValue = (RecordValue)decodingHypothesis.newInstance();
				// And fill it
				value = decodeTLVLayer(recordValue, decodingContext);
				break;
			default:
				throw new RuntimeException(String.format("Not implemented type class %s - %s", decodingHypothesis.getName(), decodingHypothesis.getTypeClass()));
		} // End of 'switch' statement

		return value;
	}

	/** 
	 * This method decodes the current TLV sequence, based on RecordValue
	 * @param recordValue A record value to decode
	 * @param encodedMsg The complete encoded message
	 * @return The decoded value contained in the TLV sequence
	 * @throws InstantiationException Thrown when an application tries to create an instance of a class using the newInstance method in class Class, but the specified class object cannot be instantiated
	 * @throws IllegalAccessException An IllegalAccessException is thrown when an application tries to reflectively create an instance (other than an array), set or get a field, or invoke a method, but the currently executing method does not have access to the definition of the specified class, field, method or construct
	 * @see RecordValue
	 */
	private RecordValue decodeTLVLayer(RecordValue recordValue, DecodingContext decodingContext) throws InstantiationException, IllegalAccessException {
		_logger.entering("LibIts_InterfaceCodec", "decodeTLVLayer (1)", recordValue.getType().getName());

		// Extract type
		TestAdapterMessageTypeEnum type = TestAdapterMessageTypeEnum.valueOf(decodingContext.getNextByte()); // Get type
		if ((type == null) || (type.name() != recordValue.getType().getName())) {
			_logger.severe(String.format("(1): Type mismatch: %s - %s", (type == null) ? "(null)" : type.name(), recordValue.getType().getName()));
			return null;
		}
		_logger.info(String.format("(1): type:%s", type.name()));
		short length = ByteHelper.byteArrayToShort(decodingContext.getNextBytes(2)); // Get Length (2 bytes)
		Value recordField = recordValue.getField(recordValue.getFieldNames()[0]);
		_logger.info(String.format("(1): recordField:%s - %s", recordField.getType().getName(), recordField.getValueEncoding()));
		// Retrieve the correct codec
		TciCDProvided codec;
		if ((codec = getCodec(recordField.getValueEncoding())) == null) {
			throw new RuntimeException(String.format("Could not retrieve codec for decoding name %s", recordField.getValueEncoding()));
		}
		switch (type) {
			case CamReq:
				// No break;
			case CamInd:
				// No break;
			case DenmReq:
				// No break;
			case DenmInd:
				// No break;
			case BtpReq:
				// No break;
			case BtpInd: {
					TriMessageImpl newMessage = new TriMessageImpl(decodingContext.getNextBytes(length)); // Get Value ('length' bytes)
					// Decoding operations
					Value decodedValue = null;
					try {
					    decodedValue = codec.decode(newMessage, recordField.getType());
                    } catch (Throwable e) {
                        throw new RuntimeException(String.format("Exception occured while decoding the message in using the codec %s", codec.getClass().getName()));
                    }
					if (decodedValue==null) {
					    throw new RuntimeException(String.format("Could not decode the message in using the codec %s", codec.getClass().getName()));
					}
                    recordValue.setField(recordValue.getFieldNames()[0], decodedValue);
					if (type == TestAdapterMessageTypeEnum.DenmInd) {
						// Extract time stamp
						type = TestAdapterMessageTypeEnum.valueOf(decodingContext.getNextByte()); // Get type
						if (type != TestAdapterMessageTypeEnum.receptionTime) {
							throw new RuntimeException("'receptionTime' type identifier expected");
						}
						length = ByteHelper.byteArrayToShort(decodingContext.getNextBytes(2)); // Get Length (2 bytes)
						IntegerValue time = (IntegerValue) recordValue.getField(recordValue.getFieldNames()[1]).getType().newInstance();
						time.setBigInt(new BigInteger(decodingContext.getNextBytes(length))); // Get Value ('length' bytes)
						recordValue.setField(recordValue.getFieldNames()[1], (Value)time);
					}
				}
				break;
			case GeoNetworkingReq:
				// No break;
			case GeoNetworkingInd: {
					TriMessageImpl newMessage = new TriMessageImpl(decodingContext.getNextBytes(length - 6/*destMacAddress length*/));
					// Decoding operations
                    Value decodedValue = null;
                    try {
                        decodedValue = codec.decode(newMessage, recordField.getType());
                    } catch (Throwable e) {
                        throw new RuntimeException(String.format("Exception occured while decoding the message in using the codec %s", codec.getClass().getName()));
                    }
                    if (decodedValue==null) {
                        throw new RuntimeException(String.format("Could not decode the message in using the codec %s", codec.getClass().getName()));
                    }
                    recordValue.setField(recordValue.getFieldNames()[0], decodedValue);
					// Extract destination MAC address
					OctetstringValue destMacAddress = (OctetstringValue) recordValue.getField(recordValue.getFieldNames()[1]).getType().newInstance();
					destMacAddress.setLength(6);
					for (int position = 0; position < 6; position++) {
						destMacAddress.setOctet(position, decodingContext.getNextByte());
					}
					//destMacAddress.setString("\'" + ByteHelper.byteArrayToString(decodingContext.getNextBytes(6)) + "'O");
					recordValue.setField(recordValue.getFieldNames()[1], destMacAddress);
				}
				break;
			case IPv6OverGeoNetworkingReq:
				// No break;
			case IPv6OverGeoNetworkingInd: {
					TriMessageImpl newMessage = new TriMessageImpl(decodingContext.getNextBytes(length)); // Get Value ('length' bytes)
					// Decoding operations
					recordValue = decodeIPv6OverGeoNetworkingReqInd(recordValue, new DecodingContext(newMessage.getEncodedMessage()));
				}
				break;
			default:
				throw new RuntimeException(String.format("(1): Unimplemented record %s", type));
		} // End of 'switch' statement

		return recordValue;
	} // End of method decodeTLVLayer

	/**
	 * This method decodes an GeoNetworking over IPv6 request/indicator message
	 * @param rv The <code>recordValue</code> type to decode
	 * @param encodedMsg The complete encoded message
	 * @return An initialised <code>recordValue</code> instance
	 */
	private RecordValue decodeIPv6OverGeoNetworkingReqInd(final RecordValue rv, DecodingContext decodingContext) {
		_logger.entering("LibIts_InterfaceCodec", "decodeIPv6OverGeoNetworkingReqInd", String.format("%s - %s", rv.getType().getName(), rv.getValueEncoding()));
		
		String[] fields = rv.getFieldNames();
		int idx = 0;
		// 1. Decode 'charstring interface' field
		rv.setField(fields[idx], decodeCharstringValue(rv.getField(fields[idx++]).getType(), decodingContext));
		// 2. Decode 'MacAddress p_dstMacAddr' and 'MacAddress p_srcMacAddr' fields
		idx++;
		rv.setField(fields[idx], decodeOctetstringValue(rv.getField(fields[idx++]).getType(), decodingContext));
		idx-=2;
		rv.setField(fields[idx], decodeOctetstringValue(rv.getField(fields[idx++]).getType(), decodingContext));
		idx++;
		// 3. Retrieve codec for LibItsIpv6OverGeoNetworking_TypesAndValues
		TciCDProvided codec = getCodec("LibItsIpv6OverGeoNetworking_TypesAndValues");
		if (codec == null) {
			throw new RuntimeException(String.format("Could not retrieve codec for decoding name %s", rv.getValueEncoding()));
		}
		// 4. Decode 'Ipv6Packet p_ipv6Packet' field
		decodingContext.getNextBytes(2); // skip packet's length
		TriMessageImpl newMessage = new TriMessageImpl(decodingContext.getRemaindingBytes());
        Value decodedValue = null;
        try {
            decodedValue = codec.decode(newMessage, rv.getField(fields[idx]).getType());
        } catch (Throwable e) {
            throw new RuntimeException(String.format("Exception occured while decoding the message in using the codec %s", codec.getClass().getName()));
        }
        if (decodedValue==null) {
            throw new RuntimeException(String.format("Could not decode the message in using the codec %s", codec.getClass().getName()));
        }
        rv.setField(fields[idx++], decodedValue);

		return rv;
	} // End of method decodeIPv6OverGeoNetworkingReqInd

	/**
	 * This method checks the present subfield-type to call the representing method
	 * 
	 * @param encodingContext The context to encode the TTCN-3 message.
	 * @param rv The <code>RecordValue</code> from getting subfields.
	 */
	@Override
	public void encodeRecordValue(EncodingContext encodingContext, final RecordValue rv) {
		_logger.entering("LibIts_InterfaceCodec", "encodeRecordValue", String.format("%s - %s", rv.getType().getName(), rv.getValueEncoding()));

		if ((rv.getValueEncoding() != null) && (rv.getValueEncoding() != encodingContext.getValueEncoding())) {
			// Retrieve the correct codec
			TciCDProvided codec = getCodec(rv.getValueEncoding());
			if (codec == null) {
				throw new RuntimeException(String.format("Could not retrieve codec for encoding name %s", rv.getValueEncoding()));
			}
			// And continue the encoding operations
			TriMessage value = codec.encode(rv);
			try {
				encodingContext.setBytes(value.getEncodedMessage());
			} catch (IOException e) {
				e.printStackTrace();
			}
		} else {
			try {
				encodingContext.setByte(TestAdapterMessageTypeEnum.convert(rv.getType().getName()));
			} catch (IOException e) {
				e.printStackTrace();
			} // Type
			if (rv.notPresent()) {
				try { // Length is 0
					encodingContext.setByte((byte) 0x00);
					encodingContext.setByte((byte) 0x00);
				} catch (IOException e) {
					e.printStackTrace();
				}
			} else {
				EncodingContext out_rv = new EncodingContext();
				super.encodeRecordValue(out_rv, rv);
				try { // Length
					encodingContext.setBytes((byte[])ByteHelper.intToByteArray(out_rv.size(), LengthFieldSize));
				} catch (IOException e) {
					e.printStackTrace();
				}
				if (out_rv.size() != 0) { // Value
					try {
						encodingContext.setBytes(out_rv.toByteArray());
					} catch (IOException e1) {
						e1.printStackTrace();
					}
				}
			}
		}

//		if (_logger.isLoggable(Level.ALL)) ByteHelper.dump("LibIts_InterfaceCodec.encodeRecordValue: ", encodingContext.toByteArray());
	} // End of method encodeRecordValue

	/**
	 * This method checks the present subfield-type to call the representing method
	 * 
	 * @param encodingContext The context to encode the TTCN-3 message.
	 * @param uv The <code>UnionValue</code> from getting subfields.
	 */
	@Override
	public void encodeUnionValue(EncodingContext encodingContext, final UnionValue uv) {
		_logger.entering("LibIts_InterfaceCodec", "encodeUnionValue", String.format("%s - %s", uv.getType().getName(), uv.getValueEncoding()));

		if ((uv.getValueEncoding() != null) && (uv.getValueEncoding() != encodingContext.getValueEncoding())) {
			// Retrieve the correct codec
			TciCDProvided codec = getCodec(uv.getValueEncoding());
			if (codec == null) {
				throw new RuntimeException(String.format("Could not retrieve codec for encoding name %s", uv.getValueEncoding()));
			}
			// And continue encoding processing
			TriMessage value = codec.encode(uv);
			try {
				encodingContext.setBytes(value.getEncodedMessage());
			} catch (IOException e) {
				e.printStackTrace();
			}
		} else {
			try {
				encodingContext.setByte(TestAdapterMessageTypeEnum.convert(uv.getType().getName()));
			} catch (IOException e) {
				e.printStackTrace();
			} // Type
			if (uv.notPresent()) {
				try { // Length is 0
					encodingContext.setByte((byte) 0x00);
					encodingContext.setByte((byte) 0x00);
				} catch (IOException e1) {
					e1.printStackTrace();
				}
			} else {
				EncodingContext out_uv = new EncodingContext();
				super.encodeUnionValue(out_uv, uv);
				try {
					encodingContext.setBytes((byte[])ByteHelper.intToByteArray(out_uv.size(), LengthFieldSize));
				} catch (IOException e) {
					e.printStackTrace();
				} // Length
				if (out_uv.size() != 0) { // Value
					try {
						encodingContext.setBytes(out_uv.toByteArray());
					} catch (IOException e1) {
						e1.printStackTrace();
					}
				}
			}
		}

//		if (_logger.isLoggable(Level.ALL)) ByteHelper.dump("LibIts_InterfaceCodec.encodeUnionValue: ", encodingContext.toByteArray());
	} // End of method encodeUnionValue
	
} // End of class LibIts_InterfaceCodec

