// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.events;

import com.google.common.eventbus.EventBus;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.util.io.OutErr;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;

/**
 * The reporter is the primary means of reporting events such as errors, warnings, progress
 * information and diagnostic information to the user. It is not intended as a logging mechanism for
 * developer-only messages; use a Logger for that.
 *
 * <p>The reporter instance is consumed by the build system, and passes events to {@link
 * EventHandler} instances. These handlers are registered via {@link #addHandler(EventHandler)}. The
 * reporter's main use is in the blaze runtime and its lifetime is the lifetime of the blaze server.
 *
 * <p>Thread-safe: calls to {@code #report} may be made on any thread. Handlers may be run in an
 * arbitary thread (but right now, they will not be run concurrently).
 */
public final class Reporter implements ExtendedEventHandler, ExceptionListener {

  private final List<EventHandler> handlers = new ArrayList<>();
  private EventBus eventBus;

  /** An OutErr that sends all of its output to this Reporter.
   * Each write will (when flushed) get mapped to an EventKind.STDOUT or EventKind.STDERR event.
   */
  private final OutErr outErrToReporter = outErrForReporter(this);
  private volatile OutputFilter outputFilter = OutputFilter.OUTPUT_EVERYTHING;
  private EventHandler ansiAllowingHandler;
  private EventHandler ansiStrippingHandler;
  private boolean ansiAllowingHandlerRegistered;

  public Reporter(EventBus eventBus) {
    this.eventBus = eventBus;
  }

  public static OutErr outErrForReporter(EventHandler rep) {
    return OutErr.create(
        // We don't use BufferedOutputStream here, because in general the Blaze
        // code base assumes that the output streams are not buffered.
        new ReporterStream(rep, EventKind.STDOUT),
        new ReporterStream(rep, EventKind.STDERR));
  }

  /**
   * A copy constructor, to make it convenient to replicate a reporter
   * config for temporary configuration changes.
   */
  public Reporter(Reporter template) {
    handlers.addAll(template.handlers);
    this.eventBus = template.eventBus;
  }

  /** Constructor which configures a reporter with the specified handlers. */
  public Reporter(EventBus eventBus, EventHandler... handlers) {
    this.eventBus = eventBus;
    for (EventHandler handler: handlers) {
      addHandler(handler);
    }
  }

  /**
   * Returns an OutErr that sends all of its output to this Reporter.
   * Each write to the OutErr will cause an EventKind.STDOUT or EventKind.STDERR event.
   */
  public OutErr getOutErr() {
    return outErrToReporter;
  }

  /**
   * Adds a handler to this reporter.
   */
  public synchronized void addHandler(EventHandler handler) {
    Preconditions.checkNotNull(handler);
    handlers.add(handler);
  }

  /**
   * Removes handler from this reporter.
   */
  public synchronized void removeHandler(EventHandler handler) {
     handlers.remove(handler);
  }

  /**
   * This method is called by the build system to report an event.
   */
  @Override
  public synchronized void handle(Event e) {
    if (e.getKind() != EventKind.ERROR && e.getTag() != null && !showOutput(e.getTag())) {
      return;
    }
    for (EventHandler handler : handlers) {
      handler.handle(e);
    }
  }

  @Override
  public void post(ExtendedEventHandler.Postable obj) {
    if (eventBus != null) {
      eventBus.post(obj);
    }
  }

  public void clearEventBus() {
    eventBus = null;
  }

  /**
   * Reports the start of a particular task.
   * Is a wrapper around report() with event kind START.
   * Should always be matched by a corresponding call to finishTask()
   * with the same message, except that the leading percentage
   * progress indicator (if any) in the message may differ.
   */
  public void startTask(Location location, String message) {
    handle(Event.of(EventKind.START, location, message));
  }

  /**
   * Reports the end of a particular task.
   * Is a wrapper around report() with event kind FINISH.
   * Should always be matched by a corresponding call to startTask()
   * with the same message, except that the leading percentage
   * progress indicator (if any) in the message may differ.
   */
  public void finishTask(Location location, String message) {
    handle(Event.of(EventKind.FINISH, location, message));
  }

  @Override
  public void error(Location location, String message, Throwable error) {
    handle(Event.error(location, message));
    error.printStackTrace(new PrintStream(getOutErr().getErrorStream()));
  }

  /**
   * Returns true iff the given tag matches the output filter.
   */
  public boolean showOutput(String tag) {
    return outputFilter.showOutput(tag);
  }

  public void setOutputFilter(OutputFilter outputFilter) {
    this.outputFilter = outputFilter;
  }

  /**
   * Registers an ANSI-control-code-allowing EventHandler with an ANSI-stripping EventHandler
   * that is already registered with the reporter.  The ANSI-stripping handler can then be replaced
   * with the ANSI-allowing handler by calling {@code #switchToAnsiAllowingHandler} which
   * calls {@code removeHandler} for the ANSI-stripping handler and then {@code addHandler} for the
   * ANSI-allowing handler.
   */
  public synchronized void registerAnsiAllowingHandler(
      EventHandler ansiStrippingHandler,
      EventHandler ansiAllowingHandler) {
    this.ansiAllowingHandler = ansiAllowingHandler;
    this.ansiStrippingHandler = ansiStrippingHandler;
    ansiAllowingHandlerRegistered = true;
  }

  /**
   * Restores the ANSI-allowing EventHandler registered using
   * {@link #registerAnsiAllowingHandler}.
   */
  public synchronized void switchToAnsiAllowingHandler() {
    if (ansiAllowingHandlerRegistered) {
      removeHandler(ansiStrippingHandler);
      addHandler(ansiAllowingHandler);
      ansiStrippingHandler = null;
      ansiAllowingHandler = null;
      ansiAllowingHandlerRegistered = false;
    }
  }
}
