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

import com.atlassian.bitbucket.mesh.io.EncodingUtils;
import com.atlassian.bitbucket.mesh.process.OsProcess;

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

/**
 * A base class for handlers which process {@code stdout} one line at a time.
 * <p>
 * As buffers of {@code stdout} data are received they are decoded and split into lines, which are then passed to
 * {@link #onStdout(String, boolean)} for processing. If a {@code Charset} is specified during construction, bytes
 * will be decoded in that character set. Otherwise, when the first buffer of {@code stdout} data is received,
 * heuristic-based {@link EncodingUtils#detectEncoding(ByteBuffer) encoding detection} is performed and the bytes
 * are decoded using the detected encoding.
 *
 * @param <T> the type of result returned by the handler
 */
public abstract class AbstractLineStdioHandler<T> extends AbstractProcessStdioHandler<T> {

    private final LineOutputAdapter lineOutputAdapter;

    protected AbstractLineStdioHandler(@Nullable Charset charset) {
        this(charset, null);
    }

    protected AbstractLineStdioHandler(@Nullable Charset charset, @Nullable LineDecoder.Mode lineMode) {
        this(charset, lineMode, Integer.MAX_VALUE);
    }

    protected AbstractLineStdioHandler(@Nullable Charset charset, @Nullable LineDecoder.Mode lineMode,
                                       int maxLineLength) {
        lineOutputAdapter = LineOutputAdapter.newBuilder()
                .charset(charset)
                .lineMode(lineMode)
                .lineSink(this::onStdout)
                .maxLineLength(maxLineLength)
                .onBinary(this::onBinary)
                .onClosed(this::onStdoutClosed)
                .build();
    }

    /**
     * Attaches the {@link LineOutputAdapter} to the {@link OsProcess}, which resets the adapter.
     * <p>
     * <b>Note</b>: This override allows line handling to be reused across multiple <i>sequential</i> commands
     * (use across <i>concurrent</i> commands is not possible). Subclasses may add their own state on top of
     * line handling, however, and it is up to such subclasses to manage their own state to allow for reuse.
     *
     * @param process the process about to be started
     */
    @Override
    public void onPreStart(@Nonnull OsProcess process) {
        super.onPreStart(process);

        lineOutputAdapter.attach(process);
    }

    /**
     * Uses a {@link LineDecoder} to decode the provided {@link ByteBuffer} and pass the decoded lines to
     * {@link #onStdout(String, boolean)}.
     *
     * @param buffer a buffer of bytes to decode, which may be empty if {@code closed} is {@code true}
     * @param closed {@code true} if this is the final buffer; otherwise, {@code false} if the command is still
     *               running and additional buffers may be provided
     */
    @Override
    public void onStdout(@Nonnull ByteBuffer buffer, boolean closed) {
        lineOutputAdapter.onBytes(buffer, closed);
    }

    protected int getMaxLineLength() {
        return lineOutputAdapter.getMaxLineLength();
    }

    /**
     * Returns the strategy for dealing with binary data. With the default value of {@link BinaryDisposition#CANCEL},
     * when the <i>first</i> {@link #onStdout(ByteBuffer, boolean)} callback is received and binary data is detected,
     * the process is canceled and further processing is aborted. Aside from {@link #onExit}, no further callbacks will
     * be made after this.
     * <p>
     * Implementations which do wish to process binary output should override this method and return
     * {@link BinaryDisposition#STREAM}.
     *
     * @return the {@link BinaryDisposition}
     */
    @SuppressWarnings("MethodMayBeStatic")
    @Nonnull
    protected BinaryDisposition onBinary() {
        return BinaryDisposition.CANCEL;
    }

    /**
     * Receives a single line of {@code stdout}.
     *
     * @param line      the line
     * @param truncated true if the line has been truncated
     * @return {@code true} if more lines should be provided; otherwise, {@code false} if the handler has processed
     *         as much {@code stdout} as it needs
     */
    protected abstract boolean onStdout(@Nonnull String line, boolean truncated);

    /**
     * Indicates the process's {@code stdout} has been closed and no more {@link #onStdout(String, boolean) lines}
     * will be provided. Aside from {@link #onExit}, no further callbacks will be made after this.
     */
    protected void onStdoutClosed() {
    }

    protected void setMaxLineLength(int value) {
        lineOutputAdapter.setMaxLineLength(value);
    }
}