/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * 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 androidx.media3.exoplayer.upstream;

import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.upstream.Loader.Callback;
import androidx.media3.exoplayer.upstream.Loader.Loadable;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * A policy that defines how load errors are handled.
 *
 * <p>Some loaders are able to choose between a number of alternate resources. Such loaders will
 * call {@link #getFallbackSelectionFor(FallbackOptions, LoadErrorInfo)} when a load error occurs.
 * The {@link FallbackSelection} returned by the policy defines whether the loader should fall back
 * to using another resource, and if so the duration for which the failing resource should be
 * excluded.
 *
 * <p>When fallback does not take place, a loader will call {@link
 * #getRetryDelayMsFor(LoadErrorInfo)}. The value returned by the policy defines whether the failed
 * load can be retried, and if so the duration to wait before retrying. If the policy indicates that
 * a load error should not be retried, it will be considered fatal by the loader. The loader may
 * also consider load errors that can be retried fatal if at least {@link
 * #getMinimumLoadableRetryCount(int)} retries have been attempted.
 *
 * <p>Methods are invoked on the playback thread.
 */
@UnstableApi
public interface LoadErrorHandlingPolicy {

  /** Fallback type. One of {@link #FALLBACK_TYPE_LOCATION} or {@link #FALLBACK_TYPE_TRACK}. */
  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @IntDef({FALLBACK_TYPE_LOCATION, FALLBACK_TYPE_TRACK})
  @interface FallbackType {}

  /**
   * Fallback to the same resource at a different location (i.e., a different URL through which the
   * exact same data can be requested).
   */
  int FALLBACK_TYPE_LOCATION = 1;
  /**
   * Fallback to a different track (i.e., a different representation of the same content; for
   * example the same video encoded at a different bitrate or resolution).
   */
  int FALLBACK_TYPE_TRACK = 2;

  /** Holds information about a load task error. */
  final class LoadErrorInfo {

    /** The {@link LoadEventInfo} associated with the load that encountered an error. */
    public final LoadEventInfo loadEventInfo;
    /** {@link MediaLoadData} associated with the load that encountered an error. */
    public final MediaLoadData mediaLoadData;
    /** The exception associated to the load error. */
    public final IOException exception;
    /** The number of errors this load task has encountered, including this one. */
    public final int errorCount;

    /** Creates an instance with the given values. */
    public LoadErrorInfo(
        LoadEventInfo loadEventInfo,
        MediaLoadData mediaLoadData,
        IOException exception,
        int errorCount) {
      this.loadEventInfo = loadEventInfo;
      this.mediaLoadData = mediaLoadData;
      this.exception = exception;
      this.errorCount = errorCount;
    }
  }

  /** Holds information about the available fallback options. */
  final class FallbackOptions {
    /** The number of available locations. */
    public final int numberOfLocations;
    /** The number of locations that are already excluded. */
    public final int numberOfExcludedLocations;
    /** The number of tracks. */
    public final int numberOfTracks;
    /** The number of tracks that are already excluded. */
    public final int numberOfExcludedTracks;

    /** Creates an instance. */
    public FallbackOptions(
        int numberOfLocations,
        int numberOfExcludedLocations,
        int numberOfTracks,
        int numberOfExcludedTracks) {
      this.numberOfLocations = numberOfLocations;
      this.numberOfExcludedLocations = numberOfExcludedLocations;
      this.numberOfTracks = numberOfTracks;
      this.numberOfExcludedTracks = numberOfExcludedTracks;
    }

    /** Returns whether a fallback is available for the given {@link FallbackType fallback type}. */
    public boolean isFallbackAvailable(@FallbackType int type) {
      return type == FALLBACK_TYPE_LOCATION
          ? numberOfLocations - numberOfExcludedLocations > 1
          : numberOfTracks - numberOfExcludedTracks > 1;
    }
  }

  /** A selected fallback option. */
  final class FallbackSelection {
    /** The type of fallback. */
    public final @FallbackType int type;
    /** The duration for which the failing resource should be excluded, in milliseconds. */
    public final long exclusionDurationMs;

    /**
     * Creates an instance.
     *
     * @param type The type of fallback.
     * @param exclusionDurationMs The duration for which the failing resource should be excluded, in
     *     milliseconds. Must be non-negative.
     */
    public FallbackSelection(@FallbackType int type, long exclusionDurationMs) {
      checkArgument(exclusionDurationMs >= 0);
      this.type = type;
      this.exclusionDurationMs = exclusionDurationMs;
    }
  }

  /**
   * Returns whether a loader should fall back to using another resource on encountering an error,
   * and if so the duration for which the failing resource should be excluded.
   *
   * <p>If the returned {@link FallbackSelection#type fallback type} was not {@link
   * FallbackOptions#isFallbackAvailable(int) advertised as available}, then the loader will not
   * fall back.
   *
   * @param fallbackOptions The available fallback options.
   * @param loadErrorInfo A {@link LoadErrorInfo} holding information about the load error.
   * @return The selected fallback, or {@code null} if the calling loader should not fall back.
   */
  @Nullable
  FallbackSelection getFallbackSelectionFor(
      FallbackOptions fallbackOptions, LoadErrorInfo loadErrorInfo);

  /**
   * Returns whether a loader can retry on encountering an error, and if so the duration to wait
   * before retrying. A return value of {@link C#TIME_UNSET} indicates that the error is fatal and
   * should not be retried.
   *
   * <p>For loads that can be retried, loaders may ignore the retry delay returned by this method in
   * order to wait for a specific event before retrying.
   *
   * @param loadErrorInfo A {@link LoadErrorInfo} holding information about the load error.
   * @return The duration to wait before retrying in milliseconds, or {@link C#TIME_UNSET} if the
   *     error is fatal and should not be retried.
   */
  long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo);

  /**
   * Called once {@code loadTaskId} will not be associated with any more load errors.
   *
   * <p>Implementations should clean up any resources associated with {@code loadTaskId} when this
   * method is called.
   */
  default void onLoadTaskConcluded(long loadTaskId) {}

  /**
   * Returns the minimum number of times to retry a load before a load error that can be retried may
   * be considered fatal.
   *
   * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data being
   *     loaded.
   * @return The minimum number of times to retry a load before a load error that can be retried may
   *     be considered fatal.
   * @see Loader#startLoading(Loadable, Callback, int)
   */
  int getMinimumLoadableRetryCount(int dataType);
}
