using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace Strata.Base.Internal.EncryptionUtils
{

    #region  Hash 

    /// <summary>
    /// Hash functions are fundamental to modern cryptography. These functions map binary 
    /// strings of an arbitrary length to small binary strings of a fixed length, known as 
    /// hash values. A cryptographic hash function has the property that it is computationally
    /// infeasible to find two distinct inputs that hash to the same value. Hash functions 
    /// are commonly used with digital signatures and for data integrity.
    /// </summary>
    public class Hasher
    {

        /// <summary>
        /// Type of hash; some are security oriented, others are fast and simple
        /// </summary>
        internal enum Provider
        {
            /// <summary>
            /// Secure Hashing Algorithm provider, SHA-2 variant, 256-bit
            /// </summary>
            SHA256,
            /// <summary>
            /// Secure Hashing Algorithm provider, SHA-2 variant, 384-bit
            /// </summary>
            SHA384,
            /// <summary>
            /// Secure Hashing Algorithm provider, SHA-2 variant, 512-bit
            /// </summary>
            SHA512
        }

        private HashAlgorithm _Hash;
        private Data _HashValue = new Data();

        internal Hasher()
        {
        }

        /// <summary>
        /// Instantiate a new hash of the specified type
        /// </summary>
        internal Hasher(Provider p)
        {
            switch (p)
            {
                case Provider.SHA256:
                    {
                        _Hash = SHA256.Create();
                        break;
                    }
                case Provider.SHA384:
                    {
                        _Hash = SHA384.Create();
                        break;
                    }
                case Provider.SHA512:
                    {
                        _Hash = SHA512.Create();
                        break;
                    }

                default:
                    {
                        _Hash = SHA256.Create();  // Default to SHA256 for unknown providers
                        break;
                    }
            }
        }

        /// <summary>
        /// Returns the previously calculated hash
        /// </summary>
        internal Data Value
        {
            get
            {
                return _HashValue;
            }
        }

        /// <summary>
        /// Calculates hash on a stream of arbitrary length
        /// </summary>
        internal Data Calculate(ref Stream s)
        {
            _HashValue.Bytes = _Hash.ComputeHash(s);
            return _HashValue;
        }

        /// <summary>
        /// Calculates hash for fixed length <see cref="Data"/>
        /// </summary>
        internal Data Calculate(Data d)
        {
            return CalculatePrivate(d.Bytes);
        }

        /// <summary>
        /// Calculates hash for a string with a prefixed salt value. 
        /// A "salt" is random data prefixed to every hashed value to prevent 
        /// common dictionary attacks.
        /// </summary>
        internal Data Calculate(Data d, Data salt)
        {
            var nb = new byte[(d.Bytes.Length + salt.Bytes.Length)];
            salt.Bytes.CopyTo(nb, 0);
            d.Bytes.CopyTo(nb, salt.Bytes.Length);
            return CalculatePrivate(nb);
        }

        /// <summary>
        /// Calculates hash for an array of bytes
        /// </summary>
        private Data CalculatePrivate(byte[] b)
        {
            _HashValue.Bytes = _Hash.ComputeHash(b);
            return _HashValue;
        }

    }
    #endregion

    #region  Symmetric 

    /// <summary>
    /// Symmetric encryption uses a single key to encrypt and decrypt. 
    /// Both parties (encryptor and decryptor) must share the same secret key.
    /// </summary>
    internal class SymmetricEncryptor
    {

        private const string _DefaultIntializationVector = "%1Az=-@qT";
        private const int _BufferSize = 2048;

        internal enum Provider
        {
            /// <summary>
            /// Advanced Encryption Standard (AES) provider
            /// </summary>
            AES
        }

        private Data _data;
        private Data _key;
        private Data _iv;
        private SymmetricAlgorithm _crypto;
        private byte[] _EncryptedBytes;
        private bool _UseDefaultInitializationVector;

        private SymmetricEncryptor()
        {
        }

        /// <summary>
        /// Instantiates a new symmetric encryption object using the specified provider.
        /// </summary>
        internal SymmetricEncryptor(Provider provider, bool useDefaultInitializationVector = true)
        {
            _crypto = Aes.Create();  // Always use AES as it's the most secure option

            // -- make sure key and IV are always set, no matter what
            Key = RandomKey();
            if (useDefaultInitializationVector)
            {
                IntializationVector = new Data(_DefaultIntializationVector);
            }
            else
            {
                IntializationVector = RandomInitializationVector();
            }
        }

        /// <summary>
        /// Key size in bytes. We use the default key size for any given provider; if you 
        /// want to force a specific key size, set this property
        /// </summary>
        internal int KeySizeBytes
        {
            get
            {
                return _crypto.KeySize / 8;
            }
            set
            {
                _crypto.KeySize = value * 8;
                _key.MaxBytes = value;
            }
        }

        /// <summary>
        /// Key size in bits. We use the default key size for any given provider; if you 
        /// want to force a specific key size, set this property
        /// </summary>
        internal int KeySizeBits
        {
            get
            {
                return _crypto.KeySize;
            }
            set
            {
                _crypto.KeySize = value;
                _key.MaxBits = value;
            }
        }

        /// <summary>
        /// The key used to encrypt/decrypt data
        /// </summary>
        internal Data Key
        {
            get
            {
                return _key;
            }
            set
            {
                _key = value;
                _key.MaxBytes = _crypto.LegalKeySizes[0].MaxSize / 8;
                _key.MinBytes = _crypto.LegalKeySizes[0].MinSize / 8;
                _key.StepBytes = _crypto.LegalKeySizes[0].SkipSize / 8;
            }
        }

        /// <summary>
        /// Using the default Cipher Block Chaining (CBC) mode, all data blocks are processed using
        /// the value derived from the previous block; the first data block has no previous data block
        /// to use, so it needs an InitializationVector to feed the first block
        /// </summary>
        internal Data IntializationVector
        {
            get
            {
                return _iv;
            }
            set
            {
                _iv = value;
                _iv.MaxBytes = _crypto.BlockSize / 8;
                _iv.MinBytes = _crypto.BlockSize / 8;
            }
        }

        /// <summary>
        /// generates a random Initialization Vector, if one was not provided
        /// </summary>
        internal Data RandomInitializationVector()
        {
            _crypto.GenerateIV();
            var d = new Data(_crypto.IV);
            return d;
        }

        /// <summary>
        /// generates a random Key, if one was not provided
        /// </summary>
        internal Data RandomKey()
        {
            _crypto.GenerateKey();
            var d = new Data(_crypto.Key);
            return d;
        }

        /// <summary>
        /// Ensures that _crypto object has valid Key and IV
        /// prior to any attempt to encrypt/decrypt anything
        /// </summary>
        private void ValidateKeyAndIv(bool isEncrypting)
        {
            if (_key.IsEmpty)
            {
                if (isEncrypting)
                {
                    _key = RandomKey();
                }
                else
                {
                    throw new CryptographicException("No key was provided for the decryption operation!");
                }
            }
            if (_iv.IsEmpty)
            {
                if (isEncrypting)
                {
                    _iv = RandomInitializationVector();
                }
                else
                {
                    throw new CryptographicException("No initialization vector was provided for the decryption operation!");
                }
            }
            try
            {
                _crypto.Key = _key.Bytes;
                _crypto.IV = _iv.Bytes;
            }
            catch (CryptographicException ex)
            {
                throw new CryptographicException("Invalid key or initialization vector.", ex);
            }
        }

        /// <summary>
        /// Encrypts the specified Data using provided key
        /// </summary>
        internal Data Encrypt(Data d, Data key)
        {
            Key = key;
            return Encrypt(d);
        }

        /// <summary>
        /// Encrypts the specified Data using preset key and preset initialization vector
        /// </summary>
        internal Data Encrypt(Data d)
        {
            var ms = new MemoryStream();

            ValidateKeyAndIv(true);

            var cs = new CryptoStream(ms, _crypto.CreateEncryptor(), CryptoStreamMode.Write);
            cs.Write(d.Bytes, 0, d.Bytes.Length);
            cs.Close();
            ms.Close();

            return new Data(ms.ToArray());
        }

        /// <summary>
        /// Encrypts the stream to memory using provided key and provided initialization vector
        /// </summary>
        internal Data Encrypt(Stream s, Data key, Data iv)
        {
            IntializationVector = iv;
            Key = key;
            return Encrypt(s);
        }

        /// <summary>
        /// Encrypts the stream to memory using specified key
        /// </summary>
        internal Data Encrypt(Stream s, Data key)
        {
            Key = key;
            return Encrypt(s);
        }

        /// <summary>
        /// Encrypts the specified stream to memory using preset key and preset initialization vector
        /// </summary>
        internal Data Encrypt(Stream s)
        {
            var ms = new MemoryStream();
            var b = new byte[2049];
            int i;

            ValidateKeyAndIv(true);

            var cs = new CryptoStream(ms, _crypto.CreateEncryptor(), CryptoStreamMode.Write);
            i = s.Read(b, 0, _BufferSize);
            while (i > 0)
            {
                cs.Write(b, 0, i);
                i = s.Read(b, 0, _BufferSize);
            }

            cs.Close();
            ms.Close();

            return new Data(ms.ToArray());
        }

        /// <summary>
        /// Decrypts the specified data using provided key and preset initialization vector
        /// </summary>
        internal Data Decrypt(Data encryptedData, Data key)
        {
            Key = key;
            return Decrypt(encryptedData);
        }

        /// <summary>
        /// Decrypts the specified stream using provided key and preset initialization vector
        /// </summary>
        internal Data Decrypt(Stream encryptedStream, Data key)
        {
            Key = key;
            return Decrypt(encryptedStream);
        }

        /// <summary>
        /// Decrypts the specified stream using preset key and preset initialization vector
        /// </summary>
        internal Data Decrypt(Stream encryptedStream)
        {
            var ms = new MemoryStream();
            var b = new byte[2049];

            ValidateKeyAndIv(false);
            var cs = new CryptoStream(encryptedStream, _crypto.CreateDecryptor(), CryptoStreamMode.Read);

            int i;
            i = cs.Read(b, 0, _BufferSize);

            while (i > 0)
            {
                ms.Write(b, 0, i);
                i = cs.Read(b, 0, _BufferSize);
            }
            cs.Close();
            ms.Close();

            return new Data(ms.ToArray());
        }

        /// <summary>
        /// Decrypts the specified data using preset key and preset initialization vector
        /// </summary>
        internal Data Decrypt(Data encryptedData)
        {
            using (var ms = new MemoryStream(encryptedData.Bytes, 0, encryptedData.Bytes.Length))
            {
                ValidateKeyAndIv(false);
                using (var cs = new CryptoStream(ms, _crypto.CreateDecryptor(), CryptoStreamMode.Read))
                {
                    using (var outputMs = new MemoryStream())
                    {
                        try
                        {
                            cs.CopyTo(outputMs);
                            return new Data(outputMs.ToArray());
                        }
                        catch (CryptographicException ex)
                        {
                            throw new CryptographicException("Unable to decrypt data. The provided key may be invalid.", ex);
                        }
                    }
                }
            }
        }

    }

    #endregion

    #region  Data 

    /// <summary>
    /// represents Hex, Byte, Base64, or String data to encrypt/decrypt;
    /// use the .Text property to set/get a string representation 
    /// use the .Hex property to set/get a string-based Hexadecimal representation 
    /// </summary>
    internal class Data
    {
        private byte[] _b = null;
        private int _MaxBytes = 0;
        private int _MinBytes = 0;
        private int _StepBytes = 0;

        /// <summary>
        /// Determines the default text encoding for this Data instance
        /// </summary>
        private Encoding _encoding = Encoding.UTF8;

        /// <summary>
        /// Creates new, empty encryption data
        /// </summary>
        internal Data()
        {
        }

        /// <summary>
        /// Creates new encryption data with the specified byte array
        /// </summary>
        internal Data(byte[] b)
        {
            _b = b;
        }

        /// <summary>
        /// Creates new encryption data with the specified string; 
        /// will be converted to byte array using UTF8 encoding
        /// </summary>
        internal Data(string s)
        {
            if (s is null)
            {
                throw new ArgumentNullException(nameof(s));
            }
            Text = s;
        }

        /// <summary>
        /// Creates new encryption data using the specified string and the 
        /// specified encoding to convert the string to a byte array.
        /// </summary>
        internal Data(string s, Encoding encoding)
        {
            if (s is null)
            {
                throw new ArgumentNullException(nameof(s));
            }
            if (encoding is null)
            {
                throw new ArgumentNullException(nameof(encoding));
            }
            _encoding = encoding;
            Text = s;
        }

        /// <summary>
        /// returns true if no data is present
        /// </summary>
        internal bool IsEmpty
        {
            get
            {
                if (_b is null)
                {
                    return true;
                }
                if (_b.Length == 0)
                {
                    return true;
                }
                return false;
            }
        }

        /// <summary>
        /// allowed step interval, in bytes, for this data; if 0, no limit
        /// </summary>
        internal int StepBytes
        {
            get
            {
                return _StepBytes;
            }
            set
            {
                _StepBytes = value;
            }
        }

        /// <summary>
        /// allowed step interval, in bits, for this data; if 0, no limit
        /// </summary>
        internal int StepBits
        {
            get
            {
                return _StepBytes * 8;
            }
            set
            {
                _StepBytes = value / 8;
            }
        }

        /// <summary>
        /// minimum number of bytes allowed for this data; if 0, no limit
        /// </summary>
        internal int MinBytes
        {
            get
            {
                return _MinBytes;
            }
            set
            {
                _MinBytes = value;
            }
        }

        /// <summary>
        /// minimum number of bits allowed for this data; if 0, no limit
        /// </summary>
        internal int MinBits
        {
            get
            {
                return _MinBytes * 8;
            }
            set
            {
                _MinBytes = value / 8;
            }
        }

        /// <summary>
        /// maximum number of bytes allowed for this data; if 0, no limit
        /// </summary>
        internal int MaxBytes
        {
            get
            {
                return _MaxBytes;
            }
            set
            {
                _MaxBytes = value;
            }
        }

        /// <summary>
        /// maximum number of bits allowed for this data; if 0, no limit
        /// </summary>
        internal int MaxBits
        {
            get
            {
                return _MaxBytes * 8;
            }
            set
            {
                _MaxBytes = value / 8;
            }
        }

        /// <summary>
        /// Returns the byte representation of the data; 
        /// This will be padded to MinBytes and trimmed to MaxBytes as necessary!
        /// </summary>
        internal byte[] Bytes
        {
            get
            {
                if (_b is null)
                {
                    return Array.Empty<byte>();
                }

                if (_MaxBytes > 0 && _b.Length > _MaxBytes)
                {
                    var b = new byte[_MaxBytes];
                    Array.Copy(_b, b, b.Length);
                    _b = b;
                }

                if (_MinBytes > 0 && _b.Length < _MinBytes)
                {
                    var b = new byte[_MinBytes];
                    Array.Copy(_b, b, _b.Length);
                    _b = b;
                }

                return _b;
            }
            set
            {
                _b = value;
            }
        }

        /// <summary>
        /// Sets or returns text representation of bytes using UTF8 encoding
        /// </summary>
        internal string Text
        {
            get
            {
                if (_b is null || _b.Length == 0)
                {
                    return string.Empty;
                }

                try
                {
                    return _encoding.GetString(_b);
                }
                catch (Exception ex)
                {
                    // If there's an encoding error, try to salvage what we can
                    int i = Array.IndexOf(_b, (byte)0);
                    if (i >= 0)
                    {
                        return _encoding.GetString(_b, 0, i);
                    }
                    throw;
                }
            }
            set
            {
                if (value is null)
                {
                    _b = Array.Empty<byte>();
                }
                else
                {
                    _b = _encoding.GetBytes(value);
                }
            }
        }

        /// <summary>
        /// Sets or returns Hex string representation of this data
        /// </summary>
        internal string Hex
        {
            get
            {
                return Utils.ToHex(_b);
            }
            set
            {
                _b = Utils.FromHex(value);
            }
        }

        /// <summary>
        /// Sets or returns Base64 string representation of this data
        /// </summary>
        internal string Base64
        {
            get
            {
                return Utils.ToBase64(_b);
            }
            set
            {
                _b = Utils.FromBase64(value);
            }
        }

    }

    #endregion

    #region  Utils 

    /// <summary>
    /// Friend class for shared utility methods used by multiple Encryption classes
    /// </summary>
    internal class Utils
    {

        /// <summary>
        /// converts an array of bytes to a string Hex representation
        /// </summary>
        internal static string ToHex(byte[] ba)
        {
            if (ba is null || ba.Length == 0)
            {
                return "";
            }
            const string HexFormat = "{0:X2}";
            var sb = new StringBuilder();
            foreach (byte b in ba)
                sb.Append(string.Format(HexFormat, b));
            return sb.ToString();
        }

        /// <summary>
        /// converts from a string Hex representation to an array of bytes
        /// </summary>
        internal static byte[] FromHex(string hexEncoded)
        {
            if (hexEncoded is null || hexEncoded.Length == 0)
            {
                return null;
            }
            try
            {
                int l = Convert.ToInt32(hexEncoded.Length / 2d);
                var b = new byte[l];
                for (int i = 0, loopTo = l - 1; i <= loopTo; i++)
                    b[i] = Convert.ToByte(hexEncoded.Substring(i * 2, 2), 16);
                return b;
            }
            catch (Exception ex)
            {
                throw new FormatException("The provided string does not appear to be Hex encoded:" + Environment.NewLine + hexEncoded + Environment.NewLine, ex);
            }
        }

        /// <summary>
        /// converts from a string Base64 representation to an array of bytes
        /// </summary>
        internal static byte[] FromBase64(string base64Encoded)
        {
            if (base64Encoded is null || base64Encoded.Length == 0)
            {
                return null;
            }
            try
            {
                return Convert.FromBase64String(base64Encoded);
            }
            catch (FormatException ex)
            {
                throw new FormatException("The provided string does not appear to be Base64 encoded:" + Environment.NewLine + base64Encoded + Environment.NewLine, ex);
            }
        }

        /// <summary>
        /// converts from an array of bytes to a string Base64 representation
        /// </summary>
        internal static string ToBase64(byte[] b)
        {
            if (b is null || b.Length == 0)
            {
                return "";
            }
            return Convert.ToBase64String(b);
        }

    }

    #endregion

}