using System;

using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Paddings;

namespace Org.BouncyCastle.Crypto.Macs
{
    /**
    * standard CBC Block Cipher MAC - if no padding is specified the default of
    * pad of zeroes is used.
    */
    public class CbcBlockCipherMac
		: IMac
    {
        private byte[] mac;
        private byte[] Buffer;
        private int bufOff;
        private IBlockCipher cipher;
        private IBlockCipherPadding padding;
		private int macSize;

		/**
        * create a standard MAC based on a CBC block cipher. This will produce an
        * authentication code half the length of the block size of the cipher.
        *
        * @param cipher the cipher to be used as the basis of the MAC generation.
        */
        public CbcBlockCipherMac(
			IBlockCipher cipher)
			: this(cipher, (cipher.GetBlockSize() * 8) / 2, null)
		{
		}

		/**
        * create a standard MAC based on a CBC block cipher. This will produce an
        * authentication code half the length of the block size of the cipher.
        *
        * @param cipher the cipher to be used as the basis of the MAC generation.
        * @param padding the padding to be used to complete the last block.
        */
        public CbcBlockCipherMac(
            IBlockCipher		cipher,
            IBlockCipherPadding	padding)
        : this(cipher, (cipher.GetBlockSize() * 8) / 2, padding)
		{
		}

		/**
        * create a standard MAC based on a block cipher with the size of the
        * MAC been given in bits. This class uses CBC mode as the basis for the
        * MAC generation.
        * <p>
        * Note: the size of the MAC must be at least 24 bits (FIPS Publication 81),
        * or 16 bits if being used as a data authenticator (FIPS Publication 113),
        * and in general should be less than the size of the block cipher as it reduces
        * the chance of an exhaustive attack (see Handbook of Applied Cryptography).
        * </p>
        * @param cipher the cipher to be used as the basis of the MAC generation.
        * @param macSizeInBits the size of the MAC in bits, must be a multiple of 8.
        */
        public CbcBlockCipherMac(
            IBlockCipher	cipher,
            int				macSizeInBits)
			: this(cipher, macSizeInBits, null)
		{
		}

		/**
        * create a standard MAC based on a block cipher with the size of the
        * MAC been given in bits. This class uses CBC mode as the basis for the
        * MAC generation.
        * <p>
        * Note: the size of the MAC must be at least 24 bits (FIPS Publication 81),
        * or 16 bits if being used as a data authenticator (FIPS Publication 113),
        * and in general should be less than the size of the block cipher as it reduces
        * the chance of an exhaustive attack (see Handbook of Applied Cryptography).
        * </p>
        * @param cipher the cipher to be used as the basis of the MAC generation.
        * @param macSizeInBits the size of the MAC in bits, must be a multiple of 8.
        * @param padding the padding to be used to complete the last block.
        */
        public CbcBlockCipherMac(
            IBlockCipher		cipher,
            int					macSizeInBits,
            IBlockCipherPadding	padding)
        {
            if ((macSizeInBits % 8) != 0)
                throw new ArgumentException("MAC size must be multiple of 8");

			this.cipher = new CbcBlockCipher(cipher);
            this.padding = padding;
            this.macSize = macSizeInBits / 8;

            mac = new byte[cipher.GetBlockSize()];

            Buffer = new byte[cipher.GetBlockSize()];
            bufOff = 0;
        }

		public string AlgorithmName
        {
            get { return cipher.AlgorithmName; }
        }

		public void Init(
            ICipherParameters parameters)
        {
            Reset();

			cipher.Init(true, parameters);
        }

		public int GetMacSize()
        {
            return macSize;
        }

		public void Update(
            byte input)
        {
			if (bufOff == Buffer.Length)
            {
				cipher.ProcessBlock(Buffer, 0, mac, 0);
                bufOff = 0;
            }

			Buffer[bufOff++] = input;
        }

        public void BlockUpdate(
            byte[]	input,
            int		inOff,
            int		len)
        {
            if (len < 0)
                throw new ArgumentException("Can't have a negative input length!");

			int blockSize = cipher.GetBlockSize();
            int resultLen = 0;
            int gapLen = blockSize - bufOff;

            if (len > gapLen)
            {
                Array.Copy(input, inOff, Buffer, bufOff, gapLen);

                resultLen += cipher.ProcessBlock(Buffer, 0, mac, 0);

                bufOff = 0;
                len -= gapLen;
                inOff += gapLen;

                while (len > blockSize)
                {
                    resultLen += cipher.ProcessBlock(input, inOff, mac, 0);

                    len -= blockSize;
                    inOff += blockSize;
                }
            }

            Array.Copy(input, inOff, Buffer, bufOff, len);

            bufOff += len;
        }

        public int DoFinal(
            byte[]	output,
            int		outOff)
        {
            int blockSize = cipher.GetBlockSize();

            if (padding == null)
            {
                // pad with zeroes
                while (bufOff < blockSize)
                {
                    Buffer[bufOff++] = 0;
                }
            }
            else
            {
                if (bufOff == blockSize)
                {
                    cipher.ProcessBlock(Buffer, 0, mac, 0);
                    bufOff = 0;
                }

				padding.AddPadding(Buffer, bufOff);
            }

			cipher.ProcessBlock(Buffer, 0, mac, 0);

			Array.Copy(mac, 0, output, outOff, macSize);

			Reset();

			return macSize;
        }

		/**
        * Reset the mac generator.
        */
        public void Reset()
        {
            // Clear the buffer.
			Array.Clear(Buffer, 0, Buffer.Length);
			bufOff = 0;

			// Reset the underlying cipher.
            cipher.Reset();
        }
    }
}
