using System;

using Org.BouncyCastle.Crypto.Parameters;

namespace Org.BouncyCastle.Crypto.Engines
{
	/**
	* A Noekeon engine, using direct-key mode.
	*/
	public class NoekeonEngine
		: IBlockCipher
	{
		private const int GenericSize = 16; // Block and key size, as well as the amount of rounds.

		private static readonly uint[] nullVector = 
		{
			0x00, 0x00, 0x00, 0x00 // Used in decryption
		};

		private static readonly uint[] roundConstants = 
		{
			0x80, 0x1b, 0x36, 0x6c,
			0xd8, 0xab, 0x4d, 0x9a,
			0x2f, 0x5e, 0xbc, 0x63,
			0xc6, 0x97, 0x35, 0x6a,
			0xd4
		};

		private uint[]	state = new uint[4], // a
						subKeys = new uint[4], // k
						decryptKeys = new uint[4];

		private bool _initialised, _forEncryption;

		/**
		* Create an instance of the Noekeon encryption algorithm
		* and set some defaults
		*/
		public NoekeonEngine()
		{
			_initialised = false;
		}

		public string AlgorithmName
		{
			get { return "Noekeon"; }
		}

		public bool IsPartialBlockOkay
		{
			get { return false; }
		}

		public int GetBlockSize()
		{
			return GenericSize;
		}

		/**
		* initialise
		*
		* @param forEncryption whether or not we are for encryption.
		* @param params the parameters required to set up the cipher.
		* @exception ArgumentException if the params argument is
		* inappropriate.
		*/
		public void Init(
			bool				forEncryption,
			ICipherParameters	parameters)
		{
			if (!(parameters is KeyParameter))
				throw new ArgumentException("Invalid parameters passed to Noekeon init - " + parameters.GetType().Name, "parameters");

			_forEncryption = forEncryption;
			_initialised = true;

			KeyParameter p = (KeyParameter) parameters;

			setKey(p.GetKey());
		}

		public int ProcessBlock(
			byte[]	input,
			int		inOff,
			byte[]	output,
			int		outOff)
		{
			if (!_initialised)
				throw new InvalidOperationException(AlgorithmName + " not initialised");
			if ((inOff + GenericSize) > input.Length)
				throw new DataLengthException("input buffer too short");
			if ((outOff + GenericSize) > output.Length)
				throw new DataLengthException("output buffer too short");

			return _forEncryption
				?	encryptBlock(input, inOff, output, outOff)
				:	decryptBlock(input, inOff, output, outOff);
		}

		public void Reset()
		{
			// TODO This should do something in case the encryption is aborted
		}

		/**
		* Re-key the cipher.
		*
		* @param  key  the key to be used
		*/
		private void setKey(byte[] key)
		{
			subKeys[0] = bytesToIntBig(key, 0);
			subKeys[1] = bytesToIntBig(key, 4);
			subKeys[2] = bytesToIntBig(key, 8);
			subKeys[3] = bytesToIntBig(key, 12);
		}

		private int encryptBlock(
			byte[]	input,
			int		inOff,
			byte[]	output,
			int		outOff)
		{
			state[0] = bytesToIntBig(input, inOff);
			state[1] = bytesToIntBig(input, inOff+4);
			state[2] = bytesToIntBig(input, inOff+8);
			state[3] = bytesToIntBig(input, inOff+12);

			int i;
			for (i = 0; i < GenericSize; i++)
			{
				state[0] ^= roundConstants[i];
				theta(state, subKeys);
				pi1(state);
				gamma(state);
				pi2(state);            
			}

			state[0] ^= roundConstants[i];
			theta(state, subKeys);

			intToBytesBig(state[0], output, outOff);
			intToBytesBig(state[1], output, outOff+4);
			intToBytesBig(state[2], output, outOff+8);
			intToBytesBig(state[3], output, outOff+12);

			return GenericSize;
		}

		private int decryptBlock(
			byte[]	input,
			int		inOff,
			byte[]	output,
			int		outOff)
		{
			state[0] = bytesToIntBig(input, inOff);
			state[1] = bytesToIntBig(input, inOff+4);
			state[2] = bytesToIntBig(input, inOff+8);
			state[3] = bytesToIntBig(input, inOff+12);

			Array.Copy(subKeys, 0, decryptKeys, 0, subKeys.Length);
			theta(decryptKeys, nullVector);

			int i;
			for (i = GenericSize; i > 0; i--)
			{
				theta(state, decryptKeys);
				state[0] ^= roundConstants[i];
				pi1(state);
				gamma(state);
				pi2(state);
			}

			theta(state, decryptKeys);
			state[0] ^= roundConstants[i];

			intToBytesBig(state[0], output, outOff);
			intToBytesBig(state[1], output, outOff+4);
			intToBytesBig(state[2], output, outOff+8);
			intToBytesBig(state[3], output, outOff+12);

			return GenericSize;
		}

		private void gamma(uint[] a)
		{
			a[1] ^= ~a[3] & ~a[2];
			a[0] ^= a[2] & a[1];

			uint tmp = a[3];
			a[3]  = a[0];
			a[0]  = tmp;
			a[2] ^= a[0]^a[1]^a[3];

			a[1] ^= ~a[3] & ~a[2];
			a[0] ^= a[2] & a[1];
		}

		private void theta(uint[] a, uint[] k)
		{
			uint tmp;
			tmp   = a[0]^a[2]; 
			tmp  ^= rotl(tmp,8)^rotl(tmp,24); 
			a[1] ^= tmp; 
			a[3] ^= tmp; 

			for (int i = 0; i < 4; i++)
			{
				a[i] ^= k[i];
			}

			tmp   = a[1]^a[3]; 
			tmp  ^= rotl(tmp,8)^rotl(tmp,24); 
			a[0] ^= tmp; 
			a[2] ^= tmp;
		}

		private void pi1(uint[] a)
		{
			a[1] = rotl(a[1], 1);
			a[2] = rotl(a[2], 5);
			a[3] = rotl(a[3], 2);
		}

		private void pi2(uint[] a)
		{
			a[1] = rotl(a[1], 31);
			a[2] = rotl(a[2], 27);
			a[3] = rotl(a[3], 30);
		}

		// Helpers

		private uint bytesToIntBig(byte[] input, int off)
		{
			int result = ((input[off++]) << 24) |
			((input[off++] & 0xff) << 16) |
			((input[off++] & 0xff) <<  8) |
			(input[off  ] & 0xff);
			return (uint) result;
		}

		private void intToBytesBig(uint x, byte[] output, int off)
		{
			output[off++] = (byte)(x >> 24);
			output[off++] = (byte)(x >> 16);
			output[off++] = (byte)(x >>  8);
			output[off  ] = (byte)x;
		}

		private uint rotl(uint x, int y)
		{
			return (x << y) | (x >> (32-y));
		}
	}
}
