/*
 * 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.cryptolib.asymmetric.keypair.openssl;

import static java.nio.channels.Channels.newChannel;
import static java.text.MessageFormat.format;
import static java.util.Arrays.asList;
import static java.util.Arrays.fill;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Implementation of {@link OpenSSL}.
 */
class OpenSSLImpl implements OpenSSL {
    private static final int CHUNK_SIZE = 2048;

    private final ProcessFactory factory;

    /**
     * Constructor.
     *
     * @param factory
     */
    public OpenSSLImpl(final ProcessFactory factory) {
        this.factory = factory;
    }

    @Override
    public byte[] genpkey(final String algorithm,
            final Map<String, String> options) throws OpenSSLException {
        return execute(newGenpkeyCommand(algorithm, options));
    }

    @Override
    public String version() throws OpenSSLException {
        return new String(execute(newVersionCommand()),
            StandardCharsets.UTF_8);
    }

    private void checkExitCode(final Process process)
            throws OpenSSLException {
        int code;
        try {
            code = process.waitFor();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new OpenSSLException("Failed to get exit code.", e);
        }
        if (code != 0) {
            throw new OpenSSLException(
                format("Process failed with exit code ''{0}''.", code));
        }
    }

    private byte[] execute(final List<String> command)
            throws OpenSSLException {
        byte[] output;
        Process process = launchProcess(command);
        output = readOutput(process);
        checkExitCode(process);
        return output;
    }

    private Process launchProcess(final List<String> command)
            throws OpenSSLException {
        Process process;
        try {
            process = factory.createProcess(command);
        } catch (IOException e) {
            String message = format(
                "Failed to launch process for command ''{0}''", command);
            throw new OpenSSLException(message, e);
        }
        return process;
    }

    private List<String> newGenpkeyCommand(final String algorithm,
            final Map<String, String> options) {
        List<String> command = new ArrayList<>(10);
        command.add("openssl");
        command.add("genpkey");
        command.add("-outform");
        command.add("PEM");
        command.add("-algorithm");
        command.add(algorithm);
        if (!options.isEmpty()) {
            command.add("-pkeyopt");
            List<String> optionArg = new ArrayList<>(options.size());
            for (Map.Entry<String, String> entry : options.entrySet()) {
            	optionArg.add(entry.getKey() + ':' + entry.getValue());
            }
            command.add(optionArg.stream().collect(Collectors.joining(",")));
        }
        return command;
    }

    private List<String> newVersionCommand() {
        return asList("openssl", "version");
    }

    private byte[] readOutput(final Process process)
            throws OpenSSLException {
        List<ByteBuffer> chunks = new ArrayList<>();
        int length = 0;
        try (InputStream stream = process.getInputStream();
                ReadableByteChannel channel = newChannel(stream);) {
            ByteBuffer chunk = ByteBuffer.allocate(CHUNK_SIZE);
            while (channel.read(chunk) != -1) {
                if (!chunk.hasRemaining()) {
                    chunks.add(chunk);
                    length += chunk.position();
                    chunk = ByteBuffer.allocate(CHUNK_SIZE);
                }
            }
            chunks.add(chunk);
            length += chunk.position();
        } catch (IOException e) {
            throw new OpenSSLException(
                "Failed to read the process output.", e);
        }
        ByteBuffer output = ByteBuffer.allocate(length);
        for (ByteBuffer chunk : chunks) {
            chunk.flip();
            output.put(chunk);
            fill(chunk.array(), 0, chunk.limit(), (byte) 0);
        }
        return output.array();
    }
}
