using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using Shadowsocks.Controller;
using Shadowsocks.Encryption.Exception;
using Shadowsocks.Properties;
using Shadowsocks.Util;

namespace Shadowsocks.Encryption
{
    // XXX: only for OpenSSL 1.1.0 and higher
    public static class OpenSSL
    {
        private const string DLLNAME = "libsscrypto.dll";

        public const int OPENSSL_ENCRYPT = 1;
        public const int OPENSSL_DECRYPT = 0;

        public const int EVP_CTRL_AEAD_SET_IVLEN = 0x9;
        public const int EVP_CTRL_AEAD_GET_TAG = 0x10;
        public const int EVP_CTRL_AEAD_SET_TAG = 0x11;

        static OpenSSL()
        {
            string dllPath = Utils.GetTempPath(DLLNAME);
            try
            {
                FileManager.UncompressFile(dllPath, Resources.libsscrypto_dll);
            }
            catch (IOException)
            {
            }
            catch (System.Exception e)
            {
                Logging.LogUsefulException(e);
            }
            LoadLibrary(dllPath);
        }

        public static IntPtr GetCipherInfo(string cipherName)
        {
            var name = Encoding.ASCII.GetBytes(cipherName);
            Array.Resize(ref name, name.Length + 1);
            return EVP_get_cipherbyname(name);
        }

        /// <summary>
        /// Need init cipher context after EVP_CipherFinal_ex to reuse context
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="cipherType"></param>
        /// <param name="nonce"></param>
        public static void SetCtxNonce(IntPtr ctx, byte[] nonce, bool isEncrypt)
        {
            var ret = EVP_CipherInit_ex(ctx, IntPtr.Zero,
                IntPtr.Zero, null,
                nonce,
                isEncrypt ? OPENSSL_ENCRYPT : OPENSSL_DECRYPT);
            if (ret != 1) throw new System.Exception("openssl: fail to set AEAD nonce");
        }

        public static void AEADGetTag(IntPtr ctx, byte[] tagbuf, int taglen)
        {
            IntPtr tagBufIntPtr = IntPtr.Zero;
            try
            {
                tagBufIntPtr = Marshal.AllocHGlobal(taglen);
                var ret = EVP_CIPHER_CTX_ctrl(ctx,
                    EVP_CTRL_AEAD_GET_TAG, taglen, tagBufIntPtr);
                if (ret != 1) throw new CryptoErrorException("openssl: fail to get AEAD tag");
                // take tag from unmanaged memory
                Marshal.Copy(tagBufIntPtr, tagbuf, 0, taglen);
            }
            finally
            {
                if (tagBufIntPtr != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(tagBufIntPtr);
                }
            }
        }

        public static void AEADSetTag(IntPtr ctx, byte[] tagbuf, int taglen)
        {
            IntPtr tagBufIntPtr = IntPtr.Zero;
            try
            {
                // allocate unmanaged memory for tag
                tagBufIntPtr = Marshal.AllocHGlobal(taglen);

                // copy tag to unmanaged memory
                Marshal.Copy(tagbuf, 0, tagBufIntPtr, taglen);

                var ret = EVP_CIPHER_CTX_ctrl(ctx,
                    EVP_CTRL_AEAD_SET_TAG, taglen, tagBufIntPtr);

                if (ret != 1) throw new CryptoErrorException("openssl: fail to set AEAD tag");

            }
            finally
            {
                if (tagBufIntPtr != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(tagBufIntPtr);
                }
            }
        }

        [DllImport("Kernel32.dll")]
        private static extern IntPtr LoadLibrary(string path);

        [SuppressUnmanagedCodeSecurity]
        [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr EVP_CIPHER_CTX_new();

        [SuppressUnmanagedCodeSecurity]
        [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)]
        public static extern void EVP_CIPHER_CTX_free(IntPtr ctx);

        [SuppressUnmanagedCodeSecurity]
        [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)]
        public static extern int EVP_CIPHER_CTX_reset(IntPtr ctx);

        [SuppressUnmanagedCodeSecurity]
        [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)]
        public static extern int EVP_CipherInit_ex(IntPtr ctx, IntPtr type,
            IntPtr impl, byte[] key, byte[] iv, int enc);

        [SuppressUnmanagedCodeSecurity]
        [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)]
        public static extern int EVP_CipherUpdate(IntPtr ctx, byte[] outb,
            out int outl, byte[] inb, int inl);

        [SuppressUnmanagedCodeSecurity]
        [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)]
        public static extern int EVP_CipherFinal_ex(IntPtr ctx, byte[] outm, ref int outl);

        [SuppressUnmanagedCodeSecurity]
        [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)]
        public static extern int EVP_CIPHER_CTX_set_padding(IntPtr x, int padding);

        [SuppressUnmanagedCodeSecurity]
        [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)]
        public static extern int EVP_CIPHER_CTX_set_key_length(IntPtr x, int keylen);

        [SuppressUnmanagedCodeSecurity]
        [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)]
        public static extern int EVP_CIPHER_CTX_ctrl(IntPtr ctx, int type, int arg, IntPtr ptr);

        /// <summary>
        /// simulate NUL-terminated string
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        [SuppressUnmanagedCodeSecurity]
        [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr EVP_get_cipherbyname(byte[] name);
    }
}