using System;
using System.Collections.Generic;
using System.Text;

namespace SuperCryptEX
{
    class Class_ElGamal
    {
        #region Parameters

        // Parameters
        public int var_Q, var_PrimitiveRootOfP, var_p, var_x, var_g, var_y, var_k, var_a;

        #endregion

        #region Constructor

        // ElGamal Constructor
        public Class_ElGamal() { }

        #endregion

        #region Core Functions

        // Core Function - Generate Sepasang Random Key
        public void Go_GenerateRandomKey()
        {
            var_Q = var_PrimitiveRootOfP = var_g = var_x = var_y = var_k = 0;

            // 1. Menentukan Bilangan Prima Acak Antara [1000 - 3000]
            do
            {
                var_p = ChooseBigRandomNumber();
            }
            while (TestPrimeElement(var_p) == false);

            // 2. Menentukan Bilangan Acak Q Sehingga Q Merupakan Divisor dari p-1 (GCD)
            do
            {
                // Menentukan Bilangan Random Q dengan Min = 2 & Max = p-1
                Random randomQ = new Random();
                var_Q = randomQ.Next(2, var_p - 1);
            }
            // Bilangan Q Harus Prima dan (p - 1) mod Q Harus = 0
            while (!TestPrimeElement(var_p) || !TestTheEstimateOfThePNumber(var_p, var_Q));

            // 3. Menentukan Sebuah Bilangan yang Merupakan Primitive Root dari p
            do
            {
                Random randomGtoA = new Random();
                var_PrimitiveRootOfP = randomGtoA.Next(2, var_p - 1);
            }
            while (!TestPrimitiveElement(var_PrimitiveRootOfP, var_p, var_Q));

            // 4. Generate - Variabel "g", Dimana g < p
            var_g = PowerAndModulo(var_PrimitiveRootOfP, var_p - 1 / var_Q, var_p);

            // 5. Generate - Variabel "x"
            do
            {
                // Menentukan Bilangan Random x dengan Min = 2 & Max = p-2
                Random randomX = new Random();
                var_x = randomX.Next(2, var_p - 2);
            }
            while (var_x == var_Q || var_x == var_PrimitiveRootOfP);

            // 6. Generate - Variabel "y", Dimana y = (g ^ x) mod p
            var_y = PowerAndModulo(var_g, var_x, var_p);

            // 7. Generate - Variabel "k"
            do
            {
                Random rdEsoK = new Random();
                var_k = rdEsoK.Next(2, var_p - 2);
            }
            while (var_k == var_x || var_k == var_g || var_k == var_Q || var_k == var_PrimitiveRootOfP || !TestCoPrime(var_k, var_p - 1));

            // 8. Generate - Variabel "a" (Kalkukasi -> a = g ^ k mod p)
            var_a = PowerAndModulo(var_g, var_k, var_p);
        }

        // Core Function - Encrypt Input Message dengan Public Key (p, g, y, k)
        public string Go_Encrypt(string inputText, int p, int g, int y, int k)
        {
            // Konversi Input String ke Byte ASCII
            byte[] inputByte = Encoding.ASCII.GetBytes(inputText);

            // Konversi Input Byte ke Base64
            string inputBase64 = Convert.ToBase64String(inputByte);

            // Pecah Setiap Karakter Base64 Menjadi Integer Array
            int[] message = new int[inputBase64.Length];
            for (int i = 0; i < inputBase64.Length; i++)
            {
                message[i] = inputBase64[i];
            }

            // Buat Variabel Output (Int)
            int[] C2Int = new int[message.Length];

            // Lakukan Proses Enkripsi
            for (int i = 0; i < message.Length; i++)
            {
                // Kalkulasi -> C2 = (P[i] * (y ^ k)) mod p
                C2Int[i] = (message[i] % p) * PowerAndModulo(y, k, p) % p;
            }

            // Gabungkan Kembali Hasil Enkripsi Setiap Karakter Menjadi Satu Kesatuan String Base64
            string C2String = string.Empty;
            for (int i = 0; i < C2Int.Length; i++)
            {
                C2String += (char)C2Int[i];
            }

            // Konversi Output String ke Byte Unicode
            byte[] encryptedByte = Encoding.Unicode.GetBytes(C2String);

            // Konversi Output Byte ke Base64
            string encryptedText = Convert.ToBase64String(encryptedByte);

            return encryptedText;
        }

        // Core Function - Decrypt Input Cipher dengan Private Key (p, x, a)
        public string Go_Decrypt(string inputCipher, int p, int x, int a)
        {
            // Konversi Input dari Base64 ke Byte
            byte[] inputByte = Convert.FromBase64String(inputCipher);

            // Konversi Input Byte ke String Base64
            string inputBase64 = Encoding.Unicode.GetString(inputByte);

            // Pecah Setiap Karakter Base64 Menjadi Integer Array
            int[] cipher = new int[inputBase64.Length];
            for (int i = 0; i < inputBase64.Length; i++)
            {
                cipher[i] = inputBase64[i];
            }

            // Buat Variabel Output (Int)
            int[] messageInt = new int[cipher.Length];

            /* Dekripsi Bisa Dilakukan dengan 2 Cara :
             * [#] Cara 1 :
             * s = a ^ (p - 1 - x) mod p
             * m = (s * cipher[i]) mod p
             * 
             * [#] Cara 2 :
             * s = (a ^ x) mod p
             * m = (s ^ (p - 2) * cipher[i]) mod p
             * 
             * Cara 1 dan Cara 2 Menghasilkan Output yang Sama
             */

            // Kalkulasi -> s = a ^ (p - 1 - x) mod p
            int var_s = PowerAndModulo(a, (p - (1 + x)), p);

            // Lakukan Proses Dekripsi
            for (int i = 0; i < messageInt.Length; i++)
            {
                // Kalkulasi -> m = (s * cipher[i]) mod p
                messageInt[i] = (var_s * cipher[i]) % p;
            }

            // Gabungkan Kembali Hasil Enkripsi Setiap Karakter Menjadi Satu Kesatuan String Base64
            string messageString = string.Empty;
            for (int i = 0; i < messageInt.Length; i++)
            {
                messageString += (char)messageInt[i];
            }

            // Konversi Output dari Base64 ke Byte
            byte[] decryptedByte = Convert.FromBase64String(messageString);

            // Konversi Output dari Byte ASCII ke String PlainText
            string decryptedText = Encoding.ASCII.GetString(decryptedByte);

            return decryptedText;
        }

        #endregion

        #region Functions

        // General Function - Cek Validitas Keypair
        public bool General_CheckKeyValidity(int p, int x, int g, int y, int k, int a)
        {
            /* Return Details (If Returns Int) :
             * 0 = No Errors!
             * 1 = Error [p] Variable
             * 2 = Error [x] Variable
             * 3 = Error [g] Variable
             * 4 = Error [y] Variable
             * 5 = Error [k] Variable
             * 6 = Error [a] Variable
             */

            // Cek Semua Parameter != 0
            if (General_CheckPrivateKey(p, x, a) == false || General_CheckPublicKey(p, g, y, k) == false)
            {
                return false;
            }

            // Cek Bilangan Prima atau Bukan
            if (TestPrimeElement(p) == false)
            {
                return false;
            }

            // Cek Apakah p < 1000
            if (p < 1000)
            {
                return false;
            }

            return true;
        }

        // General Function - Set Public Key
        public void General_SetPublicKey(int p, int g, int y, int k)
        {
            var_p = p;
            var_g = g;
            var_y = y;
            var_k = k;
        }

        // General Function - Set Private Key
        public void General_SetPrivateKey(int p, int x, int a)
        {
            var_p = p;
            var_x = x;
            var_a = a;
        }

        // General Function - Cek Apakah Public Key Berhasil Dimasukkan ke Variabel (p, g, y, dan k)
        public bool General_CheckPublicKey(int p, int g, int y, int k)
        {
            if (p != 0 && g != 0 && y != 0 && k != 0)
                return true;
            else
                return false;
        }

        // General  Function - Cek Apakah Private Key Berhasil Dimasukkan ke Variabel (p, x, dan a)
        public bool General_CheckPrivateKey(int p, int x, int a)
        {
            if (p != 0 && x != 0 && a != 0)
                return true;
            else
                return false;
        }

        // General Function - Reset Semua Variabel
        public void General_ClearVariables()
        {
            var_p =
                var_Q =
                var_PrimitiveRootOfP =
                var_g =
                var_x =
                var_y =
                var_k =
                var_a =
                0;
        }

        // Function - Generate Bilangan Random
        public int ChooseBigRandomNumber()
        {
            Random random = new Random();

            // Bilangan Random Antara 1000 - 3000
            return random.Next(1000, 3000);
        }

        // Function - Kalkulasikan Pangkat dan Modulo ((number ^ power) % modulo)
        public int PowerAndModulo(int number, int power, int modulo)
        {
            // Menggunakan "Square Multiplication Algorithm" Karena Memproses Bilangan yang Sangat Besar

            // Convert ke Binary
            int[] a = new int[100];
            int k = 0;
            do
            {
                a[k] = power % 2;
                power = power / 2;
                k++;
            }
            while (power != 0);

            // Kalkulasikan Hasil Pangkat dan Modulo
            int result = 1;

            for (int i = k - 1; i >= 0; i--)
            {
                result = (result * result) % modulo;
                if (a[i] == 1)
                {
                    result = (result * number) % modulo;
                }
            }

            return result;
        }

        // Function - Cek Bilangan Prima atau Bukan
        public bool TestPrimeElement(int element)
        {
            bool isPrime = true;

            if (element == 2 || element == 3)
            {
                return isPrime;
            }
            else
            {
                if (element == 1 || element % 2 == 0 || element % 3 == 0)
                {
                    isPrime = false;
                }
                else
                {
                    for (int i = 5; i <= Math.Sqrt(element); i = i + 6)
                        if (element % i == 0 || element % (i + 2) == 0)
                        {
                            isPrime = false;
                            break;
                        }
                }
            }

            return isPrime;
        }

        // Function - Cek Apakah (p - 1) mod Q = 0 atau Tidak
        public bool TestTheEstimateOfThePNumber(int var_p, int var_Q)
        {
            if ((var_p - 1) % var_Q == 0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        // Function - Cek Apakah Sebuah Bilangan Merupakan Primitive Root dari Bilangan Prima atau Bukan
        public bool TestPrimitiveElement(int input, int var_prime, int var_Q)
        {
            /* Primitive Root :
             * Misal Bilangan p = 7, Bilangan a = 3
             * Elemennya Yaitu : 1, 2, 3, 4, 5, 6
             * 
             * Cek Sebuah Bilangan Apakah Primitive Root atau Tidak dengan Cara :
             * Proot = a ^ (1 ... (p -1)) mod p
             * Proot = 3 ^ (1 ... 6) mod 7
             * 3 ^ 1 % 7 = [3], 3 ^ 2 % 7 = [2], 3 ^ 3 % 7 = [6], 3 ^ 4 % 7 = [4], 3 ^ 5 % 7 = [5], 3 ^ 6 % 7 = [1]
             * 
             * Dari 3 ^ 1 Hingga 3 ^ 6 Menghasilkan Output 1, 2, 3, 4, 5, 6  yang Mengandung Elemen dari p
             * Jadi 3 Adalah Primitive Root dari 7.
             */

            bool check = true;
            int power = var_prime - 1 / var_Q;
            int test = PowerAndModulo(input, power, var_prime);

            if (test != 1)
            {
                check = true;
            }
            else
            {
                if (test == 1)
                {
                    check = false;
                }
            }

            return check;
        }

        // Function - Cek Apakah 2 Bilangan Saling Prima atau Tidak (Co-Prime)
        public bool TestCoPrime(int a, int b)
        {
            // Euclidean Algorithm / GCD (Menentukan FPB dari 2 Bilangan)
            int temp;
            while (b != 0)
            {
                temp = a % b;
                a = b;
                b = temp;
            }

            // 2 atau Lebih Bilangan Bisa Dikatakan Ko-Prima Jika GDC-nya = 1
            if (a == 1)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        #endregion
    }
}
