import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import "package:pointycastle/export.dart";
import "package:asn1lib/asn1lib.dart";

/*
var rsaKeyHelper = RsaKeyHelper();
var keyPair = rsaKeyHelper.generateRSAkeyPair();
var rsaPub = keyPair.publicKey;
var rsaPriv = keyPair.privateKey;
var cipherText = rsaKeyHelper.rsaEncrypt(rsaPub, Uint8List.fromList('Test Data'.codeUnits));
print(cipherText);
var plainText = rsaKeyHelper.rsaDecrypt(rsaPriv, cipherText);
print(String.fromCharCodes(plainText));
 */


List<int> decodePEM(String pem) {
    var startsWith = [
        '-----BEGIN PUBLIC KEY-----',
        '-----BEGIN PUBLIC KEY-----',
        '\n-----BEGIN PUBLIC KEY-----',
        '-----BEGIN PRIVATE KEY-----',
        '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: React-Native-OpenPGP.js 0.1\r\nComment: http://openpgpjs.org\r\n\r\n',
        '-----BEGIN PGP PRIVATE KEY BLOCK-----\r\nVersion: React-Native-OpenPGP.js 0.1\r\nComment: http://openpgpjs.org\r\n\r\n',
    ];
    var endsWith = [
        '-----END PUBLIC KEY-----',
        '-----END PRIVATE KEY-----',
        '-----END PGP PUBLIC KEY BLOCK-----',
        '-----END PGP PRIVATE KEY BLOCK-----',
    ];
    bool isOpenPgp = pem.contains('BEGIN PGP');

    for (var s in startsWith) {
        if (pem.startsWith(s)) {
            pem = pem.substring(s.length);
        }
    }

    for (var s in endsWith) {
        if (pem.endsWith(s)) {
            pem = pem.substring(0, pem.length - s.length);
        }
    }

    if (isOpenPgp) {
        var index = pem.indexOf('\r\n');
        pem = pem.substring(0, index);
    }

    pem = pem.replaceAll('\n', '');
    pem = pem.replaceAll('\r', '');

    return base64.decode(pem);
}


class RsaKeyHelper {

    // Generate secure random sequence for seed
    SecureRandom secureRandom() {
        var secureRandom = FortunaRandom();
        var random = Random.secure();
        List<int> seeds = [];
        for (int i = 0; i < 32; i++) {
            seeds.add(random.nextInt(255));
        }
        secureRandom.seed(KeyParameter(Uint8List.fromList(seeds)));

        return secureRandom;
    }

    // Generate RSA key pair
    AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey> generateRSAkeyPair({int bitLength = 2048}) {
        var secureRandom = this.secureRandom();

        // final keyGen = KeyGenerator('RSA'); // Get using registry
        final keyGen = RSAKeyGenerator();

        keyGen.init(ParametersWithRandom(
                        RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64),
                        secureRandom));

        // Use the generator

        final pair = keyGen.generateKeyPair();

        // Cast the generated key pair into the RSA key types

        final myPublic = pair.publicKey as RSAPublicKey;
        final myPrivate = pair.privateKey as RSAPrivateKey;

        return AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>(myPublic, myPrivate);
    }


    // Encrypt data using RSA key
    Uint8List rsaEncrypt(RSAPublicKey myPublic, Uint8List dataToEncrypt) {
        final encryptor = OAEPEncoding(RSAEngine())
                ..init(true, PublicKeyParameter<RSAPublicKey>(myPublic)); // true=encrypt

        return _processInBlocks(encryptor, dataToEncrypt);
    }


    // Decrypt data using RSA key
    Uint8List rsaDecrypt(RSAPrivateKey myPrivate, Uint8List cipherText) {
        final decryptor = OAEPEncoding(RSAEngine())
                ..init(false, PrivateKeyParameter<RSAPrivateKey>(myPrivate)); // false=decrypt

        return _processInBlocks(decryptor, cipherText);
    }


    // Process blocks after encryption/descryption
    Uint8List _processInBlocks(AsymmetricBlockCipher engine, Uint8List input) {
        final numBlocks = input.length ~/ engine.inputBlockSize +
                ((input.length % engine.inputBlockSize != 0) ? 1 : 0);

        final output = Uint8List(numBlocks * engine.outputBlockSize);

        var inputOffset = 0;
        var outputOffset = 0;
        while (inputOffset < input.length) {
            final chunkSize = (inputOffset + engine.inputBlockSize <= input.length)
                    ? engine.inputBlockSize
                    : input.length - inputOffset;

            outputOffset += engine.processBlock(
                    input, inputOffset, chunkSize, output, outputOffset);

            inputOffset += chunkSize;
        }

        return (output.length == outputOffset)
                ? output
                : output.sublist(0, outputOffset);
    }


    // Encode RSA public key to pem format
    static encodePublicKeyToPem(RSAPublicKey publicKey) {
        var algorithmSeq = ASN1Sequence();
        var algorithmAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0x1]));
        var paramsAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x5, 0x0]));
        algorithmSeq.add(algorithmAsn1Obj);
        algorithmSeq.add(paramsAsn1Obj);

        var publicKeySeq = ASN1Sequence();
        publicKeySeq.add(ASN1Integer(publicKey.modulus!));
        publicKeySeq.add(ASN1Integer(publicKey.exponent!));
        var publicKeySeqBitString = ASN1BitString(Uint8List.fromList(publicKeySeq.encodedBytes));

        var topLevelSeq = ASN1Sequence();
        topLevelSeq.add(algorithmSeq);
        topLevelSeq.add(publicKeySeqBitString);
        var dataBase64 = base64.encode(topLevelSeq.encodedBytes);

        return """-----BEGIN PUBLIC KEY-----\r\n$dataBase64\r\n-----END PUBLIC KEY-----""";
    }

    // Parse public key PEM file
    static parsePublicKeyFromPem(pemString) {
        List<int> publicKeyDER = decodePEM(pemString);
        var asn1Parser = ASN1Parser(Uint8List.fromList(publicKeyDER));
        var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
        var publicKeyBitString = topLevelSeq.elements[1];

        var publicKeyAsn = ASN1Parser(publicKeyBitString.contentBytes()!, relaxedParsing: true);
        var publicKeySeq = publicKeyAsn.nextObject() as ASN1Sequence;
        var modulus = publicKeySeq.elements[0] as ASN1Integer;
        var exponent = publicKeySeq.elements[1] as ASN1Integer;

        RSAPublicKey rsaPublicKey = RSAPublicKey(
                modulus.valueAsBigInteger!,
                exponent.valueAsBigInteger!
        );

        return rsaPublicKey;
    }


    // Encode RSA private key to pem format
    static encodePrivateKeyToPem(RSAPrivateKey privateKey) {
        var version = ASN1Integer(BigInt.zero);

        var algorithmSeq = ASN1Sequence();
        var algorithmAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([
            0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0x1
        ]));
        var paramsAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x5, 0x0]));
        algorithmSeq.add(algorithmAsn1Obj);
        algorithmSeq.add(paramsAsn1Obj);

        var privateKeySeq = ASN1Sequence();
        var modulus = ASN1Integer(privateKey.n!);
        var publicExponent = ASN1Integer(BigInt.parse('65537'));
        var privateExponent = ASN1Integer(privateKey.privateExponent!);
        var p = ASN1Integer(privateKey.p!);
        var q = ASN1Integer(privateKey.q!);
        var dP = privateKey.privateExponent! % (privateKey.p! - BigInt.one);
        var exp1 = ASN1Integer(dP);
        var dQ = privateKey.privateExponent! % (privateKey.q! - BigInt.one);
        var exp2 = ASN1Integer(dQ);
        var iQ = privateKey.q?.modInverse(privateKey.p!);
        var co = ASN1Integer(iQ!);

        privateKeySeq.add(version);
        privateKeySeq.add(modulus);
        privateKeySeq.add(publicExponent);
        privateKeySeq.add(privateExponent);
        privateKeySeq.add(p);
        privateKeySeq.add(q);
        privateKeySeq.add(exp1);
        privateKeySeq.add(exp2);
        privateKeySeq.add(co);
        var publicKeySeqOctetString = ASN1OctetString(Uint8List.fromList(privateKeySeq.encodedBytes));

        var topLevelSeq = ASN1Sequence();
        topLevelSeq.add(version);
        topLevelSeq.add(algorithmSeq);
        topLevelSeq.add(publicKeySeqOctetString);
        var dataBase64 = base64.encode(topLevelSeq.encodedBytes);

        return """-----BEGIN PRIVATE KEY-----\r\n$dataBase64\r\n-----END PRIVATE KEY-----""";
    }

    static parsePrivateKeyFromPem(pemString) {
        List<int> privateKeyDER = decodePEM(pemString);
        var asn1Parser = ASN1Parser(Uint8List.fromList(privateKeyDER));
        var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
        var version = topLevelSeq.elements[0];
        var algorithm = topLevelSeq.elements[1];
        var privateKey = topLevelSeq.elements[2];

        asn1Parser = ASN1Parser(privateKey.contentBytes()!);
        var pkSeq = asn1Parser.nextObject() as ASN1Sequence;

        version = pkSeq.elements[0];
        var modulus = pkSeq.elements[1] as ASN1Integer;
        //var publicExponent = pkSeq.elements[2] as ASN1Integer;
        var privateExponent = pkSeq.elements[3] as ASN1Integer;
        var p = pkSeq.elements[4] as ASN1Integer;
        var q = pkSeq.elements[5] as ASN1Integer;
        var exp1 = pkSeq.elements[6] as ASN1Integer;
        var exp2 = pkSeq.elements[7] as ASN1Integer;
        var co = pkSeq.elements[8] as ASN1Integer;

        RSAPrivateKey rsaPrivateKey = RSAPrivateKey(
                modulus.valueAsBigInteger!,
                privateExponent.valueAsBigInteger!,
                p.valueAsBigInteger,
                q.valueAsBigInteger
        );

        return rsaPrivateKey;
    }
}
