/*
 * Copyright (c) 2006-2011 Christian Plattner. All rights reserved.
 * Please refer to the LICENSE.txt for licensing details.
 */
package ch.ethz.ssh2.crypto.cipher;

import java.io.IOException;
import java.io.OutputStream;

/**
 * CipherOutputStream.
 *
 * @author Christian Plattner
 * @version $Id$
 */
public class CipherOutputStream
{
	BlockCipher currentCipher;
	OutputStream bo;
	byte[] buffer;
	byte[] enc;
	int blockSize;
	int pos;

	/*
	 * We cannot use java.io.BufferedOutputStream, since that is not available
	 * in J2ME. Everything could be improved here alot.
	 */

	private static final int BUFF_SIZE = 8192;
	byte[] out_buffer = new byte[BUFF_SIZE];
	int out_buffer_pos = 0;

	public CipherOutputStream(BlockCipher tc, OutputStream bo)
	{
		this.bo = bo;
		changeCipher(tc);
	}

	private void internal_write(byte[] src, int off, int len) throws IOException
	{
		while (len > 0)
		{
			int space = BUFF_SIZE - out_buffer_pos;
			int copy = (len > space) ? space : len;

			System.arraycopy(src, off, out_buffer, out_buffer_pos, copy);

			off += copy;
			out_buffer_pos += copy;
			len -= copy;

			if (out_buffer_pos >= BUFF_SIZE)
			{
				bo.write(out_buffer, 0, BUFF_SIZE);
				out_buffer_pos = 0;
			}
		}
	}

	private void internal_write(int b) throws IOException
	{
		out_buffer[out_buffer_pos++] = (byte) b;
		if (out_buffer_pos >= BUFF_SIZE)
		{
			bo.write(out_buffer, 0, BUFF_SIZE);
			out_buffer_pos = 0;
		}
	}

	public void flush() throws IOException
	{
		if (pos != 0)
		{
			throw new IOException("FATAL: cannot flush since crypto buffer is not aligned.");
		}

		if (out_buffer_pos > 0)
		{
			bo.write(out_buffer, 0, out_buffer_pos);
			out_buffer_pos = 0;
		}
		bo.flush();
	}

	public void changeCipher(BlockCipher bc)
	{
		this.currentCipher = bc;
		blockSize = bc.getBlockSize();
		buffer = new byte[blockSize];
		enc = new byte[blockSize];
		pos = 0;
	}

	private void writeBlock() throws IOException
	{
		try
		{
			currentCipher.transformBlock(buffer, 0, enc, 0);
		}
		catch (Exception e)
		{
			throw new IOException("Error while decrypting block.", e);
		}

		internal_write(enc, 0, blockSize);
		pos = 0;
	}

	public void write(byte[] src, int off, int len) throws IOException
	{
		while (len > 0)
		{
			int avail = blockSize - pos;
			int copy = Math.min(avail, len);

			System.arraycopy(src, off, buffer, pos, copy);
			pos += copy;
			off += copy;
			len -= copy;

			if (pos >= blockSize)
			{
				writeBlock();
			}
		}
	}

	public void write(int b) throws IOException
	{
		buffer[pos++] = (byte) b;
		if (pos >= blockSize)
		{
			writeBlock();
		}
	}

	public void writePlain(int b) throws IOException
	{
		if (pos != 0)
		{
			throw new IOException("Cannot write plain since crypto buffer is not aligned.");
		}
		internal_write(b);
	}

	public void writePlain(byte[] b, int off, int len) throws IOException
	{
		if (pos != 0)
		{
			throw new IOException("Cannot write plain since crypto buffer is not aligned.");
		}
		internal_write(b, off, len);
	}
}
