/**
 * Created by jsroads on 2022/3/8  6:42 PM
 * Note:
 */
import { Algo } from "./algo";
const  rl = null;
export class encryptjs{
    public static version:string = "1.0.0"
    public static spaces:string = " "
    public static init = function () {
        console.log("--------------------Applying Encryption Algorithm------------------ ");
    };
    public static encrypt(plaintext, password, nBits) {
        var blockSize = 16;  // block size fixed at 16 bytes / 128 bits (Nb=4)
        if (!(nBits==128 || nBits==192 || nBits==256)) return ''; // standard allows 128/192/256 bit keys
        plaintext = this.utf8Encode(plaintext);
        password = this.utf8Encode(password);

        // use AES itself to encrypt password to get cipher key (using plain password as source for key
        // expansion) - gives us well encrypted key (though hashed key might be preferred for prod'n use)
        var nBytes = nBits/8;  // no bytes in key (16/24/32)
        var pwBytes = new Array(nBytes);
        for (var i=0; i<nBytes; i++) {  // use 1st 16/24/32 chars of password for key
            pwBytes[i] = isNaN(password.charCodeAt(i)) ? 0 : password.charCodeAt(i);
        }
        var key = Algo.cipher(pwBytes, Algo.keyExpansion(pwBytes)); // gives us 16-byte key
        key = key.concat(key.slice(0, nBytes-16));  // expand key to 16/24/32 bytes long

        // initialise 1st 8 bytes of counter block with nonce (NIST SP800-38A �B.2): [0-1] = millisec,
        // [2-3] = random, [4-7] = seconds, together giving full sub-millisec uniqueness up to Feb 2106
        var counterBlock = new Array(blockSize);

        var nonce = (new Date()).getTime();  // timestamp: milliseconds since 1-Jan-1970
        var nonceMs = nonce%1000;
        var nonceSec = Math.floor(nonce/1000);
        var nonceRnd = Math.floor(Math.random()*0xffff);
        // for debugging: nonce = nonceMs = nonceSec = nonceRnd = 0;

        for (var i=0; i<2; i++) counterBlock[i]   = (nonceMs  >>> i*8) & 0xff;
        for (var i=0; i<2; i++) counterBlock[i+2] = (nonceRnd >>> i*8) & 0xff;
        for (var i=0; i<4; i++) counterBlock[i+4] = (nonceSec >>> i*8) & 0xff;

        // and convert it to a string to go on the front of the ciphertext
        var ctrTxt = '';
        for (var i=0; i<8; i++) ctrTxt += String.fromCharCode(counterBlock[i]);

        // generate key schedule - an expansion of the key into distinct Key Rounds for each round
        var keySchedule = Algo.keyExpansion(key);

        var blockCount = Math.ceil(plaintext.length/blockSize);
        var ciphertxt = new Array(blockCount);  // ciphertext as array of strings

        for (var b=0; b<blockCount; b++) {
            // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
            // done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB)
            for (var c=0; c<4; c++) counterBlock[15-c] = (b >>> c*8) & 0xff;
            for (var c=0; c<4; c++) counterBlock[15-c-4] = (b/0x100000000 >>> c*8);

            var cipherCntr = Algo.cipher(counterBlock, keySchedule);  // -- encrypt counter block --

            // block size is reduced on final block
            var blockLength = b<blockCount-1 ? blockSize : (plaintext.length-1)%blockSize+1;
            var cipherChar = new Array(blockLength);

            for (var i=0; i<blockLength; i++) {  // -- xor plaintext with ciphered counter char-by-char --
                cipherChar[i] = cipherCntr[i] ^ plaintext.charCodeAt(b*blockSize+i);
                cipherChar[i] = String.fromCharCode(cipherChar[i]);
            }
            ciphertxt[b] = cipherChar.join('');
        }

        // use Array.join() for better performance than repeated string appends
        var ciphertext = ctrTxt + ciphertxt.join('');
        ciphertext = this.base64Encode(ciphertext);

        return ciphertext;
    };

    public static decrypt(ciphertext, password, nBits) {
        var blockSize = 16;  // block size fixed at 16 bytes / 128 bits (Nb=4) for AES
        if (!(nBits==128 || nBits==192 || nBits==256)) return ''; // standard allows 128/192/256 bit keys
        ciphertext = this.base64Decode(ciphertext);
        password = this.utf8Encode(password);

        // use AES to encrypt password (mirroring encrypt routine)
        var nBytes = nBits/8;  // no bytes in key
        var pwBytes = new Array(nBytes);
        for (var i=0; i<nBytes; i++) {
            pwBytes[i] = isNaN(password.charCodeAt(i)) ? 0 : password.charCodeAt(i);
        }
        var key = Algo.cipher(pwBytes, Algo.keyExpansion(pwBytes));
        key = key.concat(key.slice(0, nBytes-16));  // expand key to 16/24/32 bytes long

        // recover nonce from 1st 8 bytes of ciphertext
        var counterBlock = new Array(8);
        var ctrTxt = ciphertext.slice(0, 8);
        for (var i=0; i<8; i++) counterBlock[i] = ctrTxt.charCodeAt(i);

        // generate key schedule
        var keySchedule = Algo.keyExpansion(key);

        // separate ciphertext into blocks (skipping past initial 8 bytes)
        var nBlocks = Math.ceil((ciphertext.length-8) / blockSize);
        var ct = new Array(nBlocks);
        for (var b=0; b<nBlocks; b++) ct[b] = ciphertext.slice(8+b*blockSize, 8+b*blockSize+blockSize);
        ciphertext = ct;  // ciphertext is now array of block-length strings

        // plaintext will get generated block-by-block into array of block-length strings
        var plaintxt = new Array(ciphertext.length);

        for (var b=0; b<nBlocks; b++) {
            // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
            for (var c=0; c<4; c++) counterBlock[15-c] = ((b) >>> c*8) & 0xff;
            for (var c=0; c<4; c++) counterBlock[15-c-4] = (((b+1)/0x100000000-1) >>> c*8) & 0xff;

            var cipherCntr = Algo.cipher(counterBlock, keySchedule);  // encrypt counter block

            var plaintxtByte = new Array(ciphertext[b].length);
            for (var i=0; i<ciphertext[b].length; i++) {
                // -- xor plaintxt with ciphered counter byte-by-byte --
                plaintxtByte[i] = cipherCntr[i] ^ ciphertext[b].charCodeAt(i);
                plaintxtByte[i] = String.fromCharCode(plaintxtByte[i]);
            }
            plaintxt[b] = plaintxtByte.join('');
        }

        // join array of blocks into single plaintext string
        var plaintext = plaintxt.join('');
        plaintext = this.utf8Decode(plaintext);  // decode from UTF8 back to Unicode multi-byte chars

        return plaintext;
    };

    public static getTextEncryptAndSaveToTextFile(filePath,password,nBits) {
        if (!rl) throw Error("Command line not supported on this platform");
        rl.question("Enter the text to be encrypted: ", function(answer) {
            // TODO: Log the answer in a database
            console.log("'"+answer+"' This text will be encrypted and stored in a text file 'encrypted.txt'");
            var cipherText=this.encrypt(answer,password,nBits);
            // fs.writeFile(filePath,cipherText,function(){
            //     console.log("'encrypted.txt' File created in your local directory, if not present refresh your project");
            // });
            rl.close();
        });
    };

    public static getTextEncryptAndSaveToJSONFile(filePath,password,nBits) {
        if (!rl) throw Error("Command line not supported on this platform");
        rl.question("Enter the text to be encrypted: ", function(answer) {
            // TODO: Log the answer in a database
            console.log("'"+answer+"' This text will be encrypted and stored in a text file 'encrypted.txt'");
            var cipherText=this.encrypt(answer,password,nBits);
            this.writeCipherTextToJSON(filePath,{EncryptedText:cipherText},function(){
                console.log("'encryptedText.JSON' File created in your local directory, if not present refresh your project");
            });
            rl.close();
        });
    };

    public static writeCipherTextToJSON(file, obj, options, callback) {
        if (callback == null) {
            callback = options;
            options = {}
        }

        var spaces = typeof options === 'object' && options !== null
            ? 'spaces' in options
                ? options.spaces : this.spaces
            : this.spaces;

        var str = '';
        try {
            str = JSON.stringify(obj, options ? options.replacer : null, spaces) + '\n'
        } catch (err) {
            if (callback) return callback(err, null)
        }

        // fs.writeFile(file, str, options, callback)
    };
    public static utf8Encode = function(text) {
        return unescape( encodeURIComponent( text ) );
    };
    public static  utf8Decode = function(text) {
        try {
            return decodeURIComponent( escape( text ) );
        } catch (e) {
            return this; // invalid UTF-8? return as-is
        }
    };
    public static base64Encode = function(text) {
        if (typeof btoa != 'undefined') return btoa(text); // browser
        if (typeof Buffer != 'undefined') return new Buffer(text, 'utf8').toString('base64'); // Node.js
        throw new Error('No Base64 Encode');
    };
    public static  base64Decode = function(text) {
        if (typeof atob != 'undefined') return atob(text); // browser
        if (typeof Buffer != 'undefined') return new Buffer(text, 'base64').toString('utf8'); // Node.js
        throw new Error('No Base64 Decode');
    };
}
encryptjs.init()