/*
 * 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
 */
/* global CL */
/* global forge */

module.exports = (function() {

    'use strict';

    var PADDING_INTEGER_VALUE_AS_STRING = '2';
    var NUM_CHARS_PER_ENCODED_CHARACTER = 3;
    var PADDING_CHARACTER = '0';
    var TWO_PADDING_CHARACTERS = PADDING_CHARACTER + PADDING_CHARACTER;

    // this value was determined empirically
    var FACTOR_TO_DETERMINE_NUM_CHARS_TO_ENCODE_IN_EACH_GROUP_ELEMENT = 20;

    var OFFSET = 2;

    // Note: the separator character is included in the alphabet as the first
    // character (but it cannot be used as a character in a write-in).
    var encode = function(fields, alphabet, p, q, numDesiredElementsInEncoding, usableRepresentationsEncode) {

        if(typeof numDesiredElementsInEncoding === 'undefined') {
            throw new Error(('The number of desired elements was not initialized'));
        } else if (numDesiredElementsInEncoding < 1) {
            throw new Error(('The number of desired elements was ' + numDesiredElementsInEncoding));
        }

        if((typeof fields === 'undefined')||(fields.length == 0)) {

            if((usableRepresentationsEncode == null) || (usableRepresentationsEncode.length == 0)){

                var encoding = [];
                var paddingIntegerValue = new forge.jsbn.BigInteger(PADDING_INTEGER_VALUE_AS_STRING);
                for (var i = 0; i < numDesiredElementsInEncoding; i++) {
                    encoding.push(paddingIntegerValue);
                }

                return encoding;

            } else {
                throw new Error('Exception during encoding - there are valid representations but no write-ins');
            }
        }

        basicValidationsOnEncodeInputs(fields, alphabet, p, q, numDesiredElementsInEncoding);

        var separator = alphabet.charAt(0);

        confirmThatRepsAreUniqueAndAppearExacltOnceInListOfValidReps(fields, usableRepresentationsEncode, separator);

        var encoding = performEncoding(fields, alphabet, separator, p, q);

        var numElementsInRealEncoding = encoding.length;

        if(numElementsInRealEncoding > numDesiredElementsInEncoding) {
            throw new Error('The encoding has more elements than the desired number. Desired number: '
                + numDesiredElementsInEncoding + ', actual number: ' + numElementsInRealEncoding);
        }

        var numPaddingElementsToBeAdded = numDesiredElementsInEncoding - numElementsInRealEncoding;
        var paddingIntegerValue = new forge.jsbn.BigInteger(PADDING_INTEGER_VALUE_AS_STRING);
        for (var i = 0; i < numPaddingElementsToBeAdded; i++) {
            encoding.push(paddingIntegerValue);
        }

        return encoding;
    };

    var confirmThatRepsAreUniqueAndAppearExacltOnceInListOfValidReps = function(fields, validWriteInRepresentations, separator) {

        var validWriteInRepresentationsAsSet = new Set();
        for(var i = 0; i<validWriteInRepresentations.length; i++) {
            validWriteInRepresentationsAsSet.add(validWriteInRepresentations[i]);
        }
        var setToConfirmRepsUnique = new Set();

        for(var i = 0; i < fields.length; i++) {

            var parts = fields[i].split(separator);
            var rep = parts[0];

            if(setToConfirmRepsUnique.has(rep)) {
                throw Error('Exception during encoding - repeated representation: ' + rep);
            } else {
                setToConfirmRepsUnique.add(rep);
            }

            if(!validWriteInRepresentationsAsSet.delete(parts[0])) {
                throw Error('Exception during encoding - invalid representation found in encoded write-in: ' + parts[0]);
            }
        }

        if(validWriteInRepresentationsAsSet.size != 0) {
            var unusedRepresentations = [];
            validWriteInRepresentationsAsSet.forEach(item => { unusedRepresentations.push(item); });
            throw Error('Exception during encoding - representations not used: ' + unusedRepresentations);
        }
    };

    var decode = function(encoding, alphabet, p, usableRepresentationsDecode) {

        basicValidationsOnDecodeInputs(encoding, alphabet, p);

        var separator = alphabet.charAt(0);

        var numNonPaddingElements = findNumberOfNonPaddingElements(encoding);
        var nonPaddingElements = encoding.slice(0, numNonPaddingElements);

        return performDecoding(nonPaddingElements, alphabet, separator, p, usableRepresentationsDecode);
    };

    var findNumberOfNonPaddingElements = function(encoding) {

        var paddingIntegerValue = new forge.jsbn.BigInteger(PADDING_INTEGER_VALUE_AS_STRING);

        var numPaddingElements = 0;

        for(var i = encoding.length - 1; i >= 0; i--) {

            if(encoding[i].equals(paddingIntegerValue)) {
                numPaddingElements++;
            }
        }

        return encoding.length - numPaddingElements;
    };

    var performEncoding = function(fields, alphabet, separator, p, q) {

        var numCharsThatCanBeEncodedInEachGroupElement =
            Math.floor(p.bitLength() / FACTOR_TO_DETERMINE_NUM_CHARS_TO_ENCODE_IN_EACH_GROUP_ELEMENT);

        var concatenatedWithSeparators = fields.join(separator);
        var returnList = [];
        var numCharsAddedToThisGroupElement = 0;
        var encoded = "";

        for (var i = 0; i < concatenatedWithSeparators.length; i++) {

            var character = concatenatedWithSeparators.charAt(i);

            var indexOfCharacterInAlphabet = alphabet.indexOf(character) + 1;
            if (indexOfCharacterInAlphabet < 1) {
                throw new Error('Invalid character for writein encoding: ' + character);
            }

            indexOfCharacterInAlphabet += OFFSET;
            encoded += getStringRepresentationAndAddPaddingCharsIfNecessary(indexOfCharacterInAlphabet)
            numCharsAddedToThisGroupElement++;

            if(numCharsAddedToThisGroupElement === numCharsThatCanBeEncodedInEachGroupElement) {
                returnList.push(squareToEnsureIsGroupElement(new forge.jsbn.BigInteger(encoded), p, q));
                var encoded = "";
                numCharsAddedToThisGroupElement = 0;
            }
        }
        if (numCharsAddedToThisGroupElement != 0) {
            returnList.push(squareToEnsureIsGroupElement(new forge.jsbn.BigInteger(encoded), p, q));
        }

        return returnList;
    };

    var performDecoding = function(encoding, alphabet, separator, p, usableWriteInRepresentations) {

        if((encoding === 'undefined') || (encoding.length == 0) ) {
            if((usableWriteInRepresentations === 'undefined')||(usableWriteInRepresentations.length == 0)) {
                var emptyEncoding = [];
                return emptyEncoding;                
            } else {
                throw new Error(('Exception during decoding - there are valid representations but no write-ins'));
            }
        }

        var sb = '';
        for (var i = 0; i < encoding.length; i++) {

            var originalIntegerValueAsString = sqrtN(encoding[i].mod(p)).toString();

            var numPaddingCharsNeeded = determineNumPaddingCharsNeeded(originalIntegerValueAsString);

            for (var j = 0; j < numPaddingCharsNeeded; j++) {
                sb += PADDING_CHARACTER;
            }
            sb += originalIntegerValueAsString;
        }

        var originalConcatenatedString = decodeConcatenatedString(sb, alphabet);
        var bothPartsOfAllStrings = originalConcatenatedString.split(separator);
        return handleSeparatorsWithinEachString(bothPartsOfAllStrings, separator, usableWriteInRepresentations);
    };

    var handleSeparatorsWithinEachString = function(originalIndividualStrings, separator, usableWriteInRepresentations) {

        confirmAllElementsUnique(originalIndividualStrings);

        var usableWriteInRepresentationsAsSet = new Set();
        for(var i = 0; i<usableWriteInRepresentations.length; i++){
            usableWriteInRepresentationsAsSet.add(usableWriteInRepresentations[i])
        }
        var listItems = [];
        for (var i = 0; i < originalIndividualStrings.length; i = i + 2) {
			
			if(!originalIndividualStrings[i + 1]) {
				throw new Error(('Exception during decoding - empty write-in is not valid'));
			}

            if(usableWriteInRepresentationsAsSet.delete(originalIndividualStrings[i])) {
                listItems.push(originalIndividualStrings[i] + separator + originalIndividualStrings[i + 1]);
            } else {
                throw new Error(('Exception during decoding - invalid representation found in encoded write-in: ' + originalIndividualStrings[i]));
            }
        }
        if (!usableWriteInRepresentationsAsSet.length == 0) {
            var unusedRepresentations = [];
            usableWriteInRepresentationsAsSet.forEach(item => { unusedRepresentations.push(item); });
            throw new Error(('Exception during decoding - representations not used: ' + unusedRepresentations));
        }
        return listItems;
    };

    var confirmAllElementsUnique = function(originalIndividualStrings) {

        var setOfReps = new Set();
        for (var i = 0; i < originalIndividualStrings.length; i = i + 2) {
            if(setOfReps.has(originalIndividualStrings[i])) {
                throw new Error(('Exception during decoding - repeated representation: ' + originalIndividualStrings[i]));
            } else {
                setOfReps.add(originalIndividualStrings[i]);
            }
        }
    };

    var getStringRepresentationAndAddPaddingCharsIfNecessary = function(index) {
        var passingAndIndex = TWO_PADDING_CHARACTERS + index;
        return passingAndIndex.substr(passingAndIndex.length - NUM_CHARS_PER_ENCODED_CHARACTER);
    };

    var squareToEnsureIsGroupElement = function(value, p, q) {

        if (value.compareTo(q) > 0) {
            throw new Error(('Encoded writein too big for the mathematical group. Value: ' + value + ", p: " + p));
        }
        return value.pow(2).mod(p);
    };

    var determineNumPaddingCharsNeeded = function(originalIntegerValue) {

        var numPaddingCharsNeeded =
            NUM_CHARS_PER_ENCODED_CHARACTER - (originalIntegerValue.length % NUM_CHARS_PER_ENCODED_CHARACTER);
        if (numPaddingCharsNeeded == NUM_CHARS_PER_ENCODED_CHARACTER) {
            numPaddingCharsNeeded = 0;
        }

        return numPaddingCharsNeeded;
    };

    var decodeConcatenatedString = function(toBeDecoded, alphabet) {

        var length = toBeDecoded.length;
        var sb = '';

        for (var i = 0; i < length; i = i + NUM_CHARS_PER_ENCODED_CHARACTER) {

            var indexOfCharacterInAlphabet = toBeDecoded.substring(i, i + NUM_CHARS_PER_ENCODED_CHARACTER);
            var character = alphabet.charAt((indexOfCharacterInAlphabet - 1) - OFFSET);
            sb += character;
        }
        return sb;
    };

    var sqrtN = function (input) {

        var TWO = new forge.jsbn.BigInteger("2");
        var c;

        // Significantly speed-up algorithm by proper select of initial approximation
        // As square root has 2 times less digits as original value
        // we can start with 2^(length of N1 / 2)
        var n0 = TWO.pow(input.bitLength() / 2);
        // Value of approximate value on previous step
        var np = input;

        do {
            // next approximation step: n0 = (n0 + in/n0) / 2
            n0 = n0.add(input.divide(n0)).divide(TWO);

            // compare current approximation with previous step
            c = np.compareTo(n0);

            // save value as previous approximation
            np = n0;

            // finish when previous step is equal to current
        } while (c != 0);

        return n0;
    };

    var basicValidationsOnEncodeInputs = function (fields, alphabet, p, q, numDesiredElementsInEncoding) {

        if((typeof alphabet === 'undefined') || (alphabet.length == 0)) {
            throw new Error(('Exception during encoding - the alphabet was not initialized'));
        }

        var separator = alphabet.charAt(0);

        for(var i = 0; i < fields.length; i++) {

            var parts = fields[i].split(separator);
            if(parts.length != 2) {
                throw new Error('There should be exactly one separator character in each writein string, but this was not the case for: ' + fields[i]);
            }
        }

        if(typeof p === 'undefined') {
            throw new Error(('The p parameter was not initialized'));
        }

        if(typeof q === 'undefined') {
            throw new Error(('The q parameter was not initialized'));
        }
    };

    var basicValidationsOnDecodeInputs = function (encoding, alphabet, p) {

        if(typeof alphabet === 'undefined') {
            throw new Error(('The alphabet was null'));
        } else if (alphabet.length == 0) {
            throw new Error(('The alphabet was empty'));
        }

        if(typeof p === 'undefined') {
            throw new Error(('The p parameter was null'));
        } 
    };

    // @param {BigInteger} the value to convert into a group element
    // @param {BigInteger} the group's p value
    // @return {BigInteger] the group elemnt

    var makeGroupElement = function(number, p) {
        
        if ( number.compareTo(p) > 0 ) {
            throw new Error(('Encoded writein too big for the mathematical group'));
        }

        var two = new forge.jsbn.BigInteger('2');
        return number.pow(two).mod(p);

    };

    return {
        encode: encode,
        decode: decode,
        makeGroupElement: makeGroupElement,
        squareToEnsureIsGroupElement: squareToEnsureIsGroupElement,
        sqrtN: sqrtN
    };

})();
