package com.atlassian.bitbucket.mesh.util;

import com.atlassian.bitbucket.mesh.process.OsProcess;
import com.atlassian.bitbucket.mesh.process.ProcessStdioHandler;

import javax.annotation.Nonnull;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.function.BiConsumer;

import static java.util.Objects.requireNonNull;
import static org.mockito.Mockito.mock;

public class ProcessStdioHandlerUtils {

    private static final int DEFAULT_BUFFER_SIZE = 15;

    private ProcessStdioHandlerUtils() {
        throw new UnsupportedOperationException("Instance creation is not supported");
    }

    public static void handleStderr(@Nonnull ProcessStdioHandler<?> handler, @Nonnull InputStream data) {
        handleStderr(handler, data, DEFAULT_BUFFER_SIZE);
    }

    public static void handleStderr(@Nonnull ProcessStdioHandler<?> handler, @Nonnull InputStream data,
                                    int bufferSize) {
        requireNonNull(handler, "handler");

        OsProcess process = mock(OsProcess.class);
        handler.onPreStart(process);
        handler.onStart(process);
        doHandle(handler::onStderr, data, bufferSize);
        handler.onExit(0);
    }

    public static void handleStderr(@Nonnull ProcessStdioHandler<?> handler, byte[] data) {
        handleStderr(handler, data, DEFAULT_BUFFER_SIZE);
    }

    public static void handleStderr(@Nonnull ProcessStdioHandler<?> handler, byte[] data, int bufferSize) {
        handleStderr(handler, new ByteArrayInputStream(data), bufferSize);
    }

    public static byte[] handleStdin(@Nonnull ProcessStdioHandler<?> handler) {
        return handleStdin(handler, DEFAULT_BUFFER_SIZE);
    }

    public static byte[] handleStdin(@Nonnull ProcessStdioHandler<?> handler, int bufferSize) {
        OsProcess process = mock(OsProcess.class);
        requireNonNull(handler, "handler").onPreStart(process);
        handler.onStart(process);

        ByteArrayOutputStream data = new ByteArrayOutputStream();

        try (WritableByteChannel sink = Channels.newChannel(data)) {
            ByteBuffer buffer = ByteBuffer.allocate(bufferSize);

            while (handler.onStdinReady(buffer)) {
                sink.write(buffer);
                buffer.compact();
            }
            sink.write(buffer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }

        return data.toByteArray();
    }

    public static void handleStdout(@Nonnull ProcessStdioHandler<?> handler, @Nonnull String resource) {
        handleStdout(handler, resource, DEFAULT_BUFFER_SIZE);
    }

    public static void handleStdout(@Nonnull ProcessStdioHandler<?> handler, @Nonnull String resource,
                                    int bufferSize) {
        try (InputStream is = handler.getClass().getResourceAsStream(resource)) {
            if (is == null) {
                throw new IllegalStateException("No such resource: " + resource);
            }

            handleStdout(handler, is, bufferSize);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static void handleStdout(@Nonnull ProcessStdioHandler<?> handler, @Nonnull InputStream data) {
        handleStdout(handler, data, DEFAULT_BUFFER_SIZE);
    }

    public static void handleStdout(@Nonnull ProcessStdioHandler<?> handler, @Nonnull InputStream data,
                                    int bufferSize) {
        requireNonNull(handler, "handler");

        OsProcess process = mock(OsProcess.class);
        handler.onPreStart(process);
        handler.onStart(process);
        doHandle(handler::onStdout, data, bufferSize);
        handler.onExit(0);
    }

    public static void handleStdout(@Nonnull ProcessStdioHandler<?> handler, byte[] data) {
        handleStdout(handler, data, DEFAULT_BUFFER_SIZE);
    }

    public static void handleStdout(@Nonnull ProcessStdioHandler<?> handler, byte[] data, int bufferSize) {
        handleStdout(handler, new ByteArrayInputStream(data), bufferSize);
    }
    
    private static void doHandle(BiConsumer<ByteBuffer, Boolean> handler, InputStream data, int bufferSize) {
        try {
            ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
            ReadableByteChannel channel = Channels.newChannel(requireNonNull(data, "data"));
            boolean isEndOfStream;

            do {
                isEndOfStream = channel.read(buffer) == -1;
                buffer.flip();

                handler.accept(buffer, isEndOfStream);
                buffer.compact();
            } while (!isEndOfStream);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}