# src/ballotBox.ts

    import { ProxyVote } from './types'

    const BN = require('bn.js')
    import * as R from 'ramda'
    import * as assert from 'assert'
    import * as web3Utils from 'web3-utils'
    import * as svCrypto from './crypto'
    import axios from 'axios'
    import * as Light from './light'

    import BBFarmAbi from './smart_contracts/BBFarm.abi.json'

    /**
     * This object tracks the flags used for SV ballot boxes. They determine the submission
     * methods and whether ballots are tracked as binding, official, or testing.
     *
     * For more info see docs.secure.vote
     */
    export const flags = {
        // flags on submission methods
        USE_ETH: 2 ** 0,
        USE_SIGNED: 2 ** 1,
        USE_NO_ENC: 2 ** 2,
        USE_ENC: 2 ** 3,

        // other ballot settings
        IS_BINDING: 2 ** 13,
        IS_OFFICIAL: 2 ** 14,
        USE_TESTING: 2 ** 15
    }

    /**
     * Creates a packed copy of start and end times with submissionBits
     *
     * @param {number} start
     *  Start time in seconds since epoch
     * @param {number} end
     *  End time in seconds since epoch
     * @param {number} submissionBits
     *  Submission bits - can be created using mkSubmissionBits
     * @returns {BN}
     *  Returns a `bn.js` BigNum of the packed values.
     *  Format: [submissionBits(16)][startTime(64)][endTime(64)]
     */
    export const mkPacked = (start, end, submissionBits) => {
        const max64Bit = new BN('ffffffffffffffff', 16)

        const s = new BN(start)
        assert.equal(s.lte(max64Bit) && s.gtn(0), true, 'start time must be >0 and <2^64')

        const e = new BN(end)
        assert.equal(e.lte(max64Bit) && e.gtn(0), true, 'end time must be >0 and <2^64')

        const sb = new BN(submissionBits)
        assert.equal(sb.ltn(2 ** 16) && sb.gtn(0), true, 'submission bits must be >0 and <2^16') // note: submission bits of 0 is invalid

        return sb
            .shln(64)
            .add(s)
            .shln(64)
            .add(e)
    }

    /**
     * This combines flags into a finished submissionBits. It also does some validation.
     * @param {number[]} toCombine
     *  Array of all submission flags to combine. See SV.ballotBox.flags for flag options.
     *  All flags must be a power of 2 (which indicates they occupy a single bit in the number when combining).
     * @returns {number}
     *  A 16 bit integer of combined flags.
     */
    export const mkSubmissionBits = (...toCombine) => {
        const toRet = R.reduce((acc, i) => acc | i, 0, toCombine)
        assert.equal(
            R.all(i => typeof i == 'number', toCombine),
            true,
            `Bad input to mkSubmissionBits. Input is required to be an array of numbers. Instead got: ${toCombine}`
        )
        assert.equal(
            R.all(i => i === (i | 0), toCombine),
            true,
            `Bad input to mkSubmissionBits. Input was not an array of integers. Instead got: ${toCombine}`
        )
        assert.equal(
            toRet,
            R.sum(toCombine),
            `Bad input provided to mkSubmissionBits. Logical OR and sum sanity check failed. Input was: ${toCombine}`
        )
        assert.equal(toRet < 2 ** 16, true, `Submission bits must fit into a 16 bit integer (i.e. less than 2^16). Result was: ${toRet}`)
        return toRet
    }

    /**
     * Take the arguments and produce web3 data fitting the `submitProxyVote` method.
     * @param {string} ballotId
     *  a BN.js or Hex ballotId
     * @param {number} sequence
     *  the sequence number to use (0 < sequence < 2^32)
     * @param {string} voteData
     *  the vote data to use, should be 32 bytes hex encoded
     * @param {string} extra
     *  any extra data included with the vote (such as curve25519 pubkeys)
     * @param {string} privateKey
     *  the privkey used to sign
     * @param {object?} opts
     *  options:
     *   - skipSequenceSizeCheck: boolean (will not throw if sequence is >= 2^32)
     * @returns {object}
     *  { proxyReq (bytes32[5]), extra (bytes) } in the required format for `submitProxyVote`
     */
    export const mkSignedBallotForProxy = (ballotId, sequence, voteData, extra, privateKey, opts: any = {}): ProxyVote => {
        if (opts.skipSequenceSizeCheck !== true) assert.equal(0 < sequence && sequence < 2 ** 32, true, 'sequence number out of bounds')
        assert.equal(
            web3Utils.isHexStrict(ballotId) || web3Utils.isBN(ballotId),
            true,
            'ballotId incorrect format (either not a BN or not hex)'
        )
        assert.equal(web3Utils.isHexStrict(voteData), true, 'vote data is not hex (strict)')
        assert.equal(web3Utils.isHexStrict(extra), true, 'extra param is not hex (strict)')

        const _ballotId = web3Utils.isBN(ballotId) ? web3Utils.padLeft(web3Utils.toHex(ballotId), 64) : ballotId

        assert.equal(_ballotId.length, 66, 'ballotId incorrect length')
        assert.equal(voteData.length, 66, 'voteData incorrect length')

        const sequenceHex = web3Utils.padLeft(web3Utils.toHex(sequence), 8)

        const messageHash = web3Utils.soliditySha3(
            { t: 'bytes31', v: web3Utils.padLeft(sequenceHex, '62') },
            { t: 'bytes32', v: _ballotId },
            { t: 'bytes32', v: voteData },
            { t: 'bytes', v: extra }
        )

        const { v, r, s } = svCrypto.ethSignHash(messageHash, privateKey)

        const vBytes = web3Utils.hexToBytes(v)
        const midBytes = web3Utils.hexToBytes(web3Utils.padRight('0x', 54))
        const sequenceBytes = web3Utils.hexToBytes(sequenceHex)
        const packed2Bytes = R.concat(vBytes, R.concat(midBytes, sequenceBytes))
        const packed2 = web3Utils.bytesToHex(packed2Bytes)

        return {
            proxyReq: [r, s, packed2, _ballotId, voteData],
            extra
        }
    }

    /**
     * Verify a signed vote to be submitted via proxy as generated by `mkSignedBallotForProxy`
     *
     * @param {ProxyVote} proxyVote The ProxyVote object
     * @param {*} [opts={}] Not used currently; for future options
     * @returns {{verified: bool, address: EthAddress}}
     */
    export const verifySignedBallotForProxy = (proxyVote: ProxyVote, opts: any = {}) => {
        const {
            proxyReq: [r, s, packed2, ballotId, voteData],
            extra
        } = proxyVote

        const p2Bytes = web3Utils.hexToBytes(packed2)
        const v = web3Utils.bytesToHex(p2Bytes.slice(0, 1))
        const seqNum = web3Utils.bytesToHex(p2Bytes.slice(27, 32))

        const messageHash = web3Utils.soliditySha3(
            { t: 'bytes31', v: web3Utils.bytesToHex(p2Bytes.slice(1)) },
            { t: 'bytes32', v: ballotId },
            { t: 'bytes32', v: voteData },
            { t: 'bytes', v: extra }
        )

        return svCrypto.ethVerifySig(messageHash, [v, r, s])
    }

    /**
     * Prepares voteData for a Range3 ballot from an array of votes
     *
     * @param {array} votesArray
     *  Takes an array of numbers which represent the votes to be transformed
     *  Format: [1, 2, -1]
     *
     * @returns {string}
     *  Returns an eth hex string of the vote data
     */
    export const genRange3VoteData = (votesArray: number[]) => {
        assert.equal(R.all(v => (v | 0) === v, votesArray), true, 'All array elements must be defined and integers.')
        assert.equal(R.all(v => -3 <= v && v <= 3, votesArray), true, 'Votes must be in range -3 to 3.')
        assert.equal(votesArray.length <= 85, true, 'Too many votes; maximum capacity of 32 bytes is 85 individual items.')

        // Generate list of binary encoded votes. Read bottom to top.
        const binaryVotes = R.compose(
            // pad to 3 bits
            R.map((vBin: string) => R.join('', R.repeat('0', 3 - vBin.length)) + vBin),
            // convert votes to binary
            R.map((v: number) => v.toString(2)),
            // offset votes to be in range 0,6
            R.map((v: number) => v + 3)
        )(votesArray)

        // check we have converted votes to bitstring representation of length 3
        assert.equal(R.all(bVote => bVote.length == 3, binaryVotes), true, 'Assertion failed: all binary-encoded votes should be 3 bits long')

        // create the binary voteData
        const rawBinVotes = R.join('', binaryVotes)
        // and pad it with 0s to length 256 (32 bytes total)
        const binVoteData = rawBinVotes + R.join('', R.repeat('0', 32 * 8 - rawBinVotes.length))
        assert.equal(binVoteData.length, 256, 'Assertion failed: generated voteData bit string does not have length 256')
        // Convert to bytes
        const voteBytes = R.map(bStr => parseInt(bStr, 2), R.splitEvery(8, binVoteData))

        // check bytes are in range
        assert.equal(
            R.all(vByte => 0 <= vByte && vByte <= 255, voteBytes),
            true,
            'Assertion failed: voteBytes (byte array) had a byte out of bounds (<0 or >255)'
        )

        // generate final hex
        const voteData = web3Utils.bytesToHex(voteBytes)
        assert.equal(voteData.length, 66, 'Assertion failed: final hex was not 66 characters long (32 bytes)')

        return voteData
    }

    /**
     * Prepares a transaction for sending with the users web3 browser
     *
     * @param {object} txInfo
     *  Object literal containing the information required to generate the web3 transaction.
     * @param {object} svNetwork
     *  config object containing svNetwork
     *
     * @returns {object}
     *  Returns an object with all fields required to cast the transaction
     */
    export const prepareWeb3BBVoteTx = async ({ txInfo }, { svNetwork }) => {
        const { bbFarm, ballotId, userAddress, voteData } = txInfo
        const { web3 } = svNetwork

        assert.equal(web3Utils.isAddress(bbFarm), true, 'BBFarm address supplied is not a valid ethereum address.')
        assert.equal(web3Utils.isAddress(userAddress), true, 'User address supplied is not a valid ethereum address.')
        assert.equal(voteData.length, 66, 'Assertion failed: final hex was not 66 characters long (32 bytes)')

        const BBFarmContract = new web3.eth.Contract(BBFarmAbi, bbFarm)
        const submitVote = BBFarmContract.methods.submitVote(ballotId, voteData, '0x')
        const gasEstimate = await submitVote.estimateGas()
        const abiValue = await submitVote.encodeABI()
        const gasPrice = await Light.getCurrentGasPrice()

        const web3Tx = {
            to: bbFarm,
            data: abiValue,
            gas: web3.utils.toHex((gasEstimate * 1.05) | 0), // 5% added just in case
            gasPrice: gasPrice.average * 1000000000,
            from: userAddress
        }
        return web3Tx
    }

    export const castProxyVote = async (request, svConfig) => {
        assert.equal(web3Utils.isBN(request.ballotId), true, 'Ballot id is not a BN')
        assert.equal(request.proxyReq.length == 5, true, 'Proxy vote req does not contain the correct number of parameters')
        assert.equal(
            request.hasOwnProperty('extra') && request.hasOwnProperty('democHash'),
            true,
            'Request does not contain extra and democ hash data'
        )

        return new Promise((resolve, reject) => {
            const svApiUrl = svConfig.svApiUrl
            const proxyVotePath = '/sv/light/submitProxyVote'
            const requestUrl = `${svApiUrl}${proxyVotePath}`
            axios
                .post(requestUrl, request)
                .then(response => {
                    const { data } = response
                    resolve(data)
                })
                .catch(error => {
                    console.log('error :', error.response)
                    reject(error)
                })
        })
    }