#include <memory>
using std::unique_ptr;
#include <openssl/err.h> 
#include <openssl/bn.h>
#include <openssl/rsa.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
//using BN_ptr = std::unique_ptr<BIGNUM, decltype(&::BN_free)>;
//using RSA_ptr = std::unique_ptr<RSA, decltype(&::RSA_free)>;
EVP_CIPHER_CTX *ctx;
RSA* rsa;
BIGNUM* bn;
//RSA_ptr rsa_signing_keypair; //(RSA_new(), ::RSA_free);
//BN_ptr rsa_bignum;

// assumes that the digest is at least of length 256/8 bytes. 
uint32_t generate_sha256_hash(const unsigned char *message, size_t message_len, unsigned char *digest)
{
	EVP_MD_CTX *mdctx; unsigned int digest_len;

	if((mdctx = EVP_MD_CTX_create()) == NULL)
	{
		printf("EVP_MD_CTX_create returned NULL - could not create context\n"); fflush(stdout); return 0x1; 
	}

	if(EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL) != 1)
	{
                printf("EVP_DigestInit_ex returned 0 - could not initialize hash with SHA256\n"); fflush(stdout); return 0x2; 
	}

        if(EVP_DigestUpdate(mdctx, message, message_len) != 1)        
        {
                printf("EVP_DigestUpdate returned 0 - could not compute SHA256 hash\n"); fflush(stdout); return 0x3; 
        }

	if(1 != EVP_DigestFinal_ex(mdctx, digest, &digest_len))     
        {
                printf("EVP_DigestFinal_ex returned 0 - could not finalize SHA256 hash\n"); fflush(stdout); return 0x4; 
        }

	if(digest_len != 32) 
	{
		printf("EVP_DigestFinal_ex returned a digest length of 0x%x instead of 0x20\n", digest_len); fflush(stdout); return 0x5; 
	}
	EVP_MD_CTX_destroy(mdctx);
	return 0; 
}

//private: 
//	RSA_ptr rsa(RSA_new(), ::RSA_free);
//	BN_ptr bn(BN_new(), ::BN_free);
uint32_t generate_rsa_keypair(FILE* fp, std::string& priv_key_str, std::string& pub_key_str) //, uint8_t* hash)
{
    int rc; 

    rsa=RSA_new(); 
    bn=BN_new();
    rc = BN_set_word(bn, 3);
    if(rc != 1)
      return 0x1;

    rc = RSA_generate_key_ex(rsa, 3072, bn, NULL);
    if(rc != 1)
      return 0x2;
	printf("Generated key\n"); fflush(stdout); 
/*    int pub_key_der_encoded_len, priv_key_der_encoded_len;
    unsigned char *pub_key_der, priv_key_der;

    pub_key_der = NULL;
    pub_key_der_encoded_len = i2d_RSAPublicKey(rsa.get(), (unsigned char**) &pub_key_der);
    if (pub_key_der_encoded_len < 0)
      return 0x3;

    priv_key_der = NULL;
    priv_key_der_encoded_len = i2d_RSAPrivateKey(rsa.get(), (unsigned char**) &priv_key_der);
    if (priv_key_der_encoded_len < 0)
      return 0x4;
    printf("Done\n"); fflush(stdout); 
//   priv_key_str=std::string(priv_key_der, priv_key_der_encoded_len); //, priv_key_der); 
//   pub_key_str=std::string(pub_key_der, pub_key_der_encoded_len);  
*/
//	BIO* bio_rsa;
	//    rc = PEM_write_RSA_PUBKEY(fp, rsa); 
    rc=	PEM_write_RSAPrivateKey(fp, rsa, NULL, NULL, 0, NULL, NULL); 
    if(rc != 1) 
	return 0x3;
    fflush(fp); 
//    bio_rsa = BIO_new_file("apache_signature_keypair.pem", "w+");
//    rc = PEM_write_bio_RSAPublicKey(bio_rsa, rsa.get());
//    if(rc != 1)
//	return 0x3;
 //   BIO_flush(bio_rsa); free(bio_rsa); 

  return 0; 
}

uint32_t generate_rsa_keypair_hash(uint8_t* hash)
{
     uint32_t return_internal;
    const BIGNUM* n_internal_bigendian_struct;
    RSA_get0_key(rsa, &n_internal_bigendian_struct, NULL, NULL);
    BIGNUM* n_bigendian_struct = BN_dup(n_internal_bigendian_struct); 

    uint32_t count; 
    int n_bignum_length=BN_num_bytes(n_bigendian_struct);
    unsigned char *n_bigendian = (unsigned char*) malloc(n_bignum_length);
    int length_bignum_le = BN_bn2bin(n_bigendian_struct, n_bigendian); 
    unsigned char* n_littleendian = (unsigned char*) malloc(length_bignum_le);
    for(count=0; count<length_bignum_le; count++)
		n_littleendian[count] = n_bigendian[length_bignum_le-count-1]; 
    free(n_bigendian); 
//    unsigned char hash[32];  
    return_internal=generate_sha256_hash(n_littleendian, length_bignum_le, hash);
    free(n_littleendian); 
   if(return_internal != 0)
   { 	 return return_internal ;  }// TODO: Memory leak here. 

	for(count=0;count<32; count++)
		printf("%x", hash[count]);
	printf("\n");
	fflush(stdout); 
	return return_internal;
//   return 0; //length_bignum_le; 
}

void crypto_cleanup()
{
	RSA_free(rsa); 
	BN_free(bn); 
	EVP_CIPHER_CTX_free(ctx); 
}

// Code adapted from here: https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption
int aes_gcm_128(int enc, unsigned char *key, unsigned char *iv, unsigned char* plaintext, int plaintext_len, unsigned char *ciphertext,  uint32_t* op_ciphertext_len, unsigned char* tag)
{
	int len;
	int ciphertext_len;
	int reset_return;
	if(ctx == NULL)
	{
		/* Create and initialise the context */
		if(!(ctx = EVP_CIPHER_CTX_new())) { ERR_print_errors_fp(stderr); fflush(stderr);return 0x1; }
	}

	/* Initialise the encryption operation. */
	if(1 != EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv, enc))
	{
		reset_return = EVP_CIPHER_CTX_reset(ctx);
		ERR_print_errors_fp(stderr);
		if(reset_return != 1)
			return 0xf2;
		return 0x2;
	}
	/* Provide the message to be encrypted, and obtain the encrypted output.
	 * EVP_EncryptUpdate can be called multiple times if necessary
	 */
	if(1 != EVP_CipherUpdate(ctx, ciphertext, &len, plaintext, plaintext_len))
	{
                reset_return = EVP_CIPHER_CTX_reset(ctx);
                ERR_print_errors_fp(stderr);
	        if(1 != reset_return)
			return 0xF3;
		return 0x3;
	}
	ciphertext_len = len;

	if(enc == 0)
        {
                if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag))
                {
                       reset_return = EVP_CIPHER_CTX_reset(ctx);
                       ERR_print_errors_fp(stderr); fflush(stderr);
                       if(1 != reset_return)
                                return 0xF5;
                        return 0x5;
                }
        }


	/* Finalise the encryption. Normally ciphertext bytes may be written at
	 * this stage, but this does not occur in GCM mode
	 */
	// TODO: ^^^ Why the heck does it not occur in GCM mode ?
	if(1 != EVP_CipherFinal_ex(ctx, ciphertext + len, &len))
	{
		reset_return = EVP_CIPHER_CTX_reset(ctx);
		ERR_print_errors_fp(stderr); fflush(stderr);
                if(1 != reset_return)
                        return 0xF4;
                return 0x4;
	}
	ciphertext_len += len;

	/* Get the tag */
	if(enc == 1)
	{
		if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag))
		{
                	reset_return = EVP_CIPHER_CTX_reset(ctx);
                	ERR_print_errors_fp(stderr); fflush(stderr);
                	if(1 != reset_return)
                        	return 0xF5;
	                return 0x5;
		}
	}

	/* Clean up */
	if(1 != EVP_CIPHER_CTX_reset(ctx))
	{
		ERR_print_errors_fp(stderr); fflush(stderr);
		return 0xF0;
	}

	*op_ciphertext_len=ciphertext_len;
	return 0;
}

