using System;

using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;

namespace Org.BouncyCastle.Crypto.Engines
{
    /**
    * support class for constructing intergrated encryption ciphers
    * for doing basic message exchanges on top of key agreement ciphers
    */
    public class IesEngine
    {
        private readonly IBasicAgreement     agree;
        private readonly IDerivationFunction kdf;
        private readonly IMac                mac;
        private readonly BufferedBlockCipher cipher;
		private readonly byte[]              macBuf;

		private bool				forEncryption;
        private ICipherParameters	privParam, pubParam;
        private IesParameters		param;

        /**
        * set up for use with stream mode, where the key derivation function
        * is used to provide a stream of bytes to xor with the message.
        *
        * @param agree the key agreement used as the basis for the encryption
        * @param kdf the key derivation function used for byte generation
        * @param mac the message authentication code generator for the message
        */
        public IesEngine(
            IBasicAgreement     agree,
            IDerivationFunction kdf,
            IMac                mac)
        {
            this.agree = agree;
            this.kdf = kdf;
            this.mac = mac;
            this.macBuf = new byte[mac.GetMacSize()];
//            this.cipher = null;
        }

        /**
        * set up for use in conjunction with a block cipher to handle the
        * message.
        *
        * @param agree the key agreement used as the basis for the encryption
        * @param kdf the key derivation function used for byte generation
        * @param mac the message authentication code generator for the message
        * @param cipher the cipher to used for encrypting the message
        */
        public IesEngine(
            IBasicAgreement     agree,
            IDerivationFunction kdf,
            IMac                mac,
            BufferedBlockCipher cipher)
        {
            this.agree = agree;
            this.kdf = kdf;
            this.mac = mac;
            this.macBuf = new byte[mac.GetMacSize()];
            this.cipher = cipher;
        }

        /**
        * Initialise the encryptor.
        *
        * @param forEncryption whether or not this is encryption/decryption.
        * @param privParam our private key parameters
        * @param pubParam the recipient's/sender's public key parameters
        * @param param encoding and derivation parameters.
        */
        public void Init(
            bool                     forEncryption,
            ICipherParameters            privParameters,
            ICipherParameters            pubParameters,
            ICipherParameters            iesParameters)
        {
            this.forEncryption = forEncryption;
            this.privParam = privParameters;
            this.pubParam = pubParameters;
            this.param = (IesParameters)iesParameters;
        }

        private byte[] DecryptBlock(
            byte[]  in_enc,
            int     inOff,
            int     inLen,
            byte[]  z)
        {
            byte[]          M = null;
            KeyParameter    macKey = null;
            KdfParameters   kParam = new KdfParameters(z, param.GetDerivationV());
            int             macKeySize = param.MacKeySize;

            kdf.Init(kParam);

            inLen -= mac.GetMacSize();

            if (cipher == null)     // stream mode
            {
				byte[] Buffer = GenerateKdfBytes(kParam, inLen + (macKeySize / 8));

                M = new byte[inLen];

                for (int i = 0; i != inLen; i++)
                {
                    M[i] = (byte)(in_enc[inOff + i] ^ Buffer[i]);
                }

                macKey = new KeyParameter(Buffer, inLen, (macKeySize / 8));
            }
            else
            {
                int cipherKeySize = ((IesWithCipherParameters)param).CipherKeySize;
				byte[] Buffer = GenerateKdfBytes(kParam, (cipherKeySize / 8) + (macKeySize / 8));

                cipher.Init(false, new KeyParameter(Buffer, 0, (cipherKeySize / 8)));

				M = cipher.DoFinal(in_enc, inOff, inLen);

				macKey = new KeyParameter(Buffer, (cipherKeySize / 8), (macKeySize / 8));
            }

            byte[] macIV = param.GetEncodingV();

            mac.Init(macKey);
            mac.BlockUpdate(in_enc, inOff, inLen);
            mac.BlockUpdate(macIV, 0, macIV.Length);
            mac.DoFinal(macBuf, 0);

			inOff += inLen;

			for (int t = 0; t < macBuf.Length; t++)
            {
                if (macBuf[t] != in_enc[inOff + t])
                {
                    throw (new InvalidCipherTextException("IMac codes failed to equal."));
                }
            }

            return M;
        }

        private byte[] EncryptBlock(
            byte[]  input,
            int     inOff,
            int     inLen,
            byte[]  z)
        {
            byte[]          C = null;
            KeyParameter    macKey = null;
            KdfParameters   kParam = new KdfParameters(z, param.GetDerivationV());
            int             c_text_length = 0;
            int             macKeySize = param.MacKeySize;

            if (cipher == null)     // stream mode
            {
				byte[] Buffer = GenerateKdfBytes(kParam, inLen + (macKeySize / 8));

                C = new byte[inLen + mac.GetMacSize()];
                c_text_length = inLen;

				for (int i = 0; i != inLen; i++)
                {
                    C[i] = (byte)(input[inOff + i] ^ Buffer[i]);
                }

                macKey = new KeyParameter(Buffer, inLen, (macKeySize / 8));
            }
            else
            {
                int cipherKeySize = ((IesWithCipherParameters)param).CipherKeySize;
				byte[] Buffer = GenerateKdfBytes(kParam, (cipherKeySize / 8) + (macKeySize / 8));

                cipher.Init(true, new KeyParameter(Buffer, 0, (cipherKeySize / 8)));

                c_text_length = cipher.GetOutputSize(inLen);
				byte[] tmp = new byte[c_text_length];

				int len = cipher.ProcessBytes(input, inOff, inLen, tmp, 0);
				len += cipher.DoFinal(tmp, len);

				C = new byte[len + mac.GetMacSize()];
				c_text_length = len;

				Array.Copy(tmp, 0, C, 0, len);

				macKey = new KeyParameter(Buffer, (cipherKeySize / 8), (macKeySize / 8));
            }

            byte[] macIV = param.GetEncodingV();

            mac.Init(macKey);
            mac.BlockUpdate(C, 0, c_text_length);
            mac.BlockUpdate(macIV, 0, macIV.Length);
            //
            // return the message and it's MAC
            //
            mac.DoFinal(C, c_text_length);
            return C;
        }

		private byte[] GenerateKdfBytes(
			KdfParameters	kParam,
			int				length)
		{
			byte[] buf = new byte[length];

			kdf.Init(kParam);

			kdf.GenerateBytes(buf, 0, buf.Length);

			return buf;
		}

		public byte[] ProcessBlock(
            byte[]  input,
            int     inOff,
            int     inLen)
        {
            agree.Init(privParam);

			BigInteger z = agree.CalculateAgreement(pubParam);

			// TODO Check that this is right (...Unsigned? Check length?)
			byte[] zBytes = z.ToByteArray();

			return forEncryption
				?	EncryptBlock(input, inOff, inLen, zBytes)
                :	DecryptBlock(input, inOff, inLen, zBytes);
        }
    }

}
