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

import com.atlassian.bitbucket.mesh.process.OsProcess;
import com.atlassian.bitbucket.mesh.process.handler.AbstractProcessStdioHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * A support class providing some common behavior that's shared between {@code git cat-file --batch} and
 * {@code git cat-file --batch-check} handling.
 *
 * @param <T> the type of result returned by {@link #getResult ()}
 */
public abstract class BatchedCatFileStdioHandlerSupport<T> extends AbstractProcessStdioHandler<T> {

    protected final Logger log;

    protected BatchedCatFileStdioHandlerSupport() {
        log = LoggerFactory.getLogger(getClass());
    }

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

        //Note: onStart is always called on the "request" thread (the thread where the Command is
        //call()'d or start()'d). When using Command.start() and asynchronous execution, that can
        //result in deadlocks if the first object ID is pumped here.
        //
        //Consider the following scenario:
        //- getNextObjectId() is implemented to draw object IDs from a BlockingQueue
        //- IDs are offered to the blocking queue by processing on the "request" thread
        //
        //In that scenario, calling getNextObjectId() here deadlocks: Since this is called on
        //the "request" thread, the queue can never be populated. To avoid that issue, rather
        //than pumping the first object ID here, we signal to the process that we want to write
        //to stdin, and wait for that callback.
        //- If Command.call() is used, that callback will still happen on the "request" thread,
        //  but in that scenario the request thread couldn't possibly be populating the object
        //  IDs anyway since it would always be blocked in call()
        //- If Command.start() is used, that callback will happen on a background thread, and
        //  the "request" thread will be free to make forward progress
        process.wantWrite();
    }

    @Override
    public boolean onStdinReady(@Nonnull ByteBuffer buffer) {
        String objectId = getNextObjectId();
        if (objectId == null) {
            closeStdin();
        } else {
            buffer.put(StandardCharsets.UTF_8.encode(objectId));
            buffer.put((byte) '\n');
        }
        //Always flip the buffer, even if we don't put anything in it, or we may pump garbage
        //to the process's stdin
        buffer.flip();

        //Subsequent IDs will all be requested using NioProcess.writeStdin, so we don't need
        //any further onStdinReady callbacks
        return false;
    }

    /**
     * Retrieves the next object ID to request from {@code git cat-file}. If the returned ID is {@code null},
     * the process's {@code stdin} will be closed to let it terminate.
     *
     * @return the next object ID to request, or {@code null} to signal no more objects
     */
    @Nullable
    protected abstract String getNextObjectId();

    /**
     * Pumps the {@link #getNextObjectId() next object ID} if the process has not been {@link #isCanceled canceled},
     * or closes {@code stdin} if there are no more object IDs to request.
     */
    protected void pumpIfRunning() {
        if (isCanceled()) {
            closeStdin();
        } else {
            pump();
        }
    }

    /**
     * Writes the provided object ID, followed by a line feed ({@code \n}) to the process's {@code stdin}, which
     * will trigger {@code git cat-file --batch} (or {@code --batch-check}) to load the requested object.
     * <p>
     * <b>Implementation note</b>: While this method appears synchronous, the actual write to {@code stdin} is
     * asynchronous. Calling this method puts the object ID into a buffer which NuProcess will (eventually) write
     * to {@code stdin} on whatever thread is managing the process's streams. As a result, when this method returns
     * the write to {@code stdin} <i>may or may not</i> have actually happened. (For most handlers this should be
     * irrelevant; it's noted here primarily for developers to be aware of how the flow works.)
     *
     * @param objectId the object ID to request
     */
    protected void requestObject(String objectId) {
        //Trailing LF signals to Git that that's the end of the object ID
        process.writeStdin(ByteBuffer.wrap((objectId + "\n").getBytes(StandardCharsets.UTF_8)));
    }

    private void pump() {
        String objectId = getNextObjectId();
        if (objectId == null) {
            closeStdin();
        } else {
            requestObject(objectId);
        }
    }
}
