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

import com.atlassian.bitbucket.mesh.util.ByteSegmentScanner;

import javax.annotation.Nonnull;
import java.nio.ByteBuffer;

/**
 * A base class for creating {@code git cat-file --batch} handlers.
 *
 * @param <T> the type of result returned by {@link #getResult()}
 */
public abstract class AbstractBatchCatFileStdioHandler<T> extends BatchedCatFileStdioHandlerSupport<T> {

    private final ByteSegmentScanner segmentScanner;

    private long remainingSize;
    private BatchState state;

    protected AbstractBatchCatFileStdioHandler() {
        segmentScanner = new ByteSegmentScanner();
        state = BatchState.HEADER;
    }

    //Note: The state can advance multiple times handling a single buffer of data, depending on how
    //      large the requested object is. As a result, this uses a series of if statements rather
    //      than an else if chain or switch statement.
    @Override
    public void onStdout(@Nonnull ByteBuffer buffer, boolean closed) {
        //Processing is wrapped in a while loop to handle the case where a single buffer contains the
        //full output (header, content, trailer) for multiple objects. In such cases, we may not get
        //another onStdout callback until more than one of those objects is processed, resulting in
        //the cat-file process appearing to "hang"
        while (buffer.hasRemaining()) {
            if (state == BatchState.HEADER) {
                ByteBuffer headerBytes = segmentScanner.scan(buffer, closed);
                if (headerBytes == null) {
                    return;
                }
                if (!headerBytes.hasRemaining()) {
                    //This should have been handled by the TRAILER state, so this is indicative of some
                    //issue with the output handler
                    throw new IllegalStateException("Unexpected empty segment parsing output");
                }

                BatchHeader header = BatchHeader.parse(headerBytes);
                if (header.isMissing()) {
                    //Stay in HEADER state (there will be no more output for this object), and pump the
                    //next object ID
                    onMissing(header.getObjectId());
                    pumpIfRunning();

                    continue; //Continue in case the buffer already has another object
                }

                //If the handler doesn't want the object, jump straight to FLUSH; otherwise, move to
                //CONTENT state and stream the object's bytes
                state = onObject(header) ? BatchState.CONTENT : BatchState.FLUSH;
                //Whether the handler wants the object or not, we'll need to consume its bytes from
                //stdout before we can move forward to the next object
                remainingSize = header.getSize();
                if (!buffer.hasRemaining()) {
                    //If there's no more data in the buffer, short-circuit checking further states
                    //during this invocation
                    return;
                }
            }

            if (state == BatchState.CONTENT && handleContent(buffer)) {
                //If the handler is still waiting for more content, _whether current buffer was fully
                //consumed or not_, return out of the loop and wait for the next onStdout callback
                return;
            }

            if (state == BatchState.FLUSH && flushContent(buffer)) {
                //If we're still flushing content, return out of the loop now and wait for the next
                //onStdout callback with more data
                return;
            }

            if (state == BatchState.TRAILER && buffer.hasRemaining()) {
                if (buffer.get() == (byte) '\n') {
                    state = BatchState.HEADER;

                    pumpIfRunning();
                } else {
                    throw new IllegalStateException("Expected a trailing LF");
                }
            }
        }
    }

    /**
     * Receives content bytes from the current object.
     * <p>
     * When the {@link #onObject} callback returns {@code true}, it is followed by one or more content callbacks to
     * receive the object's bytes. <i>Bytes may be spread across several content callbacks</i>; handlers <i>must
     * not</i> assume they'll get the full content in a single call.
     * <p>
     * Handlers that parse object content inline may return {@code false} to signal they've seen enough and that no
     * further content should be provided. After a {@code false} return, the base handling will skip over any further
     * content to get to the next object.
     *
     * @param content a buffer containing some subset of the object's bytes
     * @param closed  {@code true} if this buffer includes the <i>final</i> content bytes; otherwise, {@code false}
     *                to indicate additional buffers may be provided (note that <i>final</i> does not imply the
     *                buffer contains the <i>full</i> bytes, which may have been spread across multiple callbacks)
     * @return {@code true} if additional content should be provided; otherwise, {@code false} if the handler has
     *         processed as much of the object's content as it needs to
     */
    protected abstract boolean onContent(@Nonnull ByteBuffer content, boolean closed);

    /**
     * Receives an object ID which was requested but not found.
     * <p>
     * This method doesn't offer a {@code boolean} return because, by definition, there is no content available for
     * missing objects. If missing objects are unexpected, the implementation can throw an exception to abort further
     * processing (or perhaps set some flag that makes the next {@link #getNextObjectId()} call return {@code null}).
     *
     * @param objectId the ID of the missing object
     */
    protected abstract void onMissing(@Nonnull String objectId);

    /**
     * Receives a {@link BatchHeader header} containing size and type details for a new object.
     * <p>
     * Implementations can return {@code true} or {@code false} to signal whether or not they wish to consume the
     * object's content. For example, handlers may be interested in the contents for {@code commit} or {@code tag}
     * objects but not {@code blob}s. If this callback returns {@code false}, the base handling will automatically
     * skip over all the provided object's content to get to the next object.
     * <p>
     * <b>Note</b>: This callback will <i>not</i> be invoked for missing objects, so {@link BatchHeader#isMissing}
     * will always be {@code false}. Missing objects are dispatched to {@link #onMissing} instead.
     *
     * @param header a header describing the object's ID, size and type
     * @return {@code true} if the handler wishes to consume content for the object via calls to {@link #onContent};
     *         otherwise, {@code false} to skip the content and move on to the next object
     */
    protected abstract boolean onObject(@Nonnull BatchHeader header);

    /**
     * Discards bytes from the provided {@link ByteBuffer} up to the current {@link #remainingSize}.
     *
     * @param buffer the buffer containing object content to flush (and maybe more)
     * @return {@code true} if additional content is expected; otherwise, {@code false} if the current object has
     *         been completely flushed and processing can continue
     */
    private boolean flushContent(@Nonnull ByteBuffer buffer) {
        int remaining = buffer.remaining();
        if (remaining < remainingSize) {
            //The buffer doesn't contain the full object. Flush what's there and wait for more
            remainingSize -= remaining;
            buffer.position(buffer.position() + remaining);

            return true;
        }

        //The buffer has more bytes in it than we asked for, which means it may contain the trailer line
        //feed and potentially another object. We only want to drain the remaining size, and then loop
        //again so the header for the next object can be parsed
        buffer.position(buffer.position() + (int) remainingSize);
        remainingSize = 0L;
        state = BatchState.TRAILER;

        return false;
    }

    /**
     * Slices off object contents from the provided buffer, up to {@link #remainingSize} in length, and provides
     * them to {@link #onContent} for processing.
     * <p>
     * <b>States</b>: This method is capable of the following state transitions:
     * <ul>
     *     <li><b>CONTENT</b> -&gt; <b>CONTENT</b>: {@link #remainingSize} indicates more content is expected, and
     *     {@link #onContent} returned {@code true}, indicating the handler wants to process that content</li>
     *     <li><b>CONTENT</b> -&gt; <b>FLUSH</b>: {@link #onContent} returned {@code false}, indicating the handler
     *     has processed as much content as it wishes to</li>
     *     <li><b>CONTENT</b> -&gt; <b>TRAILER</b>: {@link #remainingSize} indicates the full content has been
     *     received, and the handler has processed all of it</li>
     * </ul>
     * <p>
     * <b>Note</b>: The return value for this method doesn't have any relationship to whether the {@code buffer}
     * has been exhausted or not. Regardless of whether this method returns {@code true} or {@code false}, the
     * buffer or may not be empty.
     *
     * @param buffer the buffer containing object content (and maybe more)
     * @return {@code true} if the handler is still waiting for more content (whether the full {@code buffer} was
     *         consumed or not); otherwise, {@code false} if the handler has processed enough (whether the buffer
     *         has remaining bytes or not)
     */
    private boolean handleContent(@Nonnull ByteBuffer buffer) {
        //Create a slice from the buffer to pass to the subclass, rather than handing them the original buffer.
        //This way the handler can't accidentally consume more bytes than the object contains, and the buffer's
        //state is more consistent (for example, the buffer's starting position in onContent is always 0)
        ByteBuffer content = buffer.slice();
        if (content.remaining() > remainingSize) {
            //The buffer has more than one object in it; limit the slice to only the bytes for the current object
            content.limit((int) remainingSize);
        }

        boolean closed = content.limit() == remainingSize;
        if (onContent(content, closed) && !closed) {
            //Use the slice's position to detect how much of the available content the handler consumed, and
            //update the real buffer and remaining size accordingly
            int used = content.position();
            if (used > 0) {
                buffer.position(buffer.position() + used);

                remainingSize -= used;
                if (remainingSize == 0L) {
                    state = BatchState.TRAILER;
                    return false;
                }
            }

            //If we make it here, the handler wants more content. Note that it may or may not have exhausted
            //the provided buffer. The buffer may contain a partial "object" (to some definition of "object"
            //that varies from handler to handler), or the handler may be waiting for at some minimum amount
            //of data before moving forward (encoding detection, for example). Returning true here serves as
            //a hint to the main onStdout loop that the handler is done with _this_ buffer
            return true;
        }

        //If the callback doesn't want any more data, or if the buffer offered included the last bytes for
        //the object, don't bother calculating the new position; instead, just skip to FLUSH state and let
        //that handle advancing the buffer
        state = BatchState.FLUSH;
        return false;
    }

    /**
     * Defines possible states for {@link #onStdout}, to simplify tracking how bytes should be routed.
     */
    private enum BatchState {

        /**
         * The object has some number of bytes {@link #remainingSize remaining}, and the implementation has not
         * returned {@code false} from {@link #onContent} or {@link #onObject}.
         */
        CONTENT,
        /**
         * The object has some number of bytes {@link #remainingSize remaining}, but either {@link #onObject} or
         * {@link #onContent} has returned {@code false} to signal the implementation isn't interested in them.
         */
        FLUSH,
        /**
         * The header for a new object should be read.
         */
        HEADER,
        /**
         * The trailing newline that follows the completion of an object's contents should be read.
         */
        TRAILER
    }
}