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

import com.atlassian.bitbucket.content.FileContentCallback;
import com.atlassian.bitbucket.content.FileContext;
import com.atlassian.bitbucket.content.FileSummary;
import com.atlassian.bitbucket.io.LineReader;
import com.atlassian.bitbucket.io.ReaderLineReader;
import com.atlassian.bitbucket.io.TruncatedLineOutputHandler;
import com.atlassian.bitbucket.scm.CommandOutputHandler;
import com.atlassian.bitbucket.scm.CommandResult;
import com.atlassian.bitbucket.scm.CommandSummary;
import com.atlassian.bitbucket.scm.CommandSummaryHandler;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.utils.process.ProcessException;

import javax.annotation.Nonnull;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

public class CallbackCatFileOutputHandler
        extends TruncatedLineOutputHandler
        implements CommandOutputHandler<Void>, CommandSummaryHandler {

    private final FileContentCallback callback;
    private final FileContext context;
    private final PageRequest pageRequest;

    private int added;
    private boolean canceled;

    public CallbackCatFileOutputHandler(FileContentCallback callback, FileContext context, PageRequest pageRequest) {
        super(context.getMaxLineLength());

        this.callback = callback;
        this.context = context;
        this.pageRequest = pageRequest;
    }

    @Override
    public Void getOutput() {
        return null;
    }

    @Override
    public void onComplete(@Nonnull CommandSummary summary) throws ProcessException {
        try {
            if (added > 0) {
                callback.onEnd(new FileSummary.Builder(summary)
                        .lastPage(added <= pageRequest.getLimit())
                        .pageRequest(pageRequest)
                        .size(Math.min(added, pageRequest.getLimit()))
                        .build());
            } else if (summary.getResult() == CommandResult.SUCCEEDED) {
                callback.onStart(context);
                callback.onEnd(new FileSummary.Builder(summary)
                        .lastPage(true)
                        .pageRequest(pageRequest)
                        .build());
            }
        } catch (Exception e) {
            throw new ProcessException(e);
        }
    }

    @Override
    protected LineReader createReader(@Nonnull BufferedInputStream inputStream, @Nonnull Charset charset) {
        //Git commands such as blame and diff always consider both CRLF and LF as line endings. They _never_ consider
        //CR (MODE_MAC) as a line ending. To ensure source matches blame, this reader needs to apply the same rules as
        //Git applies.
        return new ReaderLineReader(LineReader.Mode.MODE_PC_UNIX_MIXED, new InputStreamReader(inputStream, charset));
    }

    @Override
    protected void handleBinary() throws ProcessException {
        try {
            callback.onBinary();
        } catch (IOException e) {
            throw new ProcessException(e);
        }
    }

    @Override
    protected void processLine(int lineNum, String line, boolean truncated) throws ProcessException {
        if (lineNum < pageRequest.getStart() || canceled) {
            //We haven't reached the lines we care about yet, or the callback has canceled processing, so ignore this
            //line and keep reading.
            //
            //Ideally the base class should detect cancellation and stop processing lines. Since it doesn't, it must
            //be tracked directly here to prevent accidentally streaming more output than the callback has requested
            //because the InputStream providing command output isn't empty.
            return;
        }

        try {
            if (added++ == 0) {
                callback.onStart(context);
            }
            //Line numbers are (logically) 1-based, but lineNum is 0-based. Hence the +1 here
            if (added > pageRequest.getLimit() || !callback.onLine(lineNum + 1, line, truncated)) {
                cancelProcess();

                canceled = true;
            }
        } catch (IOException e) {
            throw new ProcessException(e);
        }
    }
}