package com.atlassian.stash.internal.scm.git.command.catfile;

import com.atlassian.bitbucket.io.AbstractCommandInputOutputHandler;
import com.atlassian.bitbucket.scm.CommandResult;
import com.atlassian.bitbucket.scm.CommandSummary;
import com.atlassian.bitbucket.scm.CommandSummaryHandler;
import com.atlassian.stash.internal.scm.git.io.ByteSegmentScanner;
import com.atlassian.utils.process.ProcessException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BoundedInputStream;
import org.apache.commons.io.input.CloseShieldInputStream;
import org.apache.commons.io.input.ProxyInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.*;
import java.nio.charset.StandardCharsets;

/**
 * {@code git cat-file --batch} handler base class that allows subclasses to request content and process it.
 * <p>
 * This handler is both an input and an output handler, allowing it to feed additional requests into the
 * {@code git cat-file --batch} command.
 *
 * @since 4.2
 */
public abstract class AbstractBatchCatFileHandler
        extends AbstractCommandInputOutputHandler<CommandResult>
        implements CommandSummaryHandler {

    private static final Logger log = LoggerFactory.getLogger(AbstractBatchCatFileHandler.class);

    private CommandResult result;
    private Writer inputWriter;
    private InputStream output;
    private ByteSegmentScanner outputScanner;

    @Override
    public CommandResult getOutput() {
        return result;
    }

    @Override
    public void onComplete(@Nonnull CommandSummary summary) throws ProcessException {
        if (result == null) {
            result = summary.getResult();
        }
    }

    @Override
    protected void process(@Nonnull OutputStream input, @Nonnull InputStream output) throws ProcessException {
        this.output = output;
        inputWriter = new BufferedWriter(new OutputStreamWriter(input, StandardCharsets.UTF_8));
        try (Writer ignore = inputWriter;
             InputStream ignored = output) {
            outputScanner = new ByteSegmentScanner(output, (byte) '\n', BatchHeader.MAX_SIZE_IN_BYTES);

            process();

        } catch (IOException e) {
            throw new ProcessException(e);
        }
    }

    /**
     * Called when the input and output streams are ready. Implementations typically want to call {@link #requestObject}
     * in a loop until they are done.
     *
     * @throws IOException if processing fails
     */
    protected abstract void process() throws IOException;

    /**
     * Request an object from {@code git cat-file --batch} and process it via the consumer.
     *
     * @param objectId the object ID to request from Git
     * @param consumer the consumer to handle the result
     * @throws IOException if the object cannot be requested, or the requested object can't be read
     */
    protected void requestObject(@Nonnull String objectId, @Nonnull BatchOutputConsumer consumer) throws IOException {
        resetWatchdog();

        inputWriter.write(objectId);
        inputWriter.write('\n');
        inputWriter.flush();

        byte[] headerBytes = outputScanner.readSegment();
        if (headerBytes == null || headerBytes.length == 0) {
            throw new IllegalStateException("Requested object from git cat-file --batch, but did not get output");
        }

        BatchHeader header = BatchHeader.parse(headerBytes);
        BoundedInputStream contentStream = null;
        try {
            // This stream need not be closed or resource managed because it is just a wrapper over the underlying
            // input stream from process utils.
            contentStream = new BoundedInputStream(
                    new CloseShieldInputStream(new WatchdogAwareInputStream(output)), header.getSize());

            consumer.accept(header, contentStream);

        } finally {
            // If the contentStream is not fully consumed then we do it here.
            // This is to ensure that the next thing we read is the next header, not content.
            if (contentStream != null) {
                try {
                    // It's a bounded input stream, so just skip as much as possible until end of stream.
                    // Calling skip() on the stream itself doesn't work because it causes an "illegal seek" exception.
                    IOUtils.skip(contentStream, Long.MAX_VALUE);
                } catch (IOException e) {
                    // If consumption fails then we have no idea where we have to start parsing for the next
                    // batch header in the output, we have to give up.
                    log.error("Unable to fully consume the content stream", e);
                    cancelProcess();
                }
            }
        }

        if (!header.isMissing()) {
            byte[] trailerBytes = outputScanner.readSegment();
            if (trailerBytes != null && trailerBytes.length != 0) {
                throw new IllegalStateException("Expected a terminator character after content from git cat-file --batch");
            }
        }
    }

    @FunctionalInterface
    protected interface BatchOutputConsumer {
        void accept(@Nonnull BatchHeader header, @Nonnull InputStream content) throws IOException;
    }

    /**
     * A proxied input stream that resets the watchdog as it is read
     */
    private class WatchdogAwareInputStream extends ProxyInputStream {
        protected WatchdogAwareInputStream(InputStream in) {
            super(in);
        }

        @Override
        protected void beforeRead(int n) throws IOException {
            resetWatchdog();
        }
    }
}
