import Utils from "../Utils.js";
import CryptoApi from "babel-loader!crypto-api";
import MD6 from "node-md6";
import * as SHA3 from "js-sha3";
import Checksum from "./Checksum.js";
import ctph from "ctph.js";
import ssdeep from "ssdeep.js";
import bcrypt from "bcryptjs";
import scrypt from "scryptsy";


/**
 * Hashing operations.
 *
 * @author n1474335 [n1474335@gmail.com]
 * @copyright Crown Copyright 2016
 * @license Apache-2.0
 *
 * @namespace
 */
const Hash = {

    /**
     * Generic hash function.
     *
     * @param {string} name
     * @param {ArrayBuffer} input
     * @param {Object} [options={}]
     * @returns {string}
     */
    runHash: function(name, input, options={}) {
        const msg = Utils.arrayBufferToStr(input, false),
            hasher = CryptoApi.getHasher(name, options);
        hasher.update(msg);
        return CryptoApi.encoder.toHex(hasher.finalize());
    },


    /**
     * MD2 operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runMD2: function (input, args) {
        return Hash.runHash("md2", input);
    },


    /**
     * MD4 operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runMD4: function (input, args) {
        return Hash.runHash("md4", input);
    },


    /**
     * MD5 operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runMD5: function (input, args) {
        return Hash.runHash("md5", input);
    },


    /**
     * @constant
     * @default
     */
    MD6_SIZE: 256,
    /**
     * @constant
     * @default
     */
    MD6_LEVELS: 64,

    /**
     * MD6 operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    runMD6: function (input, args) {
        const size = args[0],
            levels = args[1],
            key = args[2];

        if (size < 0 || size > 512)
            return "Size must be between 0 and 512";
        if (levels < 0)
            return "Levels must be greater than 0";

        return MD6.getHashOfText(input, size, key, levels);
    },


    /**
     * SHA0 operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runSHA0: function (input, args) {
        return Hash.runHash("sha0", input);
    },


    /**
     * SHA1 operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runSHA1: function (input, args) {
        return Hash.runHash("sha1", input);
    },


    /**
     * @constant
     * @default
     */
    SHA2_SIZE: ["512", "256", "384", "224", "512/256", "512/224"],

    /**
     * SHA2 operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runSHA2: function (input, args) {
        const size = args[0];
        return Hash.runHash("sha" + size, input);
    },


    /**
     * @constant
     * @default
     */
    SHA3_SIZE: ["512", "384", "256", "224"],

    /**
     * SHA3 operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runSHA3: function (input, args) {
        const size = parseInt(args[0], 10);
        let algo;

        switch (size) {
            case 224:
                algo = SHA3.sha3_224;
                break;
            case 384:
                algo = SHA3.sha3_384;
                break;
            case 256:
                algo = SHA3.sha3_256;
                break;
            case 512:
                algo = SHA3.sha3_512;
                break;
            default:
                return "Invalid size";
        }

        return algo(input);
    },


    /**
     * @constant
     * @default
     */
    KECCAK_SIZE: ["512", "384", "256", "224"],

    /**
     * Keccak operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runKeccak: function (input, args) {
        const size = parseInt(args[0], 10);
        let algo;

        switch (size) {
            case 224:
                algo = SHA3.keccak224;
                break;
            case 384:
                algo = SHA3.keccak384;
                break;
            case 256:
                algo = SHA3.keccak256;
                break;
            case 512:
                algo = SHA3.keccak512;
                break;
            default:
                return "Invalid size";
        }

        return algo(input);
    },


    /**
     * @constant
     * @default
     */
    SHAKE_CAPACITY: ["256", "128"],
    /**
     * @constant
     * @default
     */
    SHAKE_SIZE: 512,

    /**
     * Shake operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runShake: function (input, args) {
        const capacity = parseInt(args[0], 10),
            size = args[1];
        let algo;

        if (size < 0)
            return "Size must be greater than 0";

        switch (capacity) {
            case 128:
                algo = SHA3.shake128;
                break;
            case 256:
                algo = SHA3.shake256;
                break;
            default:
                return "Invalid size";
        }

        return algo(input, size);
    },


    /**
     * @constant
     * @default
     */
    RIPEMD_SIZE: ["320", "256", "160", "128"],

    /**
     * RIPEMD operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runRIPEMD: function (input, args) {
        const size = args[0];
        return Hash.runHash("ripemd" + size, input);
    },


    /**
     * HAS-160 operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runHAS: function (input, args) {
        return Hash.runHash("has160", input);
    },


    /**
     * @constant
     * @default
     */
    WHIRLPOOL_VARIANT: ["Whirlpool", "Whirlpool-T", "Whirlpool-0"],

    /**
     * Whirlpool operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runWhirlpool: function (input, args) {
        const variant = args[0].toLowerCase();
        return Hash.runHash(variant, input);
    },


    /**
     * @constant
     * @default
     */
    SNEFRU_ROUNDS: ["8", "4", "2"],
    /**
     * @constant
     * @default
     */
    SNEFRU_SIZE: ["256", "128"],

    /**
     * Snefru operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runSnefru: function (input, args) {
        return Hash.runHash("snefru", input, {
            rounds: args[0],
            length: args[1]
        });
    },


    /**
     * CTPH operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    runCTPH: function (input, args) {
        return ctph.digest(input);
    },


    /**
     * SSDEEP operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    runSSDEEP: function (input, args) {
        return ssdeep.digest(input);
    },


    /**
     * @constant
     * @default
     */
    DELIM_OPTIONS: ["Line feed", "CRLF", "Space", "Comma"],

    /**
     * Compare CTPH hashes operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {Number}
     */
    runCompareCTPH: function (input, args) {
        const samples = input.split(Utils.charRep(args[0]));
        if (samples.length !== 2) throw "Incorrect number of samples.";
        return ctph.similarity(samples[0], samples[1]);
    },


    /**
     * Compare SSDEEP hashes operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {Number}
     */
    runCompareSSDEEP: function (input, args) {
        const samples = input.split(Utils.charRep(args[0]));
        if (samples.length !== 2) throw "Incorrect number of samples.";
        return ssdeep.similarity(samples[0], samples[1]);
    },


    /**
     * @constant
     * @default
     */
    HMAC_FUNCTIONS: [
        "MD2",
        "MD4",
        "MD5",
        "SHA0",
        "SHA1",
        "SHA224",
        "SHA256",
        "SHA384",
        "SHA512",
        "SHA512/224",
        "SHA512/256",
        "RIPEMD128",
        "RIPEMD160",
        "RIPEMD256",
        "RIPEMD320",
        "HAS160",
        "Whirlpool",
        "Whirlpool-0",
        "Whirlpool-T",
        "Snefru"
    ],

    /**
     * HMAC operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runHMAC: function (input, args) {
        const key = args[0],
            hashFunc = args[1].toLowerCase(),
            msg = Utils.arrayBufferToStr(input, false),
            hasher = CryptoApi.getHasher(hashFunc);

        // Horrible shim to fix constructor bug. Reported in nf404/crypto-api#8
        hasher.reset = () => {
            hasher.state = {};
            const tmp = new hasher.constructor();
            hasher.state = tmp.state;
        };

        const mac = CryptoApi.getHmac(CryptoApi.encoder.fromUtf(key), hasher);
        mac.update(msg);
        return CryptoApi.encoder.toHex(mac.finalize());
    },


    /**
     * @constant
     * @default
     */
    BCRYPT_ROUNDS: 10,

    /**
     * Bcrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    runBcrypt: async function (input, args) {
        const rounds = args[0];
        const salt = await bcrypt.genSalt(rounds);

        return await bcrypt.hash(input, salt, null, p => {
            // Progress callback
            if (ENVIRONMENT_IS_WORKER())
                self.sendStatusMessage(`Progress: ${(p * 100).toFixed(0)}%`);
        });
    },


    /**
     * Bcrypt compare operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    runBcryptCompare: async function (input, args) {
        const hash = args[0];

        const match = await bcrypt.compare(input, hash, null, p => {
            // Progress callback
            if (ENVIRONMENT_IS_WORKER())
                self.sendStatusMessage(`Progress: ${(p * 100).toFixed(0)}%`);
        });

        return match ? "Match: " + input : "No match";
    },


    /**
     * Bcrypt parse operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    runBcryptParse: async function (input, args) {
        try {
            return `Rounds: ${bcrypt.getRounds(input)}
Salt: ${bcrypt.getSalt(input)}
Password hash: ${input.split(bcrypt.getSalt(input))[1]}
Full hash: ${input}`;
        } catch (err) {
            return "Error: " + err.toString();
        }
    },


    /**
     * @constant
     * @default
     */
    KEY_FORMAT: ["Hex", "Base64", "UTF8", "Latin1"],
    /**
     * @constant
     * @default
     */
    SCRYPT_ITERATIONS: 16384,
    /**
     * @constant
     * @default
     */
    SCRYPT_MEM_FACTOR: 8,
    /**
     * @constant
     * @default
     */
    SCRYPT_PARALLEL_FACTOR: 1,
    /**
     * @constant
     * @default
     */
    SCRYPT_KEY_LENGTH: 64,

    /**
     * Scrypt operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    runScrypt: function (input, args) {
        const salt = Utils.convertToByteString(args[0].string || "", args[0].option),
            iterations = args[1],
            memFactor = args[2],
            parallelFactor = args[3],
            keyLength = args[4];

        try {
            const data = scrypt(
                input, salt, iterations, memFactor, parallelFactor, keyLength,
                p => {
                    // Progress callback
                    if (ENVIRONMENT_IS_WORKER())
                        self.sendStatusMessage(`Progress: ${p.percent.toFixed(0)}%`);
                }
            );

            return data.toString("hex");
        } catch (err) {
            return "Error: " + err.toString();
        }
    },


    /**
     * Generate all hashes operation.
     *
     * @param {ArrayBuffer} input
     * @param {Object[]} args
     * @returns {string}
     */
    runAll: function (input, args) {
        const arrayBuffer = input,
            str = Utils.arrayBufferToStr(arrayBuffer, false),
            byteArray = new Uint8Array(arrayBuffer),
            output = "MD2:         " + Hash.runMD2(arrayBuffer, []) +
                "\nMD4:         " + Hash.runMD4(arrayBuffer, []) +
                "\nMD5:         " + Hash.runMD5(arrayBuffer, []) +
                "\nMD6:         " + Hash.runMD6(str, []) +
                "\nSHA0:        " + Hash.runSHA0(arrayBuffer, []) +
                "\nSHA1:        " + Hash.runSHA1(arrayBuffer, []) +
                "\nSHA2 224:    " + Hash.runSHA2(arrayBuffer, ["224"]) +
                "\nSHA2 256:    " + Hash.runSHA2(arrayBuffer, ["256"]) +
                "\nSHA2 384:    " + Hash.runSHA2(arrayBuffer, ["384"]) +
                "\nSHA2 512:    " + Hash.runSHA2(arrayBuffer, ["512"]) +
                "\nSHA3 224:    " + Hash.runSHA3(arrayBuffer, ["224"]) +
                "\nSHA3 256:    " + Hash.runSHA3(arrayBuffer, ["256"]) +
                "\nSHA3 384:    " + Hash.runSHA3(arrayBuffer, ["384"]) +
                "\nSHA3 512:    " + Hash.runSHA3(arrayBuffer, ["512"]) +
                "\nKeccak 224:  " + Hash.runKeccak(arrayBuffer, ["224"]) +
                "\nKeccak 256:  " + Hash.runKeccak(arrayBuffer, ["256"]) +
                "\nKeccak 384:  " + Hash.runKeccak(arrayBuffer, ["384"]) +
                "\nKeccak 512:  " + Hash.runKeccak(arrayBuffer, ["512"]) +
                "\nShake 128:   " + Hash.runShake(arrayBuffer, ["128", 256]) +
                "\nShake 256:   " + Hash.runShake(arrayBuffer, ["256", 512]) +
                "\nRIPEMD-128:  " + Hash.runRIPEMD(arrayBuffer, ["128"]) +
                "\nRIPEMD-160:  " + Hash.runRIPEMD(arrayBuffer, ["160"]) +
                "\nRIPEMD-256:  " + Hash.runRIPEMD(arrayBuffer, ["256"]) +
                "\nRIPEMD-320:  " + Hash.runRIPEMD(arrayBuffer, ["320"]) +
                "\nHAS-160:     " + Hash.runHAS(arrayBuffer, []) +
                "\nWhirlpool-0: " + Hash.runWhirlpool(arrayBuffer, ["Whirlpool-0"]) +
                "\nWhirlpool-T: " + Hash.runWhirlpool(arrayBuffer, ["Whirlpool-T"]) +
                "\nWhirlpool:   " + Hash.runWhirlpool(arrayBuffer, ["Whirlpool"]) +
                "\nSSDEEP:      " + Hash.runSSDEEP(str) +
                "\nCTPH:        " + Hash.runCTPH(str) +
                "\n\nChecksums:" +
                "\nFletcher-8:  " + Checksum.runFletcher8(byteArray, []) +
                "\nFletcher-16: " + Checksum.runFletcher16(byteArray, []) +
                "\nFletcher-32: " + Checksum.runFletcher32(byteArray, []) +
                "\nFletcher-64: " + Checksum.runFletcher64(byteArray, []) +
                "\nAdler-32:    " + Checksum.runAdler32(byteArray, []) +
                "\nCRC-16:      " + Checksum.runCRC16(str, []) +
                "\nCRC-32:      " + Checksum.runCRC32(str, []);

        return output;
    },


    /**
     * Analyse hash operation.
     *
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    runAnalyse: function(input, args) {
        input = input.replace(/\s/g, "");

        let output = "",
            byteLength = input.length / 2,
            bitLength = byteLength * 8,
            possibleHashFunctions = [];

        if (!/^[a-f0-9]+$/i.test(input)) {
            return "Invalid hash";
        }

        output += "Hash length: " + input.length + "\n" +
            "Byte length: " + byteLength + "\n" +
            "Bit length:  " + bitLength + "\n\n" +
            "Based on the length, this hash could have been generated by one of the following hashing functions:\n";

        switch (bitLength) {
            case 4:
                possibleHashFunctions = [
                    "Fletcher-4",
                    "Luhn algorithm",
                    "Verhoeff algorithm",
                ];
                break;
            case 8:
                possibleHashFunctions = [
                    "Fletcher-8",
                ];
                break;
            case 16:
                possibleHashFunctions = [
                    "BSD checksum",
                    "CRC-16",
                    "SYSV checksum",
                    "Fletcher-16"
                ];
                break;
            case 32:
                possibleHashFunctions = [
                    "CRC-32",
                    "Fletcher-32",
                    "Adler-32",
                ];
                break;
            case 64:
                possibleHashFunctions = [
                    "CRC-64",
                    "RIPEMD-64",
                    "SipHash",
                ];
                break;
            case 128:
                possibleHashFunctions = [
                    "MD5",
                    "MD4",
                    "MD2",
                    "HAVAL-128",
                    "RIPEMD-128",
                    "Snefru",
                    "Tiger-128",
                ];
                break;
            case 160:
                possibleHashFunctions = [
                    "SHA-1",
                    "SHA-0",
                    "FSB-160",
                    "HAS-160",
                    "HAVAL-160",
                    "RIPEMD-160",
                    "Tiger-160",
                ];
                break;
            case 192:
                possibleHashFunctions = [
                    "Tiger",
                    "HAVAL-192",
                ];
                break;
            case 224:
                possibleHashFunctions = [
                    "SHA-224",
                    "SHA3-224",
                    "ECOH-224",
                    "FSB-224",
                    "HAVAL-224",
                ];
                break;
            case 256:
                possibleHashFunctions = [
                    "SHA-256",
                    "SHA3-256",
                    "BLAKE-256",
                    "ECOH-256",
                    "FSB-256",
                    "GOST",
                    "Grøstl-256",
                    "HAVAL-256",
                    "PANAMA",
                    "RIPEMD-256",
                    "Snefru",
                ];
                break;
            case 320:
                possibleHashFunctions = [
                    "RIPEMD-320",
                ];
                break;
            case 384:
                possibleHashFunctions = [
                    "SHA-384",
                    "SHA3-384",
                    "ECOH-384",
                    "FSB-384",
                ];
                break;
            case 512:
                possibleHashFunctions = [
                    "SHA-512",
                    "SHA3-512",
                    "BLAKE-512",
                    "ECOH-512",
                    "FSB-512",
                    "Grøstl-512",
                    "JH",
                    "MD6",
                    "Spectral Hash",
                    "SWIFFT",
                    "Whirlpool",
                ];
                break;
            case 1024:
                possibleHashFunctions = [
                    "Fowler-Noll-Vo",
                ];
                break;
            default:
                possibleHashFunctions = [
                    "Unknown"
                ];
                break;
        }

        return output + possibleHashFunctions.join("\n");
    },

};

export default Hash;
