using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using System.Xml;

namespace EduNetworkBuilder
{
    /// <summary>
    /// Copied from https://msdn.microsoft.com/en-us/library/system.security.cryptography.xml.encryptedxml(v=vs.110).aspx
    /// </summary>
    class TrippleDESDocumentEncryption
    {
        protected XmlDocument docValue;
        protected TripleDES algValue;

        /// <summary>
        /// The characters we use for passwords and salts.
        /// </summary>
        protected const string PWChars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ234567890_.!#$^*";

        public TrippleDESDocumentEncryption(XmlDocument Doc, TripleDES Key)
        {
            if (Doc != null)
            {
                docValue = Doc;
            }
            else
            {
                throw new ArgumentNullException("Doc");
            }

            if (Key != null)
            {

                algValue = Key;
            }
            else
            {
                throw new ArgumentNullException("Key");
            }
        }

        public XmlDocument Doc { set { docValue = value; } get { return docValue; } }
        public TripleDES Alg { set { algValue = value; } get { return algValue; } }

        public void Clear()
        {
            if (algValue != null)
            {
                algValue.Clear();
            }
            else
            {
                throw new Exception("No TripleDES key was found to clear.");
            }
        }

        public void Encrypt(string Element)
        {
            // Find the element by name and create a new
            // XmlElement object.
            XmlElement inputElement = docValue.GetElementsByTagName(Element)[0] as XmlElement;

            // If the element was not found, throw an exception.
            if (inputElement == null)
            {
                throw new Exception("The element was not found.");
            }

            // Create a new EncryptedXml object.
            EncryptedXml exml = new EncryptedXml(docValue);

            // Encrypt the element using the symmetric key.
            byte[] rgbOutput = exml.EncryptData(inputElement, algValue, false);

            // Create an EncryptedData object and populate it.
            EncryptedData ed = new EncryptedData();

            // Specify the namespace URI for XML encryption elements.
            ed.Type = Element + " " + EncryptedXml.XmlEncElementUrl;

            // Specify the namespace URI for the TrippleDES algorithm.
            ed.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncTripleDESUrl);

            // Create a CipherData element.
            ed.CipherData = new CipherData();

            // Set the CipherData element to the value of the encrypted XML element.
            ed.CipherData.CipherValue = rgbOutput;

            // Replace the plaintext XML elemnt with an EncryptedData element.
            EncryptedXml.ReplaceElement(inputElement, ed, false);
        }

        XmlElement ChooseBestElement(string preferType)
        {
            XmlNodeList xnl = docValue.GetElementsByTagName("EncryptedData");
            foreach (XmlElement one in xnl.OfType<XmlElement>())
            {
                string TypeAttr = one.GetAttribute("Type");
                if (TypeAttr != null && TypeAttr.StartsWith(preferType))
                    return one;
            }
            return xnl[0] as XmlElement;
        }

        public void Decrypt(string preferType)
        {

            // XmlElement object.
            XmlElement encryptedElement = ChooseBestElement(preferType);

            // If the EncryptedData element was not found, throw an exception.
            if (encryptedElement == null)
            {
                throw new Exception("The EncryptedData element was not found.");
            }

            // Create an EncryptedData object and populate it.
            EncryptedData ed = new EncryptedData();
            ed.LoadXml(encryptedElement);

            // Create a new EncryptedXml object.
            EncryptedXml exml = new EncryptedXml();

            // Decrypt the element using the symmetric key.
            try
            {
                //Do the decryption
                byte[] rgbOutput = exml.DecryptData(ed, algValue);
                // Replace the encryptedData element with the plaintext XML elemnt.
                exml.ReplaceData(encryptedElement, rgbOutput);
            }
            catch
            {
                //The decryption failed.
                throw new LoginException("Key/Salt unable to decrypt.");
            }
        }

        protected static string GenCharString(Random RanGen, int length)
        {
            int next;
            string result = "";
            for(int i=0; i< length; i++)
            {
                next = RanGen.Next(PWChars.Length);
                result += PWChars[next];
            }
            return result;
        }

        public static string GenSalt(Random RanGen)
        {
            return GenSalt(RanGen, NB.DefaultSaltLen);
        }
        public static string GenSalt(Random RanGen, int length)
        {
            return GenCharString(RanGen, length);
        }

        public static string GenUserPW(Random RanGen)
        {
            return GenUserPW(RanGen, NB.DefaultPasswordLen);
        }
        public static string GenMachinePW(Random RanGen)
        {
            return GenUserPW(RanGen, NB.DefaultMachinePasswordLen);
        }

        public static string GenUserPW(Random RanGen, int length)
        {
            return GenCharString(RanGen, length);
        }

        public static TripleDES GenKey(string password, string salt)
        {
            TripleDESCryptoServiceProvider tDESkey = new TripleDESCryptoServiceProvider();
            byte[] saltBytes = Encoding.UTF8.GetBytes(salt);
            var p = new Rfc2898DeriveBytes(password, saltBytes);
            tDESkey.IV = p.GetBytes(tDESkey.BlockSize / 8);
            tDESkey.Key = p.GetBytes(tDESkey.KeySize / 8);
            return tDESkey;
        }

        public void SetKey(string password, string salt)
        {            
            algValue = GenKey(password, salt);
        }

    }

    /// <summary>
    /// Create a simple login exception that we can throw when we try to decrypt and it fails
    /// </summary>
    public class LoginException : System.Exception
    {
        public LoginException() : base()
        { }

        public LoginException(String message) : base(message)
        { }

        public LoginException(String message, Exception innerException) : base(message, innerException)
        { }
    }
}
