/*
 * $Id$
 * @author aakimov
 * @date   Jul 1, 2016 9:35:16 AM
 *
 * Copyright (C) 2016 Scytl Secure Electronic Voting SA
 *
 * All rights reserved.
 */
package com.scytl.math;

import static com.scytl.math.BigIntegers.isOne;
import static com.scytl.math.BigIntegers.modMultiply;
import static com.scytl.math.BigIntegers.modSquare;

import java.math.BigInteger;
import java.util.List;

/**
 * Implementation of {@link MultiExponentiator} which uses Lim-Lee algorithm
 * from {@link http://dasan.sejong.ac.kr/~chlim/pub/multi_exp.ps}.
 * 
 * @author aakimov
 */
class LimLeeMultiExponentiator extends AbstractMultiExponentiator {
    private final int window;
    private final int windowCount;
    private final BigInteger[][] factors;

    /**
     * Constructor.
     * 
     * @param bases
     * @param modulus
     * @param maximumBitLengthOfExponent
     */
    public LimLeeMultiExponentiator(List<BigInteger> bases, BigInteger modulus,
            int maximumBitLengthOfExponent) {
        super(bases, modulus, maximumBitLengthOfExponent);
        window = chooseWindow(maximumBitLengthOfExponent);
        windowCount = getWindowCount(this.bases.length, window);
        factors = computeFactors(this.bases, modulus, window, windowCount);
    }

    private static int chooseWindow(int maximumBitLengthOfExponent) {
        int window;
        // the constants are borrowed from the article mentioned above.
        if (maximumBitLengthOfExponent <= 10) {
            window = 2;
        } else if (maximumBitLengthOfExponent <= 24) {
            window = 3;
        } else if (maximumBitLengthOfExponent <= 60) {
            window = 4;
        } else if (maximumBitLengthOfExponent <= 144) {
            window = 5;
        } else if (maximumBitLengthOfExponent <= 342) {
            window = 6;
        } else if (maximumBitLengthOfExponent <= 797) {
            window = 7;
        } else if (maximumBitLengthOfExponent <= 1828) {
            window = 8;
        } else {
            window = 9;
        }
        return window;
    }

    private static BigInteger[][] computeFactors(BigInteger[] bases,
            BigInteger modulus, int window, int windowCount) {
        BigInteger[][] factors = new BigInteger[windowCount][];
        for (int i = 0; i < factors.length; i++) {
            int offset = i * window;
            int baseCount = window;
            if (i == factors.length - 1) {
                baseCount = bases.length - offset;
            }
            factors[i] = new BigInteger[1 << baseCount];
            for (int j = 0; j < factors[i].length; j++) {
                factors[i][j] = BigInteger.ONE;
                for (int k = 0; k < baseCount; k++) {
                    if ((j & (1 << (baseCount - 1 - k))) > 0) {
                        BigInteger base = bases[offset + k];
                        factors[i][j] = modMultiply(factors[i][j], base,
                                modulus);
                    }
                }
            }
        }
        return factors;
    }

    private static int getWindowCount(int baseCount, int window) {
        int count = baseCount / window;
        if (baseCount % window > 0) {
            count++;
        }
        return count;
    }

    @Override
    protected BigInteger doExponentiate(BigInteger[] exponents) {
        BigInteger value = BigInteger.ONE;
        for (int i = maximumBitLengthOfExponent() - 1; i >= 0; i--) {
            if (!isOne(value)) {
                value = modSquare(value, modulus());
            }
            for (int j = 0; j < windowCount; j++) {
                int offset = j * window;
                int exponentCount = window;
                if (j == windowCount - 1) {
                    exponentCount = exponents.length - offset;
                }
                int index = 0;
                for (int k = 0; k < exponentCount; k++) {
                    index <<= 1;
                    if (exponents[offset + k].testBit(i)) {
                        index++;
                    }
                }
                value = modMultiply(value, factors[j][index], modulus());
            }
        }
        return value;
    }
}
