using System;
using System.Collections;
using System.Collections.Generic;
using LipingShare.LCLib.Asn1Processor;
using System.Security.Cryptography;

namespace UnityEngine.Purchasing.Security {

	internal class DistinguishedName {
		public string Country { get; set; }
		public string Organization { get; set; }
		public string OrganizationalUnit { get; set; }
		public string Dnq { get; set; }
		public string State { get; set; }
		public string CommonName { get; set; }
		public string SerialNumber { get; set; }

		public DistinguishedName(Asn1Node n) {
			/* Name:
			 * SET
			 *   SEQ (attr)
			 *     Object Identifier
			 *     Printable String || UTF8String
			 */
			if (n.MaskedTag == Asn1Tag.SEQUENCE) {
				for (int i=0; i < n.ChildNodeCount; i++) {
					Asn1Node tt = n.GetChildNode(i);
					if (tt.MaskedTag != Asn1Tag.SET || tt.ChildNodeCount != 1)
						throw new InvalidX509Data();

					tt = tt.GetChildNode(0);
					if (tt.MaskedTag != Asn1Tag.SEQUENCE || tt.ChildNodeCount != 2)
						throw new InvalidX509Data();

					Asn1Node oi = tt.GetChildNode(0);
					Asn1Node txt = tt.GetChildNode(1);

					if (oi.MaskedTag != Asn1Tag.OBJECT_IDENTIFIER ||
						!(
							(txt.MaskedTag == Asn1Tag.PRINTABLE_STRING) ||
							(txt.MaskedTag == Asn1Tag.UTF8_STRING)))
					{
						throw new InvalidX509Data();
					}
					var xoid = new LipingShare.LCLib.Asn1Processor.Oid();
					string oiName = xoid.Decode(oi.Data);
					var enc = new System.Text.UTF8Encoding();

					switch (oiName) {
					case "2.5.4.6": // countryName
						Country = enc.GetString(txt.Data);
						break;
					case "2.5.4.10": // organizationName
						Organization = enc.GetString(txt.Data);
						break;
					case "2.5.4.11": // organizationalUnit
						OrganizationalUnit = enc.GetString(txt.Data);
						break;
					case "2.5.4.3": // commonName
						CommonName = enc.GetString(txt.Data);
						break;
					case "2.5.4.5": // serial number
						SerialNumber = Asn1Util.ToHexString(txt.Data);
						break;
					case "2.5.4.46": // dnq
						Dnq = enc.GetString(txt.Data);
						break;
					case "2.5.4.8": // state
						State = enc.GetString(txt.Data);
						break;
					}
				}
			}
		}

		public bool Equals(DistinguishedName n2) {
			return this.Organization == n2.Organization &&
				this.OrganizationalUnit == n2.OrganizationalUnit &&
				this.Dnq == n2.Dnq &&
				this.Country == n2.Country &&
				this.State == n2.State &&
				this.CommonName == n2.CommonName;
		}

		public override string ToString() {
			return "CN: " + CommonName + "\n" +
				"ON: " + Organization + "\n" +
				"Unit Name: " + OrganizationalUnit + "\n" +
				"Country: " + Country;
		}
	}

	internal class X509Cert {
		public string SerialNumber { get; private set; }
		public DateTime ValidAfter { get; private set; }
		public DateTime ValidBefore { get; private set; }
		public RSAKey PubKey { get; private set; }
		public bool SelfSigned { get; private set; }
		public DistinguishedName Subject { get; private set; }
		public DistinguishedName Issuer { get; private set; }
		private Asn1Node TbsCertificate;
		public Asn1Node Signature { get; private set; }
		public byte[] rawTBSCertificate;

		public X509Cert(Asn1Node n) {
			ParseNode(n);
		}

		public X509Cert(byte[] data) {
			using (var stm = new System.IO.MemoryStream(data))
			{
				Asn1Parser parser = new Asn1Parser();
				parser.LoadData(stm);
				ParseNode(parser.RootNode);
			}
		}

		public bool CheckCertTime(DateTime time) {
			return time.CompareTo(ValidAfter) >=0 && time.CompareTo(ValidBefore) <= 0;
		}

		public bool CheckSignature(X509Cert signer) {
			if (Issuer.Equals(signer.Subject)) {
				return signer.PubKey.Verify(rawTBSCertificate, Signature.Data);
			}
			return false;
		}

		private void ParseNode(Asn1Node root) {
			if ((root.Tag & Asn1Tag.TAG_MASK) != Asn1Tag.SEQUENCE || root.ChildNodeCount != 3)
				throw new InvalidX509Data();



			// TBS cert
			TbsCertificate = root.GetChildNode(0);
			if (TbsCertificate.ChildNodeCount < 7)
				throw new InvalidX509Data();

			rawTBSCertificate = new byte[TbsCertificate.DataLength + 4];
			Array.Copy (root.Data, 0, rawTBSCertificate, 0, rawTBSCertificate.Length);

			// get the serial number
			Asn1Node sn = TbsCertificate.GetChildNode(1);
			if ((sn.Tag & Asn1Tag.TAG_MASK) != Asn1Tag.INTEGER)
				throw new InvalidX509Data();
			SerialNumber = Asn1Util.ToHexString(sn.Data);

			// get the issuer
			Issuer = new DistinguishedName(TbsCertificate.GetChildNode(3));

			// get the subject
			Subject = new DistinguishedName(TbsCertificate.GetChildNode(5));

			// get the dates
			Asn1Node validTimes = TbsCertificate.GetChildNode(4);
			if ((validTimes.Tag & Asn1Tag.TAG_MASK) != Asn1Tag.SEQUENCE || validTimes.ChildNodeCount != 2)
				throw new InvalidX509Data();
			ValidAfter = ParseTime(validTimes.GetChildNode(0));
			ValidBefore = ParseTime(validTimes.GetChildNode(1));

			// is this self signed?
			SelfSigned = Subject.Equals(Issuer);

			// get the pub key
			PubKey = new RSAKey(TbsCertificate.GetChildNode(6));

			// set the tbs cert & signature data for signature verification
			Signature = root.GetChildNode(2);
		}

		/**
		 * According to rfc5280, time should be specified in GMT:
		 * https://tools.ietf.org/html/rfc5280#section-4.1.2.5
		 */
		private DateTime ParseTime(Asn1Node n) {
			string time = (new System.Text.UTF8Encoding()).GetString(n.Data);

			if (!(time.Length == 13 || time.Length == 15))
				throw new InvalidTimeFormat();

			// only accept Zulu time
			if (time[time.Length - 1] != 'Z')
				throw new InvalidTimeFormat();

			int curIdx = 0;

			int year = 0;
			if (time.Length == 13) {
				year = Int32.Parse(time.Substring(0,2));
				if (year >= 50)
					year += 1900;
				else if (year < 50)
					year += 2000;
				curIdx += 2;
			} else {
				year = Int32.Parse(time.Substring(0,4));
				curIdx += 4;
			}

			int month = Int32.Parse(time.Substring(curIdx, 2)); curIdx += 2;
			int dom = Int32.Parse(time.Substring(curIdx, 2)); curIdx += 2;
			int hour = Int32.Parse(time.Substring(curIdx, 2)); curIdx += 2;
			int min = Int32.Parse(time.Substring(curIdx, 2)); curIdx += 2;
			int secs = Int32.Parse(time.Substring(curIdx, 2)); curIdx += 2;

			return new DateTime(year, month, dom, hour, min, secs, DateTimeKind.Utc);
		}
	}

    /// <summary>
    /// An IAP Security exception indicating some invalid time format.
    /// </summary>
    public class InvalidTimeFormat : IAPSecurityException {}

    /// <summary>
    /// An IAP Security exception indicating some invalid data for X509 certification checks.
    /// </summary>
	public class InvalidX509Data : IAPSecurityException {}
}
