package jenkins.util;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

/**
 * Filtering {@link OutputStream} that looks for {@link #MARK} in the output stream and notifies the callback.
 *
 * The mark itself will be removed from the stream.
 *
 * @author Kohsuke Kawaguchi
 * @since 1.458
 */
public abstract class MarkFindingOutputStream extends OutputStream {
    private final OutputStream base;

    protected MarkFindingOutputStream(OutputStream base) {
        this.base = base;
    }

    /**
     * Position in {@link #MARK} if we are currently suspecting a match.
     */
    private int match = 0;

    @Override
    public synchronized void write(int b) throws IOException {
        if (MBYTES[match] == b) { // another byte matched. Good. Keep going...
            match++;
            if (match == MBYTES.length) {
                // don't send MARK to the output, but instead notify the callback
                onMarkFound();
                match = 0;
            }
        } else {
            if (match > 0) {
                // only matched partially. send the partial match that we held off down the pipe
                base.write(MBYTES, 0, match);
                match = 0;

                // this might match the first byte in MARK, so retry.
                write(b);
            } else {
                base.write(b);
            }
        }
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        final int start = off;
        final int end = off + len;
        for (int i = off; i < end; ) {
            if (MBYTES[match] == b[i]) { // another byte matched. Good. Keep going...
                match++;
                i++;
                if (match == MBYTES.length) {
                    base.write(b, off, i - off - MBYTES.length);    // flush the portion up to MARK
                    // don't send MARK to the output, but instead notify the callback
                    onMarkFound();
                    match = 0;
                    off = i;
                    len = end - i;
                }
            } else {
                if (match > 0) {
                    // only matched partially.
                    // if a part of the partial match spans into the previous write, we need to fake that write.
                    int extra = match - (i - start);
                    if (extra > 0) {
                        base.write(MBYTES, 0, extra);
                    }
                    match = 0;

                    // this b[i] might be a fast byte in MARK, so we'll retry
                } else {
                    // irrelevant byte.
                    i++;
                }
            }

        }

        // if we are partially matching, can't send that portion yet.
        if (len - match > 0)
            base.write(b, off, len - match);
    }

    @Override
    public void flush() throws IOException {
        flushPartialMatch();
        base.flush();
    }

    @Override
    public void close() throws IOException {
        flushPartialMatch();
        base.close();
    }

    private void flushPartialMatch() throws IOException {
        if (match > 0) {
            base.write(MBYTES, 0, match);
            match = 0;
        }
    }

    protected  abstract void onMarkFound();

    // having a new line in the end makes it work better with line-buffering transformation
    public static final String MARK = "[Jenkins:SYNC-MARK]\n";
    private static final byte[] MBYTES = toUTF8(MARK);

    private static byte[] toUTF8(String s) {
        return s.getBytes(StandardCharsets.UTF_8);
    }
}
