/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
/* jshint ignore: start */
/* zglobal require */
/* zglobal forge */
/* zglobal CL */
/* zglobal OV */
/* zglobal self */



module.exports = (function() {

    'use strict';

    var XMLHttpRequest = XMLHttpRequest || require('xhr2');
    var _ = require('lodash');
    var Q = require('q');
    var config = require('./config.js');
    var session = require('./session.js');
    var crono = require('./crono.js')();


    // lenient json parse

    var jsonparse = function(x) {
        try {
            return JSON.parse(x);
        } catch (ignore) {
            return {};
        }
    };

    // progress notification

    var notifyProgress = function(pct) {

        if (typeof self != 'undefined') {
            self.postMessage(JSON.stringify({
                op: 'progress',
                args: pct
            }));
        }
    };

   
    // create a vote request

    var createVoteRequest = function(options, serializedEncrypterValues, precomputedPCC, precomputeProofValues, correctness, writeinValues, verificationCode, disableOptimizations) {

        var voteOptions;
        var writeinsOptions = [];
        var correctnessIds;
        var encryptedOptions;
        var partialChoiceCodes;
        var encryptedPCC;
        var schnorrProof;
        var cipherTextExponentiations;
        var exponentiationProof;
        var plaintextEqualityProof;
        var serializedEncryptedOptions;
        var serializedEncryptedPCC;
        var serializedCiphertextExponentiations;
        var dataToSign;
        var signature;
        var encrypterValues = null;

        // verification code

        if (!verificationCode) {
            verificationCode = '';
        }

        var numKeysAvailable = session('encParams').optionsEncryptionKey.getGroupElementsArray().length - 1;
        var maxPossibleNumWriteins = session('maximumPossibleNumWriteins');
        var allPossibleWriteinRepresentations = session('ballotWriteInsRepresentations');

        var shouldUseMultipleSubkeysForThisBallot = false;

        if(maxPossibleNumWriteins > numKeysAvailable) {
            throw new Error('Not enough subkeys to encrypt all possible writeins: maxPossibleNumWriteins: ' + maxPossibleNumWriteins + ', numKeysAvailable: ' + numKeysAvailable);
        }

        if(Array.isArray(writeinValues)) {
            var numReceivedWriteins = writeinValues.length;
            if(numReceivedWriteins > maxPossibleNumWriteins) {
                throw new Error('Received more writein values than the expected maximum possible number. Received: ' + numReceivedWriteins, ", max: " + maxPossibleNumWriteins);
            }
        }

        if (maxPossibleNumWriteins > 0) {
            shouldUseMultipleSubkeysForThisBallot = true;
        }

        // convert options to group elements

        voteOptions = options.map(function(o) {
            return new CL.commons.mathematical.ZpGroupElement(
                new forge.jsbn.BigInteger(o),
                session('encParams').p,
                session('encParams').q);
        });

        // get correctnessIds

        correctnessIds = options.map(function(o) {
            return correctness[o] || [];
        });

        if (serializedEncrypterValues) {
            encrypterValues = OV.deserializeEncrypterValues(serializedEncrypterValues);
        }

        // magically disable all precomputations

        if (disableOptimizations) {
            console.log('optimizations disabled');
            encrypterValues = null;
            precomputedPCC = [];
            precomputeProofValues = null;
        }

        crono.reset();

        // handle writein // add it to the message to be encrypted

        var numWriteinsElementsToEncrypt = 0;
        if (shouldUseMultipleSubkeysForThisBallot) {

            var expectedWriteinRepresentations = [];
            for(var i = 0; i < options.length; i++) {
                for(var j = 0; j < allPossibleWriteinRepresentations.length; j++) {
                    if(options[i] === allPossibleWriteinRepresentations[j]) {
                        expectedWriteinRepresentations.push(options[i]);
                        break;
                    }
                }
            }

            var writeinsAlphabet = session('writeInAlphabet');

            var listGroupElements = OV.encode(writeinValues, writeinsAlphabet, session('encParams').p, session('encParams').q, maxPossibleNumWriteins, expectedWriteinRepresentations);

            numWriteinsElementsToEncrypt = listGroupElements.length;

            for (var i = 0; i < numWriteinsElementsToEncrypt; i++) {
                var writeInEG = new CL.commons.mathematical.ZpGroupElement(
                    listGroupElements[i],
                    session('encParams').p,
                    session('encParams').q);
                writeinsOptions.push(writeInEG);
            }
        }

		var compressStandardOptionsAndConcatWriteinOptions = function(options, writeins, encryptionParms) {

        var compressedOptions = CL.commons.mathematical.groupUtils.compressGroupElements(encryptionParms.group, options);

        var standardAndWriteinsOptions = [compressedOptions].concat(writeins);

        return standardAndWriteinsOptions;
		};

	
        var whatToEncrypt = compressStandardOptionsAndConcatWriteinOptions(voteOptions, writeinsOptions, session('encParams'));

        // encrypt options

        crono.start('encrypt options');
        encryptedOptions = OV.encryptOptionsAndWriteins(
            whatToEncrypt,
            session('encParams'),
            encrypterValues);
        crono.stop();

        // generate partial choice codes

        crono.start('generate PCC');
        partialChoiceCodes = OV.generatePartialChoiceCodes(
            voteOptions,
            session('encParams'),
            session('verificationCardSecret'),
            precomputedPCC);
        crono.stop();

        // encrypt partial choice codes

        crono.start('encrypt PCC');
        encryptedPCC = OV.encryptPartialChoiceCodes(
            partialChoiceCodes,
            session('encParams'),
            encrypterValues);
        crono.stop();

        // signal 'half of work done'

        notifyProgress(50);

        // generate schnorr proof

        crono.start('schnorr');
        schnorrProof = OV.generateSchnorrProof(
            session('votingCardId'),
            config('electionEventId'),
            session('encParams'),
            encryptedOptions,
            precomputeProofValues);
        crono.stop();

        // generate cipher text exponentiations

        crono.start('cipher text exponentiations');
        cipherTextExponentiations = OV.generateCipherTextExponentiations(
            session('encParams'),
            encryptedOptions,
            session('verificationCardSecret'));
        crono.stop();

        // generate exponentiation proof

        crono.start('exponentiation proof');
        exponentiationProof = OV.generateExponentiationProof(
            session('encParams'),
            cipherTextExponentiations,
            session('verificationCardPublicKey').publicKey,
            precomputeProofValues);
        crono.stop();

        // generate plaintextEquality proof

        crono.start('plaintext equality proof');
        plaintextEqualityProof = OV.generatePlaintextEqualityProof(
            session('encParams'),
            encryptedOptions,
            encryptedPCC,
            session('verificationCardSecret'),
            cipherTextExponentiations,
            precomputeProofValues);
        crono.stop();

        // sign vote

        crono.start('sign vote');
        serializedEncryptedOptions =
            encryptedOptions.getGamma().getElementValue().toString() + ';' +
            encryptedOptions.getPhis()[0].getElementValue().toString();

        var serializedEncryptedWriteins = '';
        if (shouldUseMultipleSubkeysForThisBallot) {

            serializedEncryptedWriteins += encryptedOptions.getPhis()[1].getElementValue().toString();

            for(var i = 1; i < numWriteinsElementsToEncrypt; i++) {
                serializedEncryptedWriteins += ';';
                serializedEncryptedWriteins += encryptedOptions.getPhis()[i + 1].getElementValue().toString();
            }
        }

        serializedEncryptedPCC = encryptedPCC.getGamma().getElementValue().toString();
        _.each(encryptedPCC.getPhis(), function(phi) {
            serializedEncryptedPCC += (';' + phi.getElementValue().toString());
        });
        serializedCiphertextExponentiations =
            cipherTextExponentiations.cipherTextExponentiation[0].getElementValue().toString() + ';' +
            cipherTextExponentiations.cipherTextExponentiation[1].getElementValue().toString();

        var serializedCorrectnessIds = JSON.stringify(correctnessIds);

        dataToSign = [
            serializedEncryptedOptions,
            serializedEncryptedWriteins,
            serializedCorrectnessIds,
            session('verificationCardPublicKey').signature,
            session('authenticationToken').signature,
            schnorrProof.stringify(),
            session('votingCardId'),
            config('electionEventId')
        ];
        signature = OV.signData(
            dataToSign,
            session('credentials').voterPrivateKey);
        crono.stop();

        var reqData = {
            encryptedOptions: serializedEncryptedOptions,
            encryptedPartialChoiceCodes: serializedEncryptedPCC,
            correctnessIds: serializedCorrectnessIds,
            verificationCardPublicKey: session('verificationCardPublicKey').publicKey,
            verificationCardPKSignature: session('verificationCardPublicKey').signature,
            signature: signature,
            certificate: session('credentials').certificate,
            credentialId: session('credentials').credentialId,
            schnorrProof: schnorrProof.stringify(),
            exponentiationProof: exponentiationProof.stringify(),
            plaintextEqualityProof: plaintextEqualityProof.stringify(),
            cipherTextExponentiations: serializedCiphertextExponentiations,
            encryptedWriteIns: serializedEncryptedWriteins,
            verificationCode: verificationCode
        };

        return reqData;

    };
	
	// process vote repsonse

    var processVoteResponse = function(response) {

        return response.choiceCodes.split(';');

    };
	
	// process vote

    var processVote = function(deferred, response) {

        if (response.valid && response.choiceCodes) {

            try {
                var result = processVoteResponse(response);
                deferred.resolve(result);
            } catch (e) {
                deferred.reject(e.message);
            }

        } else {
            if (response.validationError) {
                deferred.reject(response);
            } else {
                deferred.reject('invalid vote');
            }
        }
    };
	
	
	// encrypt and send the vote

    var sendVote = function(options, encrypterValues, precomputedPCC, precomputeProofValues, correctness, writein, verificationCode, disableOptimizations) {

        var deferred = Q.defer();

        var voteRequestData = createVoteRequest(options, encrypterValues, precomputedPCC, precomputeProofValues, correctness, writein, verificationCode, disableOptimizations);

        // send vote

        var endpoint = config('endpoints.votes')
            .replace('{tenantId}', config('tenantId'))
            .replace('{electionEventId}', config('electionEventId'))
            .replace('{votingCardId}', session('votingCardId'));

        var xhr = new XMLHttpRequest();
        xhr.open('POST', config('host') + endpoint);
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {

                    processVote(deferred, JSON.parse(this.responseText));

                } else {

                    var response = jsonparse(this.responseText);
                    if (!response || typeof response !== 'object') {
                        response = {};
                    }
                    // IE11 fails to provide the 401 status to xhr2 (yields 0 instead)
                    response.httpStatus = xhr.status || 401;
                    response.httpStatusText = xhr.statusText;
                    deferred.reject(response);
                }
            }
        };
        xhr.onerror = function() {
            try {
                deferred.reject(xhr.status);
            } catch (e) {
				//This block is intentionally left blank
			}
        };
        xhr.setRequestHeader('Accept', 'application/json');
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.setRequestHeader('authenticationToken', JSON.stringify(session('authenticationToken')));
        xhr.send(JSON.stringify(voteRequestData));
		
        console.log(JSON.stringify(crono.getLog()));

        return deferred.promise;

    };

    return {
        sendVote: sendVote,
        createVoteRequest: createVoteRequest,
        processVoteResponse: processVoteResponse
    };

})();
