/*
 * 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.drm;

import static androidx.media3.common.util.Util.postOrRun;

import android.os.Handler;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import androidx.media3.common.Player;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import java.util.concurrent.CopyOnWriteArrayList;

/** Listener of {@link DrmSessionManager} events. */
@UnstableApi
public interface DrmSessionEventListener {

  /**
   * @deprecated Implement {@link #onDrmSessionAcquired(int, MediaPeriodId, int)} instead.
   */
  @Deprecated
  default void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}

  /**
   * Called each time a drm session is acquired.
   *
   * @param windowIndex The window index in the timeline this media period belongs to.
   * @param mediaPeriodId The {@link MediaPeriodId} associated with the drm session.
   * @param state The {@link DrmSession.State} of the session when the acquisition completed.
   */
  default void onDrmSessionAcquired(
      int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) {}

  /**
   * Called each time keys are loaded.
   *
   * @param windowIndex The window index in the timeline this media period belongs to.
   * @param mediaPeriodId The {@link MediaPeriodId} associated with the drm session.
   */
  default void onDrmKeysLoaded(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}

  /**
   * Called when a drm error occurs.
   *
   * <p>This method being called does not indicate that playback has failed, or that it will fail.
   * The player may be able to recover from the error and continue. Hence applications should
   * <em>not</em> implement this method to display a user visible error or initiate an application
   * level retry ({@link Player.Listener#onPlayerError} is the appropriate place to implement such
   * behavior). This method is called to provide the application with an opportunity to log the
   * error if it wishes to do so.
   *
   * @param windowIndex The window index in the timeline this media period belongs to.
   * @param mediaPeriodId The {@link MediaPeriodId} associated with the drm session.
   * @param error The corresponding exception.
   */
  default void onDrmSessionManagerError(
      int windowIndex, @Nullable MediaPeriodId mediaPeriodId, Exception error) {}

  /**
   * Called each time offline keys are restored.
   *
   * @param windowIndex The window index in the timeline this media period belongs to.
   * @param mediaPeriodId The {@link MediaPeriodId} associated with the drm session.
   */
  default void onDrmKeysRestored(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}

  /**
   * Called each time offline keys are removed.
   *
   * @param windowIndex The window index in the timeline this media period belongs to.
   * @param mediaPeriodId The {@link MediaPeriodId} associated with the drm session.
   */
  default void onDrmKeysRemoved(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}

  /**
   * Called each time a drm session is released.
   *
   * @param windowIndex The window index in the timeline this media period belongs to.
   * @param mediaPeriodId The {@link MediaPeriodId} associated with the drm session.
   */
  default void onDrmSessionReleased(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}

  /** Dispatches events to {@link DrmSessionEventListener DrmSessionEventListeners}. */
  class EventDispatcher {

    /** The timeline window index reported with the events. */
    public final int windowIndex;
    /** The {@link MediaPeriodId} reported with the events. */
    @Nullable public final MediaPeriodId mediaPeriodId;

    private final CopyOnWriteArrayList<EventDispatcher.ListenerAndHandler> listenerAndHandlers;

    /** Creates an event dispatcher. */
    public EventDispatcher() {
      this(
          /* listenerAndHandlers= */ new CopyOnWriteArrayList<>(),
          /* windowIndex= */ 0,
          /* mediaPeriodId= */ null);
    }

    private EventDispatcher(
        CopyOnWriteArrayList<EventDispatcher.ListenerAndHandler> listenerAndHandlers,
        int windowIndex,
        @Nullable MediaPeriodId mediaPeriodId) {
      this.listenerAndHandlers = listenerAndHandlers;
      this.windowIndex = windowIndex;
      this.mediaPeriodId = mediaPeriodId;
    }

    /**
     * Creates a view of the event dispatcher with the provided window index and media period id.
     *
     * @param windowIndex The timeline window index to be reported with the events.
     * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events.
     * @return A view of the event dispatcher with the pre-configured parameters.
     */
    @CheckResult
    public EventDispatcher withParameters(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
      return new EventDispatcher(listenerAndHandlers, windowIndex, mediaPeriodId);
    }

    /**
     * Adds a listener to the event dispatcher.
     *
     * @param handler A handler on the which listener events will be posted.
     * @param eventListener The listener to be added.
     */
    public void addEventListener(Handler handler, DrmSessionEventListener eventListener) {
      Assertions.checkNotNull(handler);
      Assertions.checkNotNull(eventListener);
      listenerAndHandlers.add(new ListenerAndHandler(handler, eventListener));
    }

    /**
     * Removes a listener from the event dispatcher.
     *
     * @param eventListener The listener to be removed.
     */
    public void removeEventListener(DrmSessionEventListener eventListener) {
      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
        if (listenerAndHandler.listener == eventListener) {
          listenerAndHandlers.remove(listenerAndHandler);
        }
      }
    }

    /**
     * Dispatches {@link #onDrmSessionAcquired(int, MediaPeriodId, int)} and {@link
     * #onDrmSessionAcquired(int, MediaPeriodId)}.
     */
    @SuppressWarnings("deprecation") // Calls deprecated listener method.
    public void drmSessionAcquired(@DrmSession.State int state) {
      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
        DrmSessionEventListener listener = listenerAndHandler.listener;
        postOrRun(
            listenerAndHandler.handler,
            () -> {
              listener.onDrmSessionAcquired(windowIndex, mediaPeriodId);
              listener.onDrmSessionAcquired(windowIndex, mediaPeriodId, state);
            });
      }
    }

    /** Dispatches {@link #onDrmKeysLoaded(int, MediaPeriodId)}. */
    public void drmKeysLoaded() {
      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
        DrmSessionEventListener listener = listenerAndHandler.listener;
        postOrRun(
            listenerAndHandler.handler, () -> listener.onDrmKeysLoaded(windowIndex, mediaPeriodId));
      }
    }

    /** Dispatches {@link #onDrmSessionManagerError(int, MediaPeriodId, Exception)}. */
    public void drmSessionManagerError(Exception error) {
      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
        DrmSessionEventListener listener = listenerAndHandler.listener;
        postOrRun(
            listenerAndHandler.handler,
            () -> listener.onDrmSessionManagerError(windowIndex, mediaPeriodId, error));
      }
    }

    /** Dispatches {@link #onDrmKeysRestored(int, MediaPeriodId)}. */
    public void drmKeysRestored() {
      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
        DrmSessionEventListener listener = listenerAndHandler.listener;
        postOrRun(
            listenerAndHandler.handler,
            () -> listener.onDrmKeysRestored(windowIndex, mediaPeriodId));
      }
    }

    /** Dispatches {@link #onDrmKeysRemoved(int, MediaPeriodId)}. */
    public void drmKeysRemoved() {
      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
        DrmSessionEventListener listener = listenerAndHandler.listener;
        postOrRun(
            listenerAndHandler.handler,
            () -> listener.onDrmKeysRemoved(windowIndex, mediaPeriodId));
      }
    }

    /** Dispatches {@link #onDrmSessionReleased(int, MediaPeriodId)}. */
    public void drmSessionReleased() {
      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
        DrmSessionEventListener listener = listenerAndHandler.listener;
        postOrRun(
            listenerAndHandler.handler,
            () -> listener.onDrmSessionReleased(windowIndex, mediaPeriodId));
      }
    }

    private static final class ListenerAndHandler {

      public Handler handler;
      public DrmSessionEventListener listener;

      public ListenerAndHandler(Handler handler, DrmSessionEventListener listener) {
        this.handler = handler;
        this.listener = listener;
      }
    }
  }
}
