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

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

import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import static java.util.Objects.requireNonNull;

/**
 * A {@link ProcessStdioHandler} which feeds input buffered in memory to the process.
 */
public class InputStdioHandler extends AbstractProcessStdioHandler<Void> {

    private final ByteBuffer data;

    /**
     * Creates a new {@link InputStdioHandler} that will write the contents of the provided {@code String} to
     * the process. The provided {@code String} will be converted to bytes using the UTF-8 charset.
     *
     * @param data the string to write
     * @throws NullPointerException if the provided string is {@code null}
     */
    public InputStdioHandler(@Nonnull String data) {
        this(data, StandardCharsets.UTF_8);
    }

    /**
     * Creates a new {@link InputStdioHandler} that will write the contents of the provided {@code String} to
     * the process. The provided {@code String} will be converted to bytes using the specified {@code Charset}.
     *
     * @param data the string to write
     * @param charset the {@link Charset}
     * @throws NullPointerException if the provided string or charset is {@code null}
     */
    public InputStdioHandler(@Nonnull String data, @Nonnull Charset charset) {
        this(requireNonNull(data, "data").getBytes(requireNonNull(charset, "charset")));
    }

    /**
     * Creates a new {@link InputStdioHandler} that will write the contents of the provided array to the process.
     * If the array is empty, nothing will be written to {@code stdin} but it is not considered an error.
     *
     * @param data the array to write
     * @throws NullPointerException if the provided array is {@code null}
     */
    public InputStdioHandler(@Nonnull byte[] data) {
        this.data = ByteBuffer.wrap(requireNonNull(data, "data"));
    }

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

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

        if (data.hasRemaining()) {
            process.wantWrite();
        } else {
            process.closeStdin(false);
        }
    }

    @Override
    public boolean onStdinReady(@Nonnull ByteBuffer buffer) {
        int bufferRemaining = buffer.remaining();
        if (data.remaining() > bufferRemaining) {
            //Use slice+limit to put a subset of the remaining data into the buffer
            buffer.put((ByteBuffer) data.slice().limit(bufferRemaining));
            buffer.flip();

            //Advance the data position by the buffer's original remaining size
            data.position(data.position() + bufferRemaining);

            return true;
        }

        //Otherwise, put the remaining data into the buffer, close the stdin pipe (since there's no more
        //data to write) and return false to indicate we don't need any more callbacks
        buffer.put(data);
        buffer.flip();
        closeStdin();

        return false;
    }

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