package nl.hsac.fitnesse.fixture.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;

/**
 * Helper to invoke (external) programs.
 */
public class ProgramHelper {
    private TimeoutHelper timeoutHelper;

    /**
     * @param timeoutHelper helper to use to ensure processes end.
     */
    public void setTimeoutHelper(TimeoutHelper timeoutHelper) {
        this.timeoutHelper = timeoutHelper;
    }

    /**
     * Calls a program and returns any output generated.
     * @param response details of what to invoke (output will be added).
     * @param timeout maximum time (in milliseconds) for program execution.
     */
    public void execute(ProgramResponse response, int timeout) {
        ProcessBuilder builder = createProcessBuilder(response);
        invokeProgram(builder, response, timeout);
    }

    private ProcessBuilder createProcessBuilder(ProgramResponse response) {
        List<String> command = new ArrayList<String>();
        command.add(response.getCommand());
        command.addAll(Arrays.asList(response.getArguments()));
        ProcessBuilder builder = new ProcessBuilder()
                                        .directory(response.getDirectory())
                                        .command(command);
        // set any explicit environment variables
        Map<String, String> respEnv = response.getEnvironment();
        if (respEnv != null && !respEnv.isEmpty()) {
            builder.environment().putAll(respEnv);
        }
        // store environment as used
        response.setEnvironment(builder.environment());
        return builder;
    }

    private void invokeProgram(ProcessBuilder builder,
                                ProgramResponse response,
                                int timeout) {
        final Process process;
        StreamConsumer stdOutConsumer;
        StreamConsumer stdErrConsumer;
        try {
            process = builder.start();
            stdOutConsumer = new StreamConsumer(process.getInputStream())
                                    .start();
            stdErrConsumer = new StreamConsumer(process.getErrorStream())
                                    .start();
        } catch (IOException e) {
            throw new RuntimeException(
                        "Unable to start: " + response.getCommand(), e);
        }

        try {
            Integer exitCode =
                        timeoutHelper.callWithTimeout(
                            response.getCommand(),
                            timeout,
                            new Callable<Integer>() {
                                    @Override
                                    public Integer call() throws Exception {
                                        return process.waitFor();
                                    }
                            });
            response.setExitCode(exitCode);
        } catch (RuntimeException e) {
            Throwable cause = e.getCause();
            if (cause instanceof TimeoutException) {
                process.destroy();
            }
            throw e;
        } finally {
            String stdOut = stdOutConsumer.getCurrentOutput();
            response.setStdOut(stdOut);

            String stdErr = stdErrConsumer.getCurrentOutput();
            response.setStdErr(stdErr);
        }
    }

    private static class StreamConsumer implements Runnable {
        private final InputStream stream;
        private final Thread thread;
        private final StringBuffer buffer = new StringBuffer();

        StreamConsumer(InputStream stream) {
            this.stream = stream;
            thread = new Thread(this);
        }

        StreamConsumer start() {
            thread.start();
            return this;
        }

        String getOutput() throws InterruptedException {
            thread.join();
            return getCurrentOutput();
        }

        String getCurrentOutput() {
            return buffer.toString();
        }

        @Override
        public void run() {
            BufferedReader brCleanUp =
                new BufferedReader(new InputStreamReader(stream));

            try {
                String line;
                while ((line = brCleanUp.readLine ()) != null) {
                    buffer.append(line);
                    buffer.append(System.lineSeparator());
                }
                 brCleanUp.close();
            } catch (IOException e) {
                throw new RuntimeException("Unable to consume output", e);
            }
        }
    }
}
