package com.atlassian.bitbucket.mesh.process.handler;

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

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

import static java.util.Objects.requireNonNull;

/**
 * An {@link AbstractProcessStdioHandler} which streams data from a {@link Path} on disk to the process, writes
 * the process's output to a {@link Path} on disk, and returns the number of bytes that were written.
 */
public class PathStdioHandler extends AbstractProcessStdioHandler<Long> {

    private final Path input;
    private final Path output;

    private ReadableByteChannel readChannel;
    private WritableByteChannel writeChannel;
    private long written;

    protected PathStdioHandler(Path input, Path output) {
        this.input = input;
        this.output = output;
    }

    @Nonnull
    public static PathStdioHandler reading(@Nonnull Path input) {
        return new PathStdioHandler(requireNonNull(input, "input"), null);
    }

    @Nonnull
    public static PathStdioHandler readingAndWriting(@Nonnull Path input, @Nonnull Path output) {
        return new PathStdioHandler(requireNonNull(input, "input"), requireNonNull(output, "output"));
    }

    @Nonnull
    public static PathStdioHandler writing(@Nonnull Path output) {
        return new PathStdioHandler(null, requireNonNull(output, "output"));
    }

    @Override
    public Long getResult() {
        return written;
    }

    @Override
    public void onExit(int exitCode) {
        //Ensure the channel is closed, in case an error terminates the process before input is exhausted
        WritableByteChannel writeChannel = this.writeChannel;
        if (writeChannel != null && writeChannel.isOpen()) {
            try {
                writeChannel.close();
            } catch (IOException e) {
                throw new UncheckedIOException("Failed to close output channel", e);
            }
            this.writeChannel = null;
        }

        //Ensure the channel is closed, in case an error terminates the process before input is exhausted
        ReadableByteChannel readChannel = this.readChannel;
        if (readChannel != null && readChannel.isOpen()) {
            try {
                readChannel.close();
            } catch (IOException e) {
                throw new UncheckedIOException("Failed to close input channel", e);
            }
            this.readChannel = null;
        }
    }

    @Override
    public void onStart(@Nonnull OsProcess process) {
        super.onStart(process);

        if (input != null) {
            process.wantWrite();
        }
    }

    @Override
    public boolean onStdinReady(@Nonnull ByteBuffer buffer) {
        try {
            if (readChannel == null) {
                readChannel = Files.newByteChannel(input, StandardOpenOption.READ);
            }

            int read = readChannel.read(buffer);
            buffer.flip(); //Always flip the buffer, whether we read anything into it or not

            if (read == -1) {
                //On EOF, eagerly close the channel to avoid holding a descriptor for however
                //long the process ends up running
                readChannel.close();
                readChannel = null;

                //Close the process's stdin to indicate there's no more input to provide
                closeStdin();

                //Return false so another write window isn't requested
                return false;
            }

            //Otherwise, request another window to write more data to stdin
            return true;
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to copy from input channel to stdin", e);
        }
    }

    @Override
    public void onStdout(@Nonnull ByteBuffer buffer, boolean closed) {
        if (output == null) {
            //Skip to the end of the buffer
            buffer.position(buffer.position() + buffer.remaining());
            return;
        }

        try {
            if (writeChannel == null) {
                //Create the file if necessary, but don't _require_ creating the file
                writeChannel = Files.newByteChannel(output, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
            }

            if (buffer.hasRemaining()) {
                written += writeChannel.write(buffer);
            }

            if (closed) {
                writeChannel.close();
                writeChannel = null;
            }
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to copy from stdout to output channel", e);
        }
    }
}