package org.acplt.oncrpc;

import java.io.IOException;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class XdrTcpChannelEncodingStream extends XdrEncodingStream {

	public XdrTcpChannelEncodingStream(SocketChannel tcpChannel, int bufferSize) {
		this.tcpChannel = tcpChannel;
		
		//
        // If the given buffer size is too small, start with a more sensible
        // size. Next, if bufferSize is not a multiple of four, round it up to
        // the next multiple of four.
        //
        if ( bufferSize < BUFFER_SIZE_MIN ) {
            bufferSize = 1024;
        } else if ( (bufferSize & ALIGNMENT_MOD) != 0 ) {
            bufferSize = (bufferSize + ALIGNMENT) & ~ALIGNMENT_MOD;
        }
		
        /*
         * Set up the buffer, which will hold the
         * encoded data. The byte buffer is set
         * to 'big endian' by default.
         */
        byteBuffer = ByteBuffer.allocate(bufferSize);
	}
	
	@Override
	public void beginEncoding(InetAddress receiverAddress, int receiverPort) throws OncRpcException, IOException {
		/*
		 * With TCP-Channels batched calls are supported. Batched calls mean sending
		 * multiple messages, where each message gets its own set of fragment headers.
		 * Therefore fragment headers may be located anywhere in the buffer.
		 * The location of the actual fragment header corresponding to the
		 * next message to encode, needs to be tracked, so that the fragment header
		 * can be completed after the message or a part of it has been encoded.
		 * 
		 * The actual position of the buffer will be marked and advanced
		 * by the length of the fragment header. 
		 */
		byteBuffer
			.mark()
			.position(byteBuffer.position() + FRAGMENT_HEADER_LENGTH);
	}
	
	@Override
	public void endEncoding() throws OncRpcException, IOException {
		/*
		 * The actual buffer position is reminded as last position.
		 */
		int lastPosition = byteBuffer.position();
		
		/*
		 * The byte buffer is reset to the position
		 * of the actual fragment header, which is marked in the
		 * byte buffer.
		 */
		byteBuffer.reset();
		
		/*
		 * The fragment header is completed:
		 * - It takes the length of the fragment (last buffer
		 *   position minus actual buffer position minus fragment
		 *   header length (4)).
		 * - As the encoding ends with this step, the fragment
		 *   is marked as last fragment.
		 */
		byteBuffer.putInt((lastPosition - byteBuffer.position() - FRAGMENT_HEADER_LENGTH) | 0x80000000);
		
		/*
		 * The content of the buffer is flushed.
		 */
		flush();
	}
	
	@Override
	public void xdrEncodeInt(int value) throws OncRpcException, IOException {
		/*
		 * The integer value will be put at the actual position
		 * in the buffer. But before the position is checked.
		 * If the buffer need's a flush, it will be flushed
		 * before putting in the passed value. 
		 */
		if (byteBuffer.remaining() < ALIGNMENT) {
			/*
			 * Placeholder. The call to a flush
			 * method will placed in short.
			 * The flush method is expected
			 * to reset the internal positions
			 * to their initial values. In detail,
			 * we expect the next write position
			 * right behind the initial fragment header.
			 */
			flushFragment();
		} /* endif (Is there enough space for an integer in the buffer?) */ 
		
		/*
		 * Now there should be enough space to put
		 * the passed integer value into the buffer.
		 */
		byteBuffer.putInt(value);
	}

	@Override
	public void xdrEncodeOpaque(byte[] value, int offset, int length) throws OncRpcException, IOException {
		/*
		 * Shall a check be done, that the passed byte array
		 * is different from 'null' and
		 * the sum (offset + length) does not exceed
		 * the length of the byte array?
		 * A check is done!
		 */
		if ( (value != null)
				&& ((offset + length) <= value.length) ) {
			/*
			 * As long as the remaining bytes to encode
			 * exceed the remaining space in the byte buffer,
			 * the actually possible blocks are put into the
			 * buffer and the buffer is flushed.
			 */
			int availableSpace = byteBuffer.remaining();
			
			while (length > availableSpace) {
				
				byteBuffer.put(value, offset, availableSpace);
				length -= availableSpace;
				offset += availableSpace;
				
				/*
				 * Now the byte buffer is flushed
				 * and is expected to reset its internal
				 * write position right behind the
				 * initial fragment header.
				 */
				flushFragment();

				/*
				 * Remind the remaining bytes in the
				 * buffer as the now available space.
				 */
				availableSpace = byteBuffer.remaining();
			}
			
			/*
			 * Are there any bytes left?
			 */
			if (length > 0) {
				/*
				 * Calculate the padding to align
				 * the opaque data into a data block fitting
				 * a multiple of 4 bytes.
				 */
				int padding = (ALIGNMENT - (length & ALIGNMENT_MOD)) & ALIGNMENT_MOD;
				
				/*
				 * Put the remaining bytes into the buffer.
				 */
				byteBuffer.put(value, offset, length);
				
				/* 
				 * If the padding is greater than 0,
				 * then fill up the buffer with the necessary
				 * zeros.
				 */
				if (padding > 0) {
					byteBuffer.put(PADDING_ZEROS, 0, padding);
				}
			}
		} else {
			/*
			 * The passed parameters are not properly
			 * set. The desired encoding cannot be
			 * applied.
			 */
			throw new OncRpcException(OncRpcException.RPC_CANTENCODEARGS);
		} 
	}
	
	private void flushFragment() throws IOException {
		/*
		 * There is no space left in the buffer to take more
		 * data. Therefore the buffer gets flushed to its destination.
		 * 
		 * Before, the actual fragment header is completed.
		 * The actual buffer position is reminded as last position.
		 */
		int lastPosition = byteBuffer.position();

		/*
		 * The byte buffer is reset to the position
		 * of the actual fragment header, which is marked in the
		 * byte buffer.
		 */
		byteBuffer.reset();

		/*
		 * The fragment header is completed:
		 * - It takes the length of the fragment (last buffer
		 *   position minus actual buffer position minus fragment
		 *   header length (4)).
		 * - As the encoding ends with this step, the fragment
		 *   is marked as last fragment.
		 */
		byteBuffer.putInt(lastPosition - byteBuffer.position() - FRAGMENT_HEADER_LENGTH);
		
		/*
		 * The content of the byte buffer is flushed.
		 */
		flush();
		
		/*
		 * The next data for the message is waiting to be encoded.
		 * The start position of the buffer is marked and the position
		 * is advanced by the fragement header length. 
		 */
		byteBuffer
			.mark()
			.position(byteBuffer.position() + FRAGMENT_HEADER_LENGTH);
	}
	
	private void flush() throws IOException {
		/*
		 * The byte buffer is flipped to enable the TCP-Channel to read
		 * the written contents.
		 */
		byteBuffer.flip();
		
		/*
		 * The content of the byte buffer is written by the TCP-Channel.
		 */
		tcpChannel.write(byteBuffer);
		
		/*
		 * Finally the byte buffer is rewinded in order
		 * to prepare it for the next encoding turn. 
		 */
		byteBuffer.rewind();
	}

	private static byte[] getPaddingZeros() {
		byte[] paddingZeros = new byte[ALIGNMENT];
		
		/*
		 * Explicitly set the padding bytes to 0. 
		 */
		for (int index = 0; index < ALIGNMENT; ++index) {
			paddingZeros[index] = 0;
		}
		
		return paddingZeros;
	}
	
	private SocketChannel tcpChannel;
	private ByteBuffer byteBuffer;
	
	private static final int BUFFER_SIZE_MIN = 1024;
	private static final int ALIGNMENT = (Integer.SIZE / Byte.SIZE);
	private static final int ALIGNMENT_MOD = ALIGNMENT - 1;
	private static final int FRAGMENT_HEADER_LENGTH = ALIGNMENT;
	private static final byte[] PADDING_ZEROS = getPaddingZeros();
}
