/*
 * 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
 */
package com.scytl.products.ov.commons.dto;

import java.math.BigInteger;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalComputationsValues;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;

/**
 * Implementation of {@link ElGamalComputationsValuesCodec}.
 */
public final class ElGamalComputationsValuesCodecImpl
        implements ElGamalComputationsValuesCodec {
    private static final ElGamalComputationsValuesCodecImpl INSTANCE =
        new ElGamalComputationsValuesCodecImpl();

    private ElGamalComputationsValuesCodecImpl() {
    }

    /**
     * Returns the instance.
     * 
     * @return the instance.
     */
    public static ElGamalComputationsValuesCodecImpl getInstance() {
        return INSTANCE;
    }

    @Override
    public List<ElGamalComputationsValues> decodeList(String encoding)
            throws GeneralCryptoLibException {
        return new Decoder().decodeList(encoding);
    }

    @Override
    public Map<ElGamalComputationsValues, ElGamalComputationsValues> decodeMap(
            String encoding)
            throws GeneralCryptoLibException {
        return new Decoder().decodeMap(encoding);
    }

    @Override
    public ElGamalComputationsValues decodeSingle(String encoding)
            throws GeneralCryptoLibException {
        return new Decoder().decodeSingle(encoding);
    }

    @Override
    public String encodeList(List<ElGamalComputationsValues> list) {
        return new Encoder().encodeList(list);
    }

    @Override
    public String encodeMap(
            Map<ElGamalComputationsValues, ElGamalComputationsValues> map) {
        return new Encoder().encodeMap(map);
    }

    @Override
    public String encodeSingle(ElGamalComputationsValues values) {
        return new Encoder().encodeSingle(values);
    }

    private static class Decoder {
        private BigInteger[] bigIntegers;

        public List<ElGamalComputationsValues> decodeList(String encoding)
                throws GeneralCryptoLibException {
            byte[] bytes = Base64.getDecoder().decode(encoding);
            ByteBuffer buffer = ByteBuffer.wrap(bytes);
            decodeBigIntegers(buffer);
            int size = decodeSize(buffer);
            List<ElGamalComputationsValues> list = new ArrayList<>(size);
            for (int i = 0; i < size; i++) {
                list.add(decodeElgamalComputationsValues(buffer));
            }
            return list;
        }

        public Map<ElGamalComputationsValues, ElGamalComputationsValues> decodeMap(
                String encoding)
                throws GeneralCryptoLibException {
            byte[] bytes = Base64.getDecoder().decode(encoding);
            ByteBuffer buffer = ByteBuffer.wrap(bytes);
            decodeBigIntegers(buffer);
            int size = decodeSize(buffer);
            Map<ElGamalComputationsValues, ElGamalComputationsValues> map =
                new HashMap<>(size);
            for (int i = 0; i < size; i++) {
                ElGamalComputationsValues key =
                    decodeElgamalComputationsValues(buffer);
                ElGamalComputationsValues value =
                    decodeElgamalComputationsValues(buffer);
                map.put(key, value);
            }
            return map;
        }

        public ElGamalComputationsValues decodeSingle(String encoding)
                throws GeneralCryptoLibException {
            byte[] bytes = Base64.getDecoder().decode(encoding);
            ByteBuffer buffer = ByteBuffer.wrap(bytes);
            decodeBigIntegers(buffer);
            return decodeElgamalComputationsValues(buffer);
        }

        private void checkRemaining(ByteBuffer buffer, int required)
                throws GeneralCryptoLibException {
            if (buffer.remaining() < required) {
                throw new GeneralCryptoLibException(
                    "Not enough data to decode.");
            }
        }

        private BigInteger decodeBigInteger(ByteBuffer buffer)
                throws GeneralCryptoLibException {
            int size = decodeSize(buffer);
            checkRemaining(buffer, size);
            byte[] bytes = new byte[size];
            buffer.get(bytes);
            return new BigInteger(bytes);
        }

        private int decodeBigIntegerIndex(ByteBuffer buffer)
                throws GeneralCryptoLibException {
            return decodeInt(buffer);
        }

        private void decodeBigIntegers(ByteBuffer buffer)
                throws GeneralCryptoLibException {
            int size = decodeSize(buffer);
            bigIntegers = new BigInteger[size];
            for (int i = 0; i < size; i++) {
                bigIntegers[i] = decodeBigInteger(buffer);
            }
        }

        private ElGamalComputationsValues decodeElgamalComputationsValues(
                ByteBuffer buffer)
                throws GeneralCryptoLibException {
            int size = decodeSize(buffer);
            List<ZpGroupElement> elements = new ArrayList<>(size);
            for (int i = 0; i < size; i++) {
                elements.add(decodeZpGroupElement(buffer));
            }
            return new ElGamalComputationsValues(elements);
        }

        private int decodeInt(ByteBuffer buffer)
                throws GeneralCryptoLibException {
            checkRemaining(buffer, Integer.BYTES);
            return buffer.getInt();
        }

        private int decodeSize(ByteBuffer buffer)
                throws GeneralCryptoLibException {
            return decodeInt(buffer);
        }

        private ZpGroupElement decodeZpGroupElement(ByteBuffer buffer)
                throws GeneralCryptoLibException {
            int p = decodeBigIntegerIndex(buffer);
            int q = decodeBigIntegerIndex(buffer);
            int value = decodeBigIntegerIndex(buffer);
            return new ZpGroupElement(bigIntegers[value], bigIntegers[p],
                bigIntegers[q]);
        }

    }

    private static class Encoder {
        private final Map<BigInteger, Integer> bigIntegers =
            new LinkedHashMap<>();

        private final List<ByteBuffer> bigIntegerBuffers =
            new LinkedList<>();

        private final List<ByteBuffer> valuesBuffers = new LinkedList<>();

        public String encodeList(List<ElGamalComputationsValues> list) {
            encodeSize(list.size());
            list.forEach(this::encodeElGamalComputationsValues);
            encodeBigIntegers();
            return Base64.getEncoder().encodeToString(getBytes());
        }

        public String encodeMap(
                Map<ElGamalComputationsValues, ElGamalComputationsValues> map) {
            encodeSize(map.size());
            map.forEach((key, value) -> {
                encodeElGamalComputationsValues(key);
                encodeElGamalComputationsValues(value);
            });
            encodeBigIntegers();
            return Base64.getEncoder().encodeToString(getBytes());
        }

        public String encodeSingle(ElGamalComputationsValues values) {
            encodeElGamalComputationsValues(values);
            encodeBigIntegers();
            return Base64.getEncoder().encodeToString(getBytes());
        }

        private void encodeBigInteger(BigInteger value) {
            byte[] bytes = value.toByteArray();
            bigIntegerBuffers.add(encodeInt(bytes.length));
            bigIntegerBuffers.add(ByteBuffer.wrap(bytes));
        }

        private void encodeBigIntegerIndex(BigInteger value) {
            Integer index = bigIntegers.get(value);
            if (index == null) {
                index = bigIntegers.size();
                bigIntegers.put(value, index);
            }
            valuesBuffers.add(encodeInt(index));
        }

        private void encodeBigIntegers() {
            bigIntegerBuffers.add(encodeInt(bigIntegers.size()));
            bigIntegers.keySet().forEach(this::encodeBigInteger);
        }

        private void encodeElGamalComputationsValues(
                ElGamalComputationsValues values) {
            ZpGroupElement gamma = values.getGamma();
            List<ZpGroupElement> phis = values.getPhis();
            encodeSize(phis.size() + 1);
            encodeZpGroupElement(gamma);
            phis.forEach(this::encodeZpGroupElement);
        }

        private ByteBuffer encodeInt(int value) {
            return (ByteBuffer) ByteBuffer.allocate(Integer.BYTES)
                .putInt(value).flip();
        }

        private void encodeSize(int size) {
            valuesBuffers.add(encodeInt(size));
        }

        private void encodeZpGroupElement(ZpGroupElement element) {
            encodeBigIntegerIndex(element.getP());
            encodeBigIntegerIndex(element.getQ());
            encodeBigIntegerIndex(element.getValue());
        }

        private Stream<ByteBuffer> getBufferStream() {
            return Stream.concat(bigIntegerBuffers.stream(),
                valuesBuffers.stream());
        }

        private byte[] getBytes() {
            int length =
                getBufferStream().mapToInt(Buffer::remaining).sum();
            ByteBuffer bytes = ByteBuffer.allocate(length);
            getBufferStream().forEach(bytes::put);
            return bytes.array();
        }
    }
}
