'use strict';

var nodeTfhe = require('node-tfhe');
var sodium = require('libsodium-wrappers');
var bigintBuffer = require('bigint-buffer');

/******************************************************************************
Copyright (c) Microsoft Corporation.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */


function __awaiter(thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
}

function __generator(thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (g && (g = 0, op[0] && (_ = 0)), _) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
}

var encrypt4 = function (value, publicKey) {
    var uint8Array = new Uint8Array([value]);
    var encrypted = nodeTfhe.CompactFheUint4List.encrypt_with_compact_public_key(uint8Array, publicKey);
    return encrypted.serialize();
};
var encryptBool = function (value, publicKey) {
    var encrypted = nodeTfhe.CompactFheBoolList.encrypt_with_compact_public_key([value], publicKey);
    return encrypted.serialize();
};
var encrypt8 = function (value, publicKey) {
    var uint8Array = new Uint8Array([value]);
    var encrypted = nodeTfhe.CompactFheUint8List.encrypt_with_compact_public_key(uint8Array, publicKey);
    return encrypted.serialize();
};
var encrypt16 = function (value, publicKey) {
    var uint16Array = new Uint16Array([value]);
    var encrypted = nodeTfhe.CompactFheUint16List.encrypt_with_compact_public_key(uint16Array, publicKey);
    return encrypted.serialize();
};
var encrypt32 = function (value, publicKey) {
    var uint32Array = new Uint32Array([value]);
    var encrypted = nodeTfhe.CompactFheUint32List.encrypt_with_compact_public_key(uint32Array, publicKey);
    return encrypted.serialize();
};
var encrypt64 = function (value, publicKey) {
    var uint64Array = new BigUint64Array([BigInt(value)]);
    var encrypted = nodeTfhe.CompactFheUint64List.encrypt_with_compact_public_key(uint64Array, publicKey);
    return encrypted.serialize();
};
var encryptAddress = function (value, publicKey) {
    // value is something like 0x8ba1f109551bd432803012645ac136ddd64dba72
    var encrypted = nodeTfhe.CompactFheUint160List.encrypt_with_compact_public_key([BigInt(value)], publicKey);
    return encrypted.serialize();
};

var fromHexString = function (hexString) {
    var arr = hexString.replace(/^(0x)/, '').match(/.{1,2}/g);
    if (!arr)
        return new Uint8Array();
    return Uint8Array.from(arr.map(function (byte) { return parseInt(byte, 16); }));
};
var toHexString = function (bytes) {
    return bytes.reduce(function (str, byte) { return str + byte.toString(16).padStart(2, '0'); }, '');
};
var bytesToBigInt = function (byteArray) {
    if (!byteArray || (byteArray === null || byteArray === void 0 ? void 0 : byteArray.length) === 0) {
        return BigInt(0);
    }
    byteArray.length;
    var buffer = Buffer.from(byteArray);
    var result = bigintBuffer.toBigIntBE(buffer);
    return result;
};
var isAddress = function (address) {
    if (address.match(/^0x[0-9a-fA-F]{40}$/)) {
        // check if it has the basic requirements of an address
        return true;
    }
    return false;
};

var generatePublicKey = function (params) {
    var keypair = params.keypair || sodium.crypto_box_keypair();
    var msgParams = {
        types: {
            // This refers to the domain the contract is hosted on.
            EIP712Domain: [
                { name: 'name', type: 'string' },
                { name: 'version', type: 'string' },
                { name: 'chainId', type: 'uint256' },
                { name: 'verifyingContract', type: 'address' },
            ],
            // Refer to primaryType.
            Reencrypt: [{ name: 'publicKey', type: 'bytes32' }],
        },
        // This defines the message you're proposing the user to sign, is dapp-specific, and contains
        // anything you want. There are no required fields. Be as explicit as possible when building out
        // the message schema.
        // This refers to the keys of the following types object.
        primaryType: 'Reencrypt',
        domain: {
            // Give a user-friendly name to the specific contract you're signing for.
            name: params.name || 'Authorization token',
            // This identifies the latest version.
            version: params.version || '1',
            // This defines the network, in this case, Mainnet.
            chainId: params.chainId || 9000,
            // // Add a verifying contract to make sure you're establishing contracts with the proper entity.
            verifyingContract: params.verifyingContract,
        },
        message: {
            publicKey: "0x".concat(toHexString(keypair.publicKey)),
        },
    };
    return {
        keypair: {
            publicKey: keypair.publicKey,
            privateKey: keypair.privateKey,
        },
        eip712: msgParams,
    };
};

var decrypt = function (keypair, ciphertext) {
    var toDecrypt = typeof ciphertext === 'string' ? fromHexString(ciphertext) : ciphertext;
    var decrypted = sodium.crypto_box_seal_open(toDecrypt, keypair.publicKey, keypair.privateKey);
    return bytesToBigInt(decrypted);
};

var getPublicKeyCallParams = function () { return ({
    to: '0x000000000000000000000000000000000000005d',
    data: '0xd9d47bb001',
}); };
var createInstance = function (params) { return __awaiter(void 0, void 0, void 0, function () {
    var chainId, publicKey, keypairs, tfheCompactPublicKey, buff, contractKeypairs, hasKeypair, checkEncryptedValue;
    return __generator(this, function (_a) {
        switch (_a.label) {
            case 0: return [4 /*yield*/, sodium.ready];
            case 1:
                _a.sent();
                chainId = params.chainId, publicKey = params.publicKey, keypairs = params.keypairs;
                if (typeof chainId !== 'number')
                    throw new Error('chainId must be a number');
                if (publicKey && typeof publicKey !== 'string')
                    throw new Error('publicKey must be a string');
                if (publicKey) {
                    buff = fromHexString(publicKey);
                    tfheCompactPublicKey = nodeTfhe.TfheCompactPublicKey.deserialize(buff);
                }
                contractKeypairs = {};
                if (keypairs) {
                    Object.keys(keypairs).forEach(function (contractAddress) {
                        if (isAddress(contractAddress)) {
                            var oKeys_1 = Object.keys(keypairs[contractAddress]);
                            if (['signature', 'privateKey', 'publicKey'].every(function (v) {
                                return oKeys_1.includes(v);
                            })) {
                                contractKeypairs[contractAddress] = {
                                    signature: keypairs[contractAddress].signature,
                                    publicKey: fromHexString(keypairs[contractAddress].publicKey),
                                    privateKey: fromHexString(keypairs[contractAddress].privateKey),
                                };
                            }
                        }
                    });
                }
                hasKeypair = function (contractAddress) {
                    return (contractKeypairs[contractAddress] != null &&
                        !!contractKeypairs[contractAddress].signature);
                };
                checkEncryptedValue = function (value, bits) {
                    if (value == null)
                        throw new Error('Missing value');
                    var limit = BigInt(Math.pow(2, bits));
                    if (typeof value !== 'number' && typeof value !== 'bigint')
                        throw new Error('Value must be a number or a bigint.');
                    if (value >= limit) {
                        throw new Error("The value exceeds the limit for ".concat(bits, "bits integer (").concat((limit - BigInt(1)).toString(), ")."));
                    }
                };
                return [2 /*return*/, {
                        // Parameters
                        encryptBool: function (value) {
                            if (!tfheCompactPublicKey)
                                throw new Error('Your instance has been created without the public blockchain key.');
                            if (value == null)
                                throw new Error('Missing value');
                            if (typeof value !== 'boolean' &&
                                typeof value !== 'number' &&
                                typeof value !== 'bigint')
                                throw new Error('Value must be a boolean, a number or a bigint.');
                            if ((typeof value !== 'bigint' || typeof value !== 'number') &&
                                Number(value) > 1)
                                throw new Error('Value must be 1 or 0.');
                            return encryptBool(Boolean(value), tfheCompactPublicKey);
                        },
                        encrypt4: function (value) {
                            if (!tfheCompactPublicKey)
                                throw new Error('Your instance has been created without the public blockchain key.');
                            checkEncryptedValue(value, 4);
                            return encrypt4(Number(value), tfheCompactPublicKey);
                        },
                        encrypt8: function (value) {
                            if (!tfheCompactPublicKey)
                                throw new Error('Your instance has been created without the public blockchain key.');
                            checkEncryptedValue(value, 8);
                            return encrypt8(Number(value), tfheCompactPublicKey);
                        },
                        encrypt16: function (value) {
                            if (!tfheCompactPublicKey)
                                throw new Error('Your instance has been created without the public blockchain key.');
                            checkEncryptedValue(value, 16);
                            return encrypt16(Number(value), tfheCompactPublicKey);
                        },
                        encrypt32: function (value) {
                            if (!tfheCompactPublicKey)
                                throw new Error('Your instance has been created without the public blockchain key.');
                            checkEncryptedValue(value, 32);
                            return encrypt32(Number(value), tfheCompactPublicKey);
                        },
                        encrypt64: function (value) {
                            if (!tfheCompactPublicKey)
                                throw new Error('Your instance has been created without the public blockchain key.');
                            checkEncryptedValue(value, 64);
                            return encrypt64(value, tfheCompactPublicKey);
                        },
                        encryptAddress: function (value) {
                            if (!tfheCompactPublicKey)
                                throw new Error('Your instance has been created without the public blockchain key.');
                            if (typeof value !== 'string')
                                throw new Error('Value must be a string.');
                            if (!isAddress(value))
                                throw new Error('Value must be a valid address.');
                            return encryptAddress(value, tfheCompactPublicKey);
                        },
                        // Reencryption
                        generatePublicKey: function (options) {
                            if (!options || !options.verifyingContract)
                                throw new Error('Missing contract address');
                            if (!isAddress(options.verifyingContract))
                                throw new Error('Invalid contract address');
                            var kp;
                            if (!options.force && contractKeypairs[options.verifyingContract]) {
                                kp = contractKeypairs[options.verifyingContract];
                            }
                            var _a = generatePublicKey({
                                verifyingContract: options.verifyingContract,
                                name: options.name,
                                version: options.version,
                                chainId: chainId,
                                keypair: kp,
                            }), eip712 = _a.eip712, keypair = _a.keypair;
                            contractKeypairs[options.verifyingContract] = {
                                privateKey: keypair.privateKey,
                                publicKey: keypair.publicKey,
                                signature: null,
                            };
                            return { eip712: eip712, publicKey: keypair.publicKey };
                        },
                        setSignature: function (contractAddress, signature) {
                            if (contractKeypairs[contractAddress] &&
                                contractKeypairs[contractAddress].privateKey) {
                                contractKeypairs[contractAddress].signature = signature;
                            }
                        },
                        getPublicKey: function (contractAddress) {
                            if (hasKeypair(contractAddress)) {
                                return {
                                    publicKey: contractKeypairs[contractAddress].publicKey,
                                    signature: contractKeypairs[contractAddress].signature,
                                };
                            }
                            return null;
                        },
                        hasKeypair: hasKeypair,
                        decrypt: function (contractAddress, ciphertext) {
                            if (!ciphertext)
                                throw new Error('Missing ciphertext.');
                            if (!contractAddress)
                                throw new Error('Missing contract address.');
                            var kp = contractKeypairs[contractAddress];
                            if (!kp)
                                throw new Error("Missing keypair for ".concat(contractAddress, "."));
                            return decrypt(kp, ciphertext);
                        },
                        serializeKeypairs: function () {
                            var stringKeypairs = {};
                            Object.keys(contractKeypairs).forEach(function (contractAddress) {
                                var signature = contractKeypairs[contractAddress].signature;
                                if (!signature)
                                    return;
                                stringKeypairs[contractAddress] = {
                                    signature: signature,
                                    publicKey: toHexString(contractKeypairs[contractAddress].publicKey),
                                    privateKey: toHexString(contractKeypairs[contractAddress].privateKey),
                                };
                            });
                            return stringKeypairs;
                        },
                    }];
        }
    });
}); };

var createTfheKeypair = function () {
    var block_params = new nodeTfhe.ShortintParameters(nodeTfhe.ShortintParametersName.PARAM_MESSAGE_2_CARRY_2_COMPACT_PK_PBS_KS);
    // const configBuilder = new TfheConfigBuilder();
    // const config = configBuilder.use_custom_parameters(block_params).build();
    var config = nodeTfhe.TfheConfigBuilder.default()
        .use_custom_parameters(block_params)
        .build();
    var clientKey = nodeTfhe.TfheClientKey.generate(config);
    var publicKey = nodeTfhe.TfheCompactPublicKey.new(clientKey);
    publicKey = nodeTfhe.TfheCompactPublicKey.deserialize(publicKey.serialize());
    return { clientKey: clientKey, publicKey: publicKey };
};
var createTfhePublicKey = function () {
    var publicKey = createTfheKeypair().publicKey;
    return toHexString(publicKey.serialize());
};

exports.createInstance = createInstance;
exports.createTfheKeypair = createTfheKeypair;
exports.createTfhePublicKey = createTfhePublicKey;
exports.getPublicKeyCallParams = getPublicKeyCallParams;
