using System;
using System.IO;

using Org.BouncyCastle.Bcpg.Sig;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;

namespace Org.BouncyCastle.Bcpg.OpenPgp
{
	/// <remarks>Generator for PGP signatures.</remarks>
	// TODO Should be able to implement ISigner?
    public class PgpSignatureGenerator
    {
		private static readonly SignatureSubpacket[] EmptySignatureSubpackets = new SignatureSubpacket[0];

		private PublicKeyAlgorithmTag	keyAlgorithm;
        private HashAlgorithmTag		hashAlgorithm;
        private PgpPrivateKey			privKey;
        private ISigner					sig;
        private IDigest					dig;
        private int						signatureType;
        private byte					lastb;

		private SignatureSubpacket[] unhashed = EmptySignatureSubpackets;
        private SignatureSubpacket[] hashed = EmptySignatureSubpackets;

		/// <summary>Create a generator for the passed in keyAlgorithm and hashAlgorithm codes.</summary>
        public PgpSignatureGenerator(
            PublicKeyAlgorithmTag	keyAlgorithm,
            HashAlgorithmTag		hashAlgorithm)
        {
            this.keyAlgorithm = keyAlgorithm;
            this.hashAlgorithm = hashAlgorithm;

			dig = DigestUtilities.GetDigest(PgpUtilities.GetDigestName(hashAlgorithm));
            sig = SignerUtilities.GetSigner(PgpUtilities.GetSignatureName(keyAlgorithm, hashAlgorithm));
        }

		/// <summary>Initialise the generator for signing.</summary>
        public void InitSign(
            int				sigType,
            PgpPrivateKey	key)
        {
			InitSign(sigType, key, null);
        }

		/// <summary>Initialise the generator for signing.</summary>
		public void InitSign(
			int				sigType,
			PgpPrivateKey	key,
			SecureRandom	random)
		{
			this.privKey = key;
			this.signatureType = sigType;

			try
			{
				ICipherParameters cp = key.Key;
				if (random != null)
				{
					cp = new ParametersWithRandom(key.Key, random);
				}

				sig.Init(true, cp);
			}
			catch (InvalidKeyException e)
			{
				throw new PgpException("invalid key.", e);
			}

			dig.Reset();
			lastb = 0;
		}

		public void Update(
            byte b)
        {
            if (signatureType == PgpSignature.CanonicalTextDocument)
            {
				doCanonicalUpdateByte(b);
            }
            else
            {
				doUpdateByte(b);
            }
        }

		private void doCanonicalUpdateByte(
			byte b)
		{
			if (b == '\r')
			{
				doUpdateCRLF();
			}
			else if (b == '\n')
			{
				if (lastb != '\r')
				{
					doUpdateCRLF();
				}
			}
			else
			{
				doUpdateByte(b);
			}

			lastb = b;
		}

		private void doUpdateCRLF()
		{
			doUpdateByte((byte)'\r');
			doUpdateByte((byte)'\n');
		}

		private void doUpdateByte(
			byte b)
		{
			sig.Update(b);
			dig.Update(b);
		}

		public void Update(
            params byte[] b)
        {
			Update(b, 0, b.Length);
        }

		public void Update(
            byte[]	b,
            int		off,
            int		len)
        {
            if (signatureType == PgpSignature.CanonicalTextDocument)
            {
                int finish = off + len;

				for (int i = off; i != finish; i++)
                {
                    doCanonicalUpdateByte(b[i]);
                }
            }
            else
            {
                sig.BlockUpdate(b, off, len);
                dig.BlockUpdate(b, off, len);
            }
        }

		public void SetHashedSubpackets(
            PgpSignatureSubpacketVector hashedPackets)
        {
			hashed = hashedPackets == null
				?	EmptySignatureSubpackets
				:	hashedPackets.ToSubpacketArray();
        }

		public void SetUnhashedSubpackets(
            PgpSignatureSubpacketVector unhashedPackets)
        {
			unhashed = unhashedPackets == null
				?	EmptySignatureSubpackets
				:	unhashedPackets.ToSubpacketArray();
        }

		/// <summary>Return the one pass header associated with the current signature.</summary>
        public PgpOnePassSignature GenerateOnePassVersion(
            bool isNested)
        {
            return new PgpOnePassSignature(
				new OnePassSignaturePacket(
					signatureType, hashAlgorithm, keyAlgorithm, privKey.KeyId, isNested));
        }

		/// <summary>Return a signature object containing the current signature state.</summary>
        public PgpSignature Generate()
        {
			SignatureSubpacket[] hPkts = hashed, unhPkts = unhashed;

			if (!packetPresent(hashed, SignatureSubpacketTag.CreationTime))
			{
				hPkts = insertSubpacket(hPkts, new SignatureCreationTime(false, DateTime.UtcNow));
			}

			if (!packetPresent(hashed, SignatureSubpacketTag.IssuerKeyId)
				&& !packetPresent(unhashed, SignatureSubpacketTag.IssuerKeyId))
			{
				unhPkts = insertSubpacket(unhPkts, new IssuerKeyId(false, privKey.KeyId));
			}

			int version = 4;
			byte[] hData;

			try
            {
				MemoryStream hOut = new MemoryStream();

				for (int i = 0; i != hPkts.Length; i++)
				{
					hPkts[i].Encode(hOut);
				}

				byte[] data = hOut.ToArray();

				MemoryStream sOut = new MemoryStream(data.Length + 6);
				sOut.WriteByte((byte)version);
                sOut.WriteByte((byte)signatureType);
                sOut.WriteByte((byte)keyAlgorithm);
                sOut.WriteByte((byte)hashAlgorithm);
				sOut.WriteByte((byte)(data.Length >> 8));
                sOut.WriteByte((byte)data.Length);
                sOut.Write(data, 0, data.Length);

				hData = sOut.ToArray();
			}
            catch (IOException e)
            {
                throw new PgpException("exception encoding hashed data.", e);
            }

			sig.BlockUpdate(hData, 0, hData.Length);
            dig.BlockUpdate(hData, 0, hData.Length);

			hData = new byte[]
			{
				(byte) version,
				0xff,
				(byte)(hData.Length >> 24),
				(byte)(hData.Length >> 16),
				(byte)(hData.Length >> 8),
				(byte) hData.Length
			};

			sig.BlockUpdate(hData, 0, hData.Length);
            dig.BlockUpdate(hData, 0, hData.Length);

			byte[] sigBytes = sig.GenerateSignature();
			byte[] digest = DigestUtilities.DoFinal(dig);
			byte[] fingerPrint = new byte[] { digest[0], digest[1] };

			// an RSA signature
			bool isRsa = keyAlgorithm == PublicKeyAlgorithmTag.RsaSign
				|| keyAlgorithm == PublicKeyAlgorithmTag.RsaGeneral;

			MPInteger[] sigValues = isRsa
				?	PgpUtilities.RsaSigToMpi(sigBytes)
				:	PgpUtilities.DsaSigToMpi(sigBytes);

			return new PgpSignature(
				new SignaturePacket(signatureType, privKey.KeyId, keyAlgorithm,
					hashAlgorithm, hPkts, unhPkts, fingerPrint, sigValues));
        }

		/// <summary>Generate a certification for the passed in ID and key.</summary>
		/// <param name="id">The ID we are certifying against the public key.</param>
		/// <param name="pubKey">The key we are certifying against the ID.</param>
		/// <returns>The certification.</returns>
        public PgpSignature GenerateCertification(
            string			id,
            PgpPublicKey	pubKey)
        {
			UpdateWithPublicKey(pubKey);

			//
            // hash in the id
            //
			UpdateWithIdData(0xb4, Strings.ToByteArray(id));

			return Generate();
        }

		/// <summary>Generate a certification for the passed in userAttributes.</summary>
		/// <param name="userAttributes">The ID we are certifying against the public key.</param>
		/// <param name="pubKey">The key we are certifying against the ID.</param>
		/// <returns>The certification.</returns>
		public PgpSignature GenerateCertification(
			PgpUserAttributeSubpacketVector	userAttributes,
			PgpPublicKey					pubKey)
		{
			UpdateWithPublicKey(pubKey);

			//
			// hash in the attributes
			//
			try
			{
				MemoryStream bOut = new MemoryStream();
				foreach (UserAttributeSubpacket packet in userAttributes.ToSubpacketArray())
				{
					packet.Encode(bOut);
				}
				UpdateWithIdData(0xd1, bOut.ToArray());
			}
			catch (IOException e)
			{
				throw new PgpException("cannot encode subpacket array", e);
			}

			return this.Generate();
		}

		/// <summary>Generate a certification for the passed in key against the passed in master key.</summary>
		/// <param name="masterKey">The key we are certifying against.</param>
		/// <param name="pubKey">The key we are certifying.</param>
		/// <returns>The certification.</returns>
        public PgpSignature GenerateCertification(
            PgpPublicKey	masterKey,
            PgpPublicKey	pubKey)
        {
			UpdateWithPublicKey(masterKey);
			UpdateWithPublicKey(pubKey);

			return Generate();
        }

		/// <summary>Generate a certification, such as a revocation, for the passed in key.</summary>
		/// <param name="pubKey">The key we are certifying.</param>
		/// <returns>The certification.</returns>
        public PgpSignature GenerateCertification(
            PgpPublicKey pubKey)
        {
			UpdateWithPublicKey(pubKey);

			return Generate();
        }

		private byte[] GetEncodedPublicKey(
			PgpPublicKey pubKey) 
		{
			try
			{
				return pubKey.publicPk.GetEncodedContents();
			}
			catch (IOException e)
			{
				throw new PgpException("exception preparing key.", e);
			}
		}

		private bool packetPresent(
			SignatureSubpacket[]	packets,
			SignatureSubpacketTag	type)
		{
			for (int i = 0; i != packets.Length; i++)
			{
				if (packets[i].SubpacketType == type)
				{
					return true;
				}
			}

			return false;
		}

		private SignatureSubpacket[] insertSubpacket(
			SignatureSubpacket[]	packets,
			SignatureSubpacket		subpacket)
		{
			SignatureSubpacket[] tmp = new SignatureSubpacket[packets.Length + 1];
			tmp[0] = subpacket;
			packets.CopyTo(tmp, 1);
			return tmp;
		}

		private void UpdateWithIdData(
			int		header,
			byte[]	idBytes)
		{
			this.Update(
				(byte) header,
				(byte)(idBytes.Length >> 24),
				(byte)(idBytes.Length >> 16),
				(byte)(idBytes.Length >> 8),
				(byte)(idBytes.Length));
			this.Update(idBytes);
		}

		private void UpdateWithPublicKey(
			PgpPublicKey key)
		{
			byte[] keyBytes = GetEncodedPublicKey(key);

			this.Update(
				(byte) 0x99,
				(byte)(keyBytes.Length >> 8),
				(byte)(keyBytes.Length));
			this.Update(keyBytes);
		}
	}
}
