using System;

using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;

namespace Org.BouncyCastle.Crypto.Encodings
{
	/**
	* this does your basic Pkcs 1 v1.5 padding - whether or not you should be using this
	* depends on your application - see Pkcs1 Version 2 for details.
	*/
	public class Pkcs1Encoding
		: IAsymmetricBlockCipher
	{
		/**
		 * some providers fail to include the leading zero in PKCS1 encoded blocks. If you need to
		 * work with one of these set the system property Org.BouncyCastle.Pkcs1.Strict to false.
		 */
		public const string StrictLengthEnabledProperty = "Org.BouncyCastle.Pkcs1.Strict";

		private const int HeaderLength = 10;

		/**
		 * The same effect can be achieved by setting the static property directly
		 * <p>
		 * The static property is checked during construction of the encoding object, it is set to
		 * true by default.
		 * </p>
		 */
		public static bool StrictLengthEnabled
		{
			get { return strictLengthEnabled[0]; }
			set { strictLengthEnabled[0] = value; }
		}

		private static readonly bool[] strictLengthEnabled;

		static Pkcs1Encoding()
		{
			string strictProperty = Platform.GetEnvironmentVariable(StrictLengthEnabledProperty);

			strictLengthEnabled = new bool[]{ strictProperty == null || strictProperty.Equals("true")};
		}


		private SecureRandom			random;
		private IAsymmetricBlockCipher	engine;
		private bool					forEncryption;
		private bool					forPrivateKey;
		private bool					useStrictLength;

		/**
		 * Basic constructor.
		 * @param cipher
		 */
		public Pkcs1Encoding(
			IAsymmetricBlockCipher cipher)
		{
			this.engine = cipher;
			this.useStrictLength = StrictLengthEnabled;
		}

		public IAsymmetricBlockCipher GetUnderlyingCipher()
		{
			return engine;
		}

		public string AlgorithmName
		{
			get { return engine.AlgorithmName + "/PKCS1Padding"; }
		}

		public void Init(
			bool				forEncryption,
			ICipherParameters	parameters)
		{
			AsymmetricKeyParameter kParam;
			if (parameters is ParametersWithRandom)
			{
				ParametersWithRandom rParam = (ParametersWithRandom)parameters;

				this.random = rParam.Random;
				kParam = (AsymmetricKeyParameter)rParam.Parameters;
			}
			else
			{
				this.random = new SecureRandom();
				kParam = (AsymmetricKeyParameter)parameters;
			}

			engine.Init(forEncryption, parameters);

			this.forPrivateKey = kParam.IsPrivate;
			this.forEncryption = forEncryption;
		}

		public int GetInputBlockSize()
		{
			int baseBlockSize = engine.GetInputBlockSize();

			return forEncryption
				?	baseBlockSize - HeaderLength
				:	baseBlockSize;
		}

		public int GetOutputBlockSize()
		{
			int baseBlockSize = engine.GetOutputBlockSize();

			return forEncryption
				?	baseBlockSize
				:	baseBlockSize - HeaderLength;
		}

		public byte[] ProcessBlock(
			byte[]	input,
			int		inOff,
			int		length)
		{
			return forEncryption
				?	EncodeBlock(input, inOff, length)
				:	DecodeBlock(input, inOff, length);
		}

		private byte[] EncodeBlock(
			byte[]	input,
			int		inOff,
			int		inLen)
		{
	        if (inLen > GetInputBlockSize())
	            throw new ArgumentException("input data too large", "inLen");

	        byte[] block = new byte[engine.GetInputBlockSize()];

			if (forPrivateKey)
			{
				block[0] = 0x01;                        // type code 1

				for (int i = 1; i != block.Length - inLen - 1; i++)
				{
					block[i] = (byte)0xFF;
				}
			}
			else
			{
				random.NextBytes(block);                // random fill

				block[0] = 0x02;                        // type code 2

				//
				// a zero byte marks the end of the padding, so all
				// the pad bytes must be non-zero.
				//
				for (int i = 1; i != block.Length - inLen - 1; i++)
				{
					while (block[i] == 0)
					{
						block[i] = (byte)random.NextInt();
					}
				}
			}

			block[block.Length - inLen - 1] = 0x00;       // mark the end of the padding
			Array.Copy(input, inOff, block, block.Length - inLen, inLen);

			return engine.ProcessBlock(block, 0, block.Length);
		}

		/**
		* @exception InvalidCipherTextException if the decrypted block is not in Pkcs1 format.
		*/
		private byte[] DecodeBlock(
			byte[]	input,
			int		inOff,
			int		inLen)
		{
			byte[] block = engine.ProcessBlock(input, inOff, inLen);

			if (block.Length < GetOutputBlockSize())
			{
				throw new InvalidCipherTextException("block truncated");
			}

			byte type = block[0];

			if (type != 1 && type != 2)
			{
				throw new InvalidCipherTextException("unknown block type");
			}

			if (useStrictLength && block.Length != engine.GetOutputBlockSize())
			{
				throw new InvalidCipherTextException("block incorrect size");
			}

			//
			// find and extract the message block.
			//
			int start;
			for (start = 1; start != block.Length; start++)
			{
				byte pad = block[start];

				if (pad == 0)
				{
					break;
				}

				if (type == 1 && pad != (byte)0xff)
				{
					throw new InvalidCipherTextException("block padding incorrect");
				}
			}

			start++;           // data should start at the next byte

			if (start > block.Length || start < HeaderLength)
			{
				throw new InvalidCipherTextException("no data in block");
			}

			byte[] result = new byte[block.Length - start];

			Array.Copy(block, start, result, 0, result.Length);

			return result;
		}
	}

}
