'use strict'

/*  ------------------------------------------------------------------------ */

const CryptoJS = require ('../../static_dependencies/crypto-js/crypto-js')
const { capitalize } = require ('./string')
const { stringToBase64, urlencodeBase64 } = require ('./encode')
const NodeRSA = require ('./../../static_dependencies/node-rsa/NodeRSA')
const { binaryToBase58, byteArrayToWordArray } = require ('./encode')
const elliptic = require ('./../../static_dependencies/elliptic/lib/elliptic')
const EC = elliptic.ec
const EDDSA = elliptic.eddsa
const { ArgumentsRequired } = require ('./../errors')
const BN = require ('../../static_dependencies/BN/bn.js')

/*  ------------------------------------------------------------------------ */

const hash = (request, hash = 'md5', digest = 'hex') => {
    const options = {}
    if (hash === 'keccak') {
        hash = 'SHA3'
        options['outputLength'] = 256
    }
    const result = CryptoJS[hash.toUpperCase ()] (request, options)
    return (digest === 'binary') ? result : result.toString (CryptoJS.enc[capitalize (digest)])
}

/*  .............................................   */

const hmac = (request, secret, hash = 'sha256', digest = 'hex') => {
    const result = CryptoJS['Hmac' + hash.toUpperCase ()] (request, secret)
    if (digest) {
        const encoding = (digest === 'binary') ? 'Latin1' : capitalize (digest)
        return result.toString (CryptoJS.enc[capitalize (encoding)])
    }
    return result
}

/*  .............................................   */

function rsa (request, secret, alg = 'RS256') {
    const algos = {
        'RS256': 'pkcs1-sha256',
        'RS512': 'pkcs1-sha512',
    }
    if (!(alg in algos)) {
        throw new ExchangeError (alg + ' is not a supported rsa signing algorithm.')
    }
    const algorithm = algos[alg]
    let key = new NodeRSA (secret, {
        'environment': 'browser',
        'signingScheme': algorithm,
    })
    return key.sign (request, 'base64', 'binary')
}


/**
 * @return {string}
 */
function jwt (request, secret, alg = 'HS256') {
    const algos = {
        'HS256': 'sha256',
        'HS384': 'sha384',
        'HS512': 'sha512',
    };
    const encodedHeader = urlencodeBase64 (stringToBase64 (JSON.stringify ({ 'alg': alg, 'typ': 'JWT' })))
    const encodedData = urlencodeBase64 (stringToBase64 (JSON.stringify (request)))
    const token = [ encodedHeader, encodedData ].join ('.')
    const algoType = alg.slice (0, 2);
    const algorithm = algos[alg]
    let signature = undefined
    if (algoType === 'HS') {
        signature = urlencodeBase64 (hmac (token, secret, algorithm, 'base64'))
    } else if (algoType === 'RS') {
        signature = urlencodeBase64 (rsa (token, secret, alg))
    }
    return [ token, signature ].join ('.')
}

function ecdsa (request, secret, algorithm = 'p256', hashFunction = undefined, fixedLength = false) {
    let digest = request
    if (hashFunction !== undefined) {
        digest = hash (request, hashFunction, 'hex')
    }
    const curve = new EC (algorithm)
    let signature = curve.sign (digest, secret, 'hex',  { 'canonical': true })
    let counter = new BN ('0')
    const minimum_size = new BN ('1').shln (8 * 31).sub (new BN ('1'))
    while (fixedLength && (signature.r.gt (curve.nh) || signature.r.lte (minimum_size) || signature.s.lte (minimum_size))) {
        signature = curve.sign (digest, secret, 'hex',  { 'canonical': true, 'extraEntropy': counter.toArray ('le', 32)})
        counter = counter.add (new BN ('1'))
    }
    return {
        'r': signature.r.toString (16).padStart (64, '0'),
        's': signature.s.toString (16).padStart (64, '0'),
        'v': signature.recoveryParam,
    }
}


function eddsa (request, secret, algorithm = 'ed25519') {
    // used for waves.exchange (that's why the output is base58)
    const curve = new EDDSA (algorithm)
    const signature = curve.signModified (request, secret)
    return binaryToBase58 (byteArrayToWordArray (signature.toBytes ()))
}

/*  ------------------------------------------------------------------------ */

const totp = (secret) => {

    const dec2hex = s => ((s < 15.5 ? '0' : '') + Math.round (s).toString (16))
        , hex2dec = s => parseInt (s, 16)
        , leftpad = (s, p) => (p + s).slice (-p.length) // both s and p are short strings

    const base32tohex = (base32) => {
        let base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
        let bits = ''
        let hex = ''
        for (let i = 0; i < base32.length; i++) {
            let val = base32chars.indexOf (base32.charAt (i).toUpperCase ())
            bits += leftpad (val.toString (2), '00000')
        }
        for (let i = 0; i + 4 <= bits.length; i += 4) {
            let chunk = bits.substr (i, 4)
            hex = hex + parseInt (chunk, 2).toString (16)
        }
        return hex
    }

    const getOTP = (secret) => {
        secret = secret.replace (' ', '') // support 2fa-secrets with spaces like "4TDV WOGO" → "4TDVWOGO"
        let epoch = Math.round (new Date ().getTime () / 1000.0)
        let time = leftpad (dec2hex (Math.floor (epoch / 30)), '0000000000000000')
        let hmacRes = hmac (CryptoJS.enc.Hex.parse (time), CryptoJS.enc.Hex.parse (base32tohex (secret)), 'sha1', 'hex')
        let offset = hex2dec (hmacRes.substring (hmacRes.length - 1))
        let otp = (hex2dec (hmacRes.substr (offset * 2, 8)) & hex2dec ('7fffffff')) + ''
        otp = (otp).substr (otp.length - 6, 6)
        return otp
    }

    return getOTP (secret)
}

/*  ------------------------------------------------------------------------ */

module.exports = {
    hash,
    hmac,
    jwt,
    totp,
    rsa,
    ecdsa,
    eddsa,
}

/*  ------------------------------------------------------------------------ */
