package com.atlassian.bitbucket.scm.git.command;

import com.atlassian.bitbucket.scm.AsyncCommand;
import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.CommandOutputHandler;
import com.atlassian.utils.process.BaseOutputHandler;
import com.atlassian.utils.process.ProcessException;
import com.google.common.io.ByteStreams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.InputStream;

import static com.google.common.base.Preconditions.checkNotNull;

public abstract class AbstractLoggingCommandOutputHandler<T> extends BaseOutputHandler implements CommandOutputHandler<T> {

    private Command<?> command;
    private Logger log;

    /**
     * Constructs a new {@code AbstractLoggingCommandOutputHandler} which will retrieve its {@code Logger} based on the
     * class of the {@link Command} that is {@link #setCommand(Command) set}.
     */
    public AbstractLoggingCommandOutputHandler() {
    }

    /**
     * Constructs a new {@code AbstractLoggingCommandOutputHandler} which will write to the specified {@code Logger}, rather
     * than retrieving one based on the {@link Command}'s class.
     *
     * @param log the logger to write to
     * @throws NullPointerException if the provided {@code log} is {@code null}
     */
    public AbstractLoggingCommandOutputHandler(@Nonnull Logger log) {
        this.log = checkNotNull(log, "log");
    }

    /**
     * Reads all output from the provided {@code InputStream} and logs it as UTF-8 text.
     *
     * @param stream the stream to read all output from
     * @throws ProcessException if an {@code IOException} is thrown while reading from the provided stream
     */
    @Override
    public void process(@Nonnull InputStream stream) throws ProcessException {
        try {
            byte[] bytes = ByteStreams.toByteArray(stream);
            if (bytes == null || bytes.length == 0) {
                log.trace("{}: Completed with no output", command);
            } else {
                if (log.isDebugEnabled()) {
                    String output = new String(bytes, "UTF-8");
                    log.debug("{}: Completed with the following output:\n{}", command, output);
                }
            }
        } catch (IOException e) {
            throw new ProcessException(e);
        }
    }

    /**
     * Sets the command which, when {@link Command#call() called}, will use this handler to log its output. The
     * provided command is used for two purposes:
     * <ul>
     * <li>To describe the command being executed (using its {@code toString()} implementation)</li>
     * <li>To determine the logger to write to (using its {@code getClass()})</li>
     * </ul>
     *
     * @param command the command to log output for
     * @param <C>     The command type, such as {@link Command} or {@link AsyncCommand AsyncCommand}
     * @return the provided {@link Command}
     * @throws NullPointerException if the provided {@code command} is {@code null}
     */
    @Nonnull
    public <C extends Command<T>> C setCommand(@Nonnull C command) {
        this.command = checkNotNull(command, "command");
        if (log == null) {
            log = LoggerFactory.getLogger(command.getClass());
        }

        return command;
    }
}
