/*
 * Copyright (C) 2015 The Dagger Authors.
 *
 * 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 dagger.producers.monitoring;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import dagger.internal.Beta;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Utility methods relating to timing.
 *
 * @since 2.1
 */
// TODO(beder): Reduce the visibility of this class to package-private.
@Beta
@SuppressWarnings("GoodTime") // Should be using java.time.Instant/Duration as opposed to nanos
public final class TimingRecorders {
  private static final Logger logger = Logger.getLogger(TimingRecorders.class.getName());

  /**
   * Returns a timing recorder factory that delegates to the given factories, and ensures that any
   * method called on this object, even transitively, does not throw a {@link RuntimeException} or
   * return null.
   *
   * <p>If the delegate recorders throw an {@link Error}, then that will escape this recorder
   * implementation. Errors are treated as unrecoverable conditions, and may cause the entire
   * component's execution to fail.
   */
  public static ProductionComponentTimingRecorder.Factory
      delegatingProductionComponentTimingRecorderFactory(
          Collection<ProductionComponentTimingRecorder.Factory> factories) {
    switch (factories.size()) {
      case 0:
        return noOpProductionComponentTimingRecorderFactory();
      case 1:
        return new NonThrowingProductionComponentTimingRecorder.Factory(
            Iterables.getOnlyElement(factories));
      default:
        return new DelegatingProductionComponentTimingRecorder.Factory(factories);
    }
  }

  /**
   * A component recorder that delegates to a single recorder, and catches and logs all exceptions
   * that the delegate throws.
   */
  private static final class NonThrowingProductionComponentTimingRecorder
      implements ProductionComponentTimingRecorder {
    private final ProductionComponentTimingRecorder delegate;

    NonThrowingProductionComponentTimingRecorder(ProductionComponentTimingRecorder delegate) {
      this.delegate = delegate;
    }

    @Override
    public ProducerTimingRecorder producerTimingRecorderFor(ProducerToken token) {
      try {
        ProducerTimingRecorder recorder = delegate.producerTimingRecorderFor(token);
        return recorder == null
            ? ProducerTimingRecorder.noOp()
            : new NonThrowingProducerTimingRecorder(recorder);
      } catch (RuntimeException e) {
        logProducerTimingRecorderForException(e, delegate, token);
        return ProducerTimingRecorder.noOp();
      }
    }

    static final class Factory implements ProductionComponentTimingRecorder.Factory {
      private final ProductionComponentTimingRecorder.Factory delegate;

      Factory(ProductionComponentTimingRecorder.Factory delegate) {
        this.delegate = delegate;
      }

      @Override
      public ProductionComponentTimingRecorder create(Object component) {
        try {
          ProductionComponentTimingRecorder recorder = delegate.create(component);
          return recorder == null
              ? noOpProductionComponentTimingRecorder()
              : new NonThrowingProductionComponentTimingRecorder(recorder);
        } catch (RuntimeException e) {
          logCreateException(e, delegate, component);
          return noOpProductionComponentTimingRecorder();
        }
      }
    }
  }

  /**
   * A producer recorder that delegates to a single recorder, and catches and logs all exceptions
   * that the delegate throws.
   */
  private static final class NonThrowingProducerTimingRecorder extends ProducerTimingRecorder {
    private final ProducerTimingRecorder delegate;

    NonThrowingProducerTimingRecorder(ProducerTimingRecorder delegate) {
      this.delegate = delegate;
    }

    @Override
    public void recordMethod(long startedNanos, long durationNanos) {
      try {
        delegate.recordMethod(startedNanos, durationNanos);
      } catch (RuntimeException e) {
        logProducerTimingRecorderMethodException(e, delegate, "recordMethod");
      }
    }

    @Override
    public void recordSuccess(long latencyNanos) {
      try {
        delegate.recordSuccess(latencyNanos);
      } catch (RuntimeException e) {
        logProducerTimingRecorderMethodException(e, delegate, "recordSuccess");
      }
    }

    @Override
    public void recordFailure(Throwable exception, long latencyNanos) {
      try {
        delegate.recordFailure(exception, latencyNanos);
      } catch (RuntimeException e) {
        logProducerTimingRecorderMethodException(e, delegate, "recordFailure");
      }
    }

    @Override
    public void recordSkip(Throwable exception) {
      try {
        delegate.recordSkip(exception);
      } catch (RuntimeException e) {
        logProducerTimingRecorderMethodException(e, delegate, "recordSkip");
      }
    }
  }

  /**
   * A component recorder that delegates to several recorders, and catches and logs all exceptions
   * that the delegates throw.
   */
  private static final class DelegatingProductionComponentTimingRecorder
      implements ProductionComponentTimingRecorder {
    private final ImmutableList<ProductionComponentTimingRecorder> delegates;

    DelegatingProductionComponentTimingRecorder(
        ImmutableList<ProductionComponentTimingRecorder> delegates) {
      this.delegates = delegates;
    }

    @Override
    public ProducerTimingRecorder producerTimingRecorderFor(ProducerToken token) {
      ImmutableList.Builder<ProducerTimingRecorder> recordersBuilder = ImmutableList.builder();
      for (ProductionComponentTimingRecorder delegate : delegates) {
        try {
          ProducerTimingRecorder recorder = delegate.producerTimingRecorderFor(token);
          if (recorder != null) {
            recordersBuilder.add(recorder);
          }
        } catch (RuntimeException e) {
          logProducerTimingRecorderForException(e, delegate, token);
        }
      }
      ImmutableList<ProducerTimingRecorder> recorders = recordersBuilder.build();
      switch (recorders.size()) {
        case 0:
          return ProducerTimingRecorder.noOp();
        case 1:
          return new NonThrowingProducerTimingRecorder(Iterables.getOnlyElement(recorders));
        default:
          return new DelegatingProducerTimingRecorder(recorders);
      }
    }

    static final class Factory implements ProductionComponentTimingRecorder.Factory {
      private final ImmutableList<? extends ProductionComponentTimingRecorder.Factory> delegates;

      Factory(Iterable<? extends ProductionComponentTimingRecorder.Factory> delegates) {
        this.delegates = ImmutableList.copyOf(delegates);
      }

      @Override
      public ProductionComponentTimingRecorder create(Object component) {
        ImmutableList.Builder<ProductionComponentTimingRecorder> recordersBuilder =
            ImmutableList.builder();
        for (ProductionComponentTimingRecorder.Factory delegate : delegates) {
          try {
            ProductionComponentTimingRecorder recorder = delegate.create(component);
            if (recorder != null) {
              recordersBuilder.add(recorder);
            }
          } catch (RuntimeException e) {
            logCreateException(e, delegate, component);
          }
        }
        ImmutableList<ProductionComponentTimingRecorder> recorders = recordersBuilder.build();
        switch (recorders.size()) {
          case 0:
            return noOpProductionComponentTimingRecorder();
          case 1:
            return new NonThrowingProductionComponentTimingRecorder(
                Iterables.getOnlyElement(recorders));
          default:
            return new DelegatingProductionComponentTimingRecorder(recorders);
        }
      }
    }
  }

  /**
   * A producer recorder that delegates to several recorders, and catches and logs all exceptions
   * that the delegates throw.
   */
  private static final class DelegatingProducerTimingRecorder extends ProducerTimingRecorder {
    private final ImmutableList<ProducerTimingRecorder> delegates;

    DelegatingProducerTimingRecorder(ImmutableList<ProducerTimingRecorder> delegates) {
      this.delegates = delegates;
    }

    @Override
    public void recordMethod(long startedNanos, long durationNanos) {
      for (ProducerTimingRecorder delegate : delegates) {
        try {
          delegate.recordMethod(startedNanos, durationNanos);
        } catch (RuntimeException e) {
          logProducerTimingRecorderMethodException(e, delegate, "recordMethod");
        }
      }
    }

    @Override
    public void recordSuccess(long latencyNanos) {
      for (ProducerTimingRecorder delegate : delegates) {
        try {
          delegate.recordSuccess(latencyNanos);
        } catch (RuntimeException e) {
          logProducerTimingRecorderMethodException(e, delegate, "recordSuccess");
        }
      }
    }

    @Override
    public void recordFailure(Throwable exception, long latencyNanos) {
      for (ProducerTimingRecorder delegate : delegates) {
        try {
          delegate.recordFailure(exception, latencyNanos);
        } catch (RuntimeException e) {
          logProducerTimingRecorderMethodException(e, delegate, "recordFailure");
        }
      }
    }

    @Override
    public void recordSkip(Throwable exception) {
      for (ProducerTimingRecorder delegate : delegates) {
        try {
          delegate.recordSkip(exception);
        } catch (RuntimeException e) {
          logProducerTimingRecorderMethodException(e, delegate, "recordSkip");
        }
      }
    }
  }

  /** Returns a recorder factory that returns no-op component recorders. */
  public static ProductionComponentTimingRecorder.Factory
      noOpProductionComponentTimingRecorderFactory() {
    return NO_OP_PRODUCTION_COMPONENT_TIMING_RECORDER_FACTORY;
  }

  /** Returns a component recorder that returns no-op producer recorders. */
  public static ProductionComponentTimingRecorder noOpProductionComponentTimingRecorder() {
    return NO_OP_PRODUCTION_COMPONENT_TIMING_RECORDER;
  }

  private static final ProductionComponentTimingRecorder.Factory
      NO_OP_PRODUCTION_COMPONENT_TIMING_RECORDER_FACTORY =
          new ProductionComponentTimingRecorder.Factory() {
            @Override
            public ProductionComponentTimingRecorder create(Object component) {
              return noOpProductionComponentTimingRecorder();
            }
          };

  private static final ProductionComponentTimingRecorder
      NO_OP_PRODUCTION_COMPONENT_TIMING_RECORDER =
          new ProductionComponentTimingRecorder() {
            @Override
            public ProducerTimingRecorder producerTimingRecorderFor(ProducerToken token) {
              return ProducerTimingRecorder.noOp();
            }
          };

  private static void logCreateException(
      RuntimeException e, ProductionComponentTimingRecorder.Factory factory, Object component) {
    logger.log(
        Level.SEVERE,
        "RuntimeException while calling ProductionComponentTimingRecorder.Factory.create on"
            + " factory "
            + factory
            + " with component "
            + component,
        e);
  }

  private static void logProducerTimingRecorderForException(
      RuntimeException e, ProductionComponentTimingRecorder recorder, ProducerToken token) {
    logger.log(
        Level.SEVERE,
        "RuntimeException while calling ProductionComponentTimingRecorder.producerTimingRecorderFor"
            + "on recorder "
            + recorder
            + " with token "
            + token,
        e);
  }

  private static void logProducerTimingRecorderMethodException(
      RuntimeException e, ProducerTimingRecorder recorder, String method) {
    logger.log(
        Level.SEVERE,
        "RuntimeException while calling ProducerTimingRecorder."
            + method
            + " on recorder "
            + recorder,
        e);
  }

  private TimingRecorders() {}
}
