package com.atlassian.bitbucket.mesh.git.builder.packobjects;

import com.atlassian.bitbucket.mesh.git.hook.GitHookOutputWriter;
import com.atlassian.bitbucket.mesh.git.hook.HookStdin;
import com.atlassian.bitbucket.mesh.process.OsProcess;
import com.atlassian.bitbucket.mesh.process.ProcessStdioHandler;
import com.atlassian.bitbucket.mesh.process.handler.AbstractProcessStdioHandler;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;

import static java.util.Objects.requireNonNull;

/**
 * A {@link ProcessStdioHandler} which uses the <em>SCM hook output protocol</em>.
 * <p>
 * Messages written to {@code stderr} or {@code stdout} are constructed as a series of chunks in the following format:
 * <code>[ASCII char][unsigned short][bytes]</code>
 * The meaning of each part of the chunk is:
 * <dl>
 *     <dt>ASCII char</dt>
 *     <dd>The <em>channel</em> of the data. Can be standard output (O) or standard error (E) or exit code (X)</dd>
 *     <dt>unsigned short</dt>
 *     <dd>The length of the bytes to be sent.</dd>
 *     <dt>bytes</dt>
 *     <dd>The data</dd>
 * </dl>
 * The exit code chunk will always be the last chunk and there will only ever be one. There can be multiple chunks of
 * standard output or standard error and they can be interleaved.
 */
public class PackObjectsStdioHandler extends AbstractProcessStdioHandler<Void> {

    private final HookStdin stdin;
    private final GitHookOutputWriter writer;

    private ReadableByteChannel channel;

    public PackObjectsStdioHandler(@Nonnull HookStdin stdin, @Nonnull GitHookOutputWriter writer) {
        this.stdin = requireNonNull(stdin, "stdin");
        this.writer = requireNonNull(writer, "writer");
    }

    @Override
    public void onExit(int exitCode) {
        if (channel != null && channel.isOpen()) {
            try {
                channel.close();
                channel = null;
            } catch (IOException e) {
                throw new UncheckedIOException("Failed to close stdin", e);
            }
        }
    }

    @Override
    public Void getResult() {
        return null;
    }

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

    @Override
    public void onStderr(@Nonnull ByteBuffer buffer, boolean closed) {
        try {
            writer.writeStderr(buffer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public boolean onStdinReady(@Nonnull ByteBuffer buffer) {
        try {
            if (channel == null) {
                //Open stdin unbuffered to ensure we don't wrap whatever stream it uses; Channels has
                //internal smarts that optimize FileInputStream handling
                channel = Channels.newChannel(stdin.open());
            }

            int read = channel.read(buffer);
            buffer.flip();

            if (read == -1) {
                //Once we've streamed stdin to pack-objects, we no longer need access to it
                stdin.release();

                //Close the channel and null it so onExit won't close it again
                channel.close();
                channel = null;

                //Close the process's stdin pipe so pack-objects can complete
                closeStdin();
                return false;
            }

            return true;
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to copy stdin", e);
        }
    }

    @Override
    public void onStdout(@Nonnull ByteBuffer buffer, boolean closed) {
        try {
            writer.writeStdout(buffer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}
