/*
 * Copyright (C) 2019 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;

import static java.lang.Math.max;
import static java.lang.Math.min;

import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MaskingMediaPeriod;
import androidx.media3.exoplayer.source.MaskingMediaSource;
import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSourceEventListener;
import androidx.media3.exoplayer.source.ShuffleOrder;
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder;
import androidx.media3.exoplayer.upstream.Allocator;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
 * during playback. It is valid for the same {@link MediaSource} instance to be present more than
 * once in the playlist.
 *
 * <p>With the exception of the constructor, all methods are called on the playback thread.
 */
/* package */ final class MediaSourceList {

  /** Listener for source events. */
  public interface MediaSourceListInfoRefreshListener {

    /**
     * Called when the timeline of a media item has changed and a new timeline that reflects the
     * current playlist state needs to be created by calling {@link #createTimeline()}.
     *
     * <p>Called on the playback thread.
     */
    void onPlaylistUpdateRequested();
  }

  private static final String TAG = "MediaSourceList";

  private final PlayerId playerId;
  private final List<MediaSourceHolder> mediaSourceHolders;
  private final IdentityHashMap<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
  private final Map<Object, MediaSourceHolder> mediaSourceByUid;
  private final MediaSourceListInfoRefreshListener mediaSourceListInfoListener;
  private final MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher;
  private final DrmSessionEventListener.EventDispatcher drmEventDispatcher;
  private final HashMap<MediaSourceList.MediaSourceHolder, MediaSourceAndListener> childSources;
  private final Set<MediaSourceHolder> enabledMediaSourceHolders;

  private ShuffleOrder shuffleOrder;
  private boolean isPrepared;

  @Nullable private TransferListener mediaTransferListener;

  /**
   * Creates the media source list.
   *
   * @param listener The {@link MediaSourceListInfoRefreshListener} to be informed of timeline
   *     changes.
   * @param analyticsCollector An {@link AnalyticsCollector} to be registered for media source
   *     events.
   * @param analyticsCollectorHandler The {@link Handler} to call {@link AnalyticsCollector} methods
   *     on.
   * @param playerId The {@link PlayerId} of the player using this list.
   */
  public MediaSourceList(
      MediaSourceListInfoRefreshListener listener,
      AnalyticsCollector analyticsCollector,
      Handler analyticsCollectorHandler,
      PlayerId playerId) {
    this.playerId = playerId;
    mediaSourceListInfoListener = listener;
    shuffleOrder = new DefaultShuffleOrder(0);
    mediaSourceByMediaPeriod = new IdentityHashMap<>();
    mediaSourceByUid = new HashMap<>();
    mediaSourceHolders = new ArrayList<>();
    mediaSourceEventDispatcher = new MediaSourceEventListener.EventDispatcher();
    drmEventDispatcher = new DrmSessionEventListener.EventDispatcher();
    childSources = new HashMap<>();
    enabledMediaSourceHolders = new HashSet<>();
    mediaSourceEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
    drmEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
  }

  /**
   * Sets the media sources replacing any sources previously contained in the playlist.
   *
   * @param holders The list of {@link MediaSourceHolder}s to set.
   * @param shuffleOrder The new shuffle order.
   * @return The new {@link Timeline}.
   */
  public Timeline setMediaSources(List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder) {
    removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size());
    return addMediaSources(/* index= */ this.mediaSourceHolders.size(), holders, shuffleOrder);
  }

  /**
   * Adds multiple {@link MediaSourceHolder}s to the playlist.
   *
   * @param index The index at which the new {@link MediaSourceHolder}s will be inserted. This index
   *     must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @param holders A list of {@link MediaSourceHolder}s to be added.
   * @param shuffleOrder The new shuffle order.
   * @return The new {@link Timeline}.
   */
  public Timeline addMediaSources(
      int index, List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder) {
    if (!holders.isEmpty()) {
      this.shuffleOrder = shuffleOrder;
      for (int insertionIndex = index; insertionIndex < index + holders.size(); insertionIndex++) {
        MediaSourceHolder holder = holders.get(insertionIndex - index);
        if (insertionIndex > 0) {
          MediaSourceHolder previousHolder = mediaSourceHolders.get(insertionIndex - 1);
          Timeline previousTimeline = previousHolder.mediaSource.getTimeline();
          holder.reset(
              /* firstWindowIndexInChild= */ previousHolder.firstWindowIndexInChild
                  + previousTimeline.getWindowCount());
        } else {
          holder.reset(/* firstWindowIndexInChild= */ 0);
        }
        Timeline newTimeline = holder.mediaSource.getTimeline();
        correctOffsets(
            /* startIndex= */ insertionIndex,
            /* windowOffsetUpdate= */ newTimeline.getWindowCount());
        mediaSourceHolders.add(insertionIndex, holder);
        mediaSourceByUid.put(holder.uid, holder);
        if (isPrepared) {
          prepareChildSource(holder);
          if (mediaSourceByMediaPeriod.isEmpty()) {
            enabledMediaSourceHolders.add(holder);
          } else {
            disableChildSource(holder);
          }
        }
      }
    }
    return createTimeline();
  }

  /**
   * Removes a range of {@link MediaSourceHolder}s from the playlist, by specifying an initial index
   * (included) and a final index (excluded).
   *
   * <p>Note: when specified range is empty, no actual media source is removed and no exception is
   * thrown.
   *
   * @param fromIndex The initial range index, pointing to the first media source that will be
   *     removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @param toIndex The final range index, pointing to the first media source that will be left
   *     untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @param shuffleOrder The new shuffle order.
   * @return The new {@link Timeline}.
   * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
   *     {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex}
   */
  public Timeline removeMediaSourceRange(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) {
    Assertions.checkArgument(fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize());
    this.shuffleOrder = shuffleOrder;
    removeMediaSourcesInternal(fromIndex, toIndex);
    return createTimeline();
  }

  /**
   * Moves an existing media source within the playlist.
   *
   * @param currentIndex The current index of the media source in the playlist. This index must be
   *     in the range of 0 &lt;= index &lt; {@link #getSize()}.
   * @param newIndex The target index of the media source in the playlist. This index must be in the
   *     range of 0 &lt;= index &lt; {@link #getSize()}.
   * @param shuffleOrder The new shuffle order.
   * @return The new {@link Timeline}.
   * @throws IllegalArgumentException When an index is invalid, i.e. {@code currentIndex} &lt; 0,
   *     {@code currentIndex} &gt;= {@link #getSize()}, {@code newIndex} &lt; 0
   */
  public Timeline moveMediaSource(int currentIndex, int newIndex, ShuffleOrder shuffleOrder) {
    return moveMediaSourceRange(currentIndex, currentIndex + 1, newIndex, shuffleOrder);
  }

  /**
   * Moves a range of media sources within the playlist.
   *
   * <p>Note: when specified range is empty or the from index equals the new from index, no actual
   * media source is moved and no exception is thrown.
   *
   * @param fromIndex The initial range index, pointing to the first media source of the range that
   *     will be moved. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @param toIndex The final range index, pointing to the first media source that will be left
   *     untouched. This index must be larger or equals than {@code fromIndex}.
   * @param newFromIndex The target index of the first media source of the range that will be moved.
   * @param shuffleOrder The new shuffle order.
   * @return The new {@link Timeline}.
   * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
   *     {@code toIndex} &lt; {@code fromIndex}, {@code fromIndex} &gt; {@code toIndex}, {@code
   *     newFromIndex} &lt; 0
   */
  public Timeline moveMediaSourceRange(
      int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) {
    Assertions.checkArgument(
        fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize() && newFromIndex >= 0);
    this.shuffleOrder = shuffleOrder;
    if (fromIndex == toIndex || fromIndex == newFromIndex) {
      return createTimeline();
    }
    int startIndex = min(fromIndex, newFromIndex);
    int newEndIndex = newFromIndex + (toIndex - fromIndex) - 1;
    int endIndex = max(newEndIndex, toIndex - 1);
    int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild;
    Util.moveItems(mediaSourceHolders, fromIndex, toIndex, newFromIndex);
    for (int i = startIndex; i <= endIndex; i++) {
      MediaSourceHolder holder = mediaSourceHolders.get(i);
      holder.firstWindowIndexInChild = windowOffset;
      windowOffset += holder.mediaSource.getTimeline().getWindowCount();
    }
    return createTimeline();
  }

  /** Clears the playlist. */
  public Timeline clear(@Nullable ShuffleOrder shuffleOrder) {
    this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear();
    removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ getSize());
    return createTimeline();
  }

  /** Whether the playlist is prepared. */
  public boolean isPrepared() {
    return isPrepared;
  }

  /** Returns the number of media sources in the playlist. */
  public int getSize() {
    return mediaSourceHolders.size();
  }

  /**
   * Sets a new shuffle order to use when shuffling the child media sources.
   *
   * @param shuffleOrder A {@link ShuffleOrder}.
   */
  public Timeline setShuffleOrder(ShuffleOrder shuffleOrder) {
    int size = getSize();
    if (shuffleOrder.getLength() != size) {
      shuffleOrder =
          shuffleOrder
              .cloneAndClear()
              .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
    }
    this.shuffleOrder = shuffleOrder;
    return createTimeline();
  }

  /** Prepares the playlist. */
  public void prepare(@Nullable TransferListener mediaTransferListener) {
    Assertions.checkState(!isPrepared);
    this.mediaTransferListener = mediaTransferListener;
    for (int i = 0; i < mediaSourceHolders.size(); i++) {
      MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
      prepareChildSource(mediaSourceHolder);
      enabledMediaSourceHolders.add(mediaSourceHolder);
    }
    isPrepared = true;
  }

  /**
   * Returns a new {@link MediaPeriod} identified by {@code periodId}.
   *
   * @param id The identifier of the period.
   * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
   * @param startPositionUs The expected start position, in microseconds.
   * @return A new {@link MediaPeriod}.
   */
  public MediaPeriod createPeriod(
      MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs) {
    Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
    MediaSource.MediaPeriodId childMediaPeriodId =
        id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));
    MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid));
    enableMediaSource(holder);
    holder.activeMediaPeriodIds.add(childMediaPeriodId);
    MediaPeriod mediaPeriod =
        holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
    mediaSourceByMediaPeriod.put(mediaPeriod, holder);
    disableUnusedMediaSources();
    return mediaPeriod;
  }

  /**
   * Releases the period.
   *
   * @param mediaPeriod The period to release.
   */
  public void releasePeriod(MediaPeriod mediaPeriod) {
    MediaSourceHolder holder =
        Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
    holder.mediaSource.releasePeriod(mediaPeriod);
    holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);
    if (!mediaSourceByMediaPeriod.isEmpty()) {
      disableUnusedMediaSources();
    }
    maybeReleaseChildSource(holder);
  }

  /** Releases the playlist. */
  public void release() {
    for (MediaSourceAndListener childSource : childSources.values()) {
      try {
        childSource.mediaSource.releaseSource(childSource.caller);
      } catch (RuntimeException e) {
        // There's nothing we can do.
        Log.e(TAG, "Failed to release child source.", e);
      }
      childSource.mediaSource.removeEventListener(childSource.eventListener);
      childSource.mediaSource.removeDrmEventListener(childSource.eventListener);
    }
    childSources.clear();
    enabledMediaSourceHolders.clear();
    isPrepared = false;
  }

  /** Creates a timeline reflecting the current state of the playlist. */
  public Timeline createTimeline() {
    if (mediaSourceHolders.isEmpty()) {
      return Timeline.EMPTY;
    }
    int windowOffset = 0;
    for (int i = 0; i < mediaSourceHolders.size(); i++) {
      MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
      mediaSourceHolder.firstWindowIndexInChild = windowOffset;
      windowOffset += mediaSourceHolder.mediaSource.getTimeline().getWindowCount();
    }
    return new PlaylistTimeline(mediaSourceHolders, shuffleOrder);
  }

  // Internal methods.

  private void enableMediaSource(MediaSourceHolder mediaSourceHolder) {
    enabledMediaSourceHolders.add(mediaSourceHolder);
    @Nullable MediaSourceAndListener enabledChild = childSources.get(mediaSourceHolder);
    if (enabledChild != null) {
      enabledChild.mediaSource.enable(enabledChild.caller);
    }
  }

  private void disableUnusedMediaSources() {
    Iterator<MediaSourceHolder> iterator = enabledMediaSourceHolders.iterator();
    while (iterator.hasNext()) {
      MediaSourceHolder holder = iterator.next();
      if (holder.activeMediaPeriodIds.isEmpty()) {
        disableChildSource(holder);
        iterator.remove();
      }
    }
  }

  private void disableChildSource(MediaSourceHolder holder) {
    @Nullable MediaSourceAndListener disabledChild = childSources.get(holder);
    if (disabledChild != null) {
      disabledChild.mediaSource.disable(disabledChild.caller);
    }
  }

  private void removeMediaSourcesInternal(int fromIndex, int toIndex) {
    for (int index = toIndex - 1; index >= fromIndex; index--) {
      MediaSourceHolder holder = mediaSourceHolders.remove(index);
      mediaSourceByUid.remove(holder.uid);
      Timeline oldTimeline = holder.mediaSource.getTimeline();
      correctOffsets(
          /* startIndex= */ index, /* windowOffsetUpdate= */ -oldTimeline.getWindowCount());
      holder.isRemoved = true;
      if (isPrepared) {
        maybeReleaseChildSource(holder);
      }
    }
  }

  private void correctOffsets(int startIndex, int windowOffsetUpdate) {
    for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
      MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
      mediaSourceHolder.firstWindowIndexInChild += windowOffsetUpdate;
    }
  }

  // Internal methods to manage child sources.

  @Nullable
  private static MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
      MediaSourceHolder mediaSourceHolder, MediaSource.MediaPeriodId mediaPeriodId) {
    for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) {
      // Ensure the reported media period id has the same window sequence number as the one created
      // by this media source. Otherwise it does not belong to this child source.
      if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber
          == mediaPeriodId.windowSequenceNumber) {
        Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid);
        return mediaPeriodId.copyWithPeriodUid(periodUid);
      }
    }
    return null;
  }

  private static int getWindowIndexForChildWindowIndex(
      MediaSourceHolder mediaSourceHolder, int windowIndex) {
    return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
  }

  private void prepareChildSource(MediaSourceHolder holder) {
    MediaSource mediaSource = holder.mediaSource;
    MediaSource.MediaSourceCaller caller =
        (source, timeline) -> mediaSourceListInfoListener.onPlaylistUpdateRequested();
    ForwardingEventListener eventListener = new ForwardingEventListener(holder);
    childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener));
    mediaSource.addEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener);
    mediaSource.addDrmEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener);
    mediaSource.prepareSource(caller, mediaTransferListener, playerId);
  }

  private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {
    // Release if the source has been removed from the playlist and no periods are still active.
    if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) {
      MediaSourceAndListener removedChild =
          Assertions.checkNotNull(childSources.remove(mediaSourceHolder));
      removedChild.mediaSource.releaseSource(removedChild.caller);
      removedChild.mediaSource.removeEventListener(removedChild.eventListener);
      removedChild.mediaSource.removeDrmEventListener(removedChild.eventListener);
      enabledMediaSourceHolders.remove(mediaSourceHolder);
    }
  }

  /** Return uid of media source holder from period uid of concatenated source. */
  private static Object getMediaSourceHolderUid(Object periodUid) {
    return PlaylistTimeline.getChildTimelineUidFromConcatenatedUid(periodUid);
  }

  /** Return uid of child period from period uid of concatenated source. */
  private static Object getChildPeriodUid(Object periodUid) {
    return PlaylistTimeline.getChildPeriodUidFromConcatenatedUid(periodUid);
  }

  private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) {
    return PlaylistTimeline.getConcatenatedUid(holder.uid, childPeriodUid);
  }

  /** Data class to hold playlist media sources together with meta data needed to process them. */
  /* package */ static final class MediaSourceHolder implements MediaSourceInfoHolder {

    public final MaskingMediaSource mediaSource;
    public final Object uid;
    public final List<MediaSource.MediaPeriodId> activeMediaPeriodIds;

    public int firstWindowIndexInChild;
    public boolean isRemoved;

    public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) {
      this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation);
      this.activeMediaPeriodIds = new ArrayList<>();
      this.uid = new Object();
    }

    public void reset(int firstWindowIndexInChild) {
      this.firstWindowIndexInChild = firstWindowIndexInChild;
      this.isRemoved = false;
      this.activeMediaPeriodIds.clear();
    }

    @Override
    public Object getUid() {
      return uid;
    }

    @Override
    public Timeline getTimeline() {
      return mediaSource.getTimeline();
    }
  }

  private static final class MediaSourceAndListener {

    public final MediaSource mediaSource;
    public final MediaSource.MediaSourceCaller caller;
    public final ForwardingEventListener eventListener;

    public MediaSourceAndListener(
        MediaSource mediaSource,
        MediaSource.MediaSourceCaller caller,
        ForwardingEventListener eventListener) {
      this.mediaSource = mediaSource;
      this.caller = caller;
      this.eventListener = eventListener;
    }
  }

  private final class ForwardingEventListener
      implements MediaSourceEventListener, DrmSessionEventListener {

    private final MediaSourceList.MediaSourceHolder id;
    private MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher;
    private DrmSessionEventListener.EventDispatcher drmEventDispatcher;

    public ForwardingEventListener(MediaSourceList.MediaSourceHolder id) {
      mediaSourceEventDispatcher = MediaSourceList.this.mediaSourceEventDispatcher;
      drmEventDispatcher = MediaSourceList.this.drmEventDispatcher;
      this.id = id;
    }

    // MediaSourceEventListener implementation

    @Override
    public void onLoadStarted(
        int windowIndex,
        @Nullable MediaSource.MediaPeriodId mediaPeriodId,
        LoadEventInfo loadEventData,
        MediaLoadData mediaLoadData) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        mediaSourceEventDispatcher.loadStarted(loadEventData, mediaLoadData);
      }
    }

    @Override
    public void onLoadCompleted(
        int windowIndex,
        @Nullable MediaSource.MediaPeriodId mediaPeriodId,
        LoadEventInfo loadEventData,
        MediaLoadData mediaLoadData) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        mediaSourceEventDispatcher.loadCompleted(loadEventData, mediaLoadData);
      }
    }

    @Override
    public void onLoadCanceled(
        int windowIndex,
        @Nullable MediaSource.MediaPeriodId mediaPeriodId,
        LoadEventInfo loadEventData,
        MediaLoadData mediaLoadData) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        mediaSourceEventDispatcher.loadCanceled(loadEventData, mediaLoadData);
      }
    }

    @Override
    public void onLoadError(
        int windowIndex,
        @Nullable MediaSource.MediaPeriodId mediaPeriodId,
        LoadEventInfo loadEventData,
        MediaLoadData mediaLoadData,
        IOException error,
        boolean wasCanceled) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        mediaSourceEventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled);
      }
    }

    @Override
    public void onUpstreamDiscarded(
        int windowIndex,
        @Nullable MediaSource.MediaPeriodId mediaPeriodId,
        MediaLoadData mediaLoadData) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        mediaSourceEventDispatcher.upstreamDiscarded(mediaLoadData);
      }
    }

    @Override
    public void onDownstreamFormatChanged(
        int windowIndex,
        @Nullable MediaSource.MediaPeriodId mediaPeriodId,
        MediaLoadData mediaLoadData) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        mediaSourceEventDispatcher.downstreamFormatChanged(mediaLoadData);
      }
    }

    // DrmSessionEventListener implementation

    @Override
    public void onDrmSessionAcquired(
        int windowIndex,
        @Nullable MediaSource.MediaPeriodId mediaPeriodId,
        @DrmSession.State int state) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        drmEventDispatcher.drmSessionAcquired(state);
      }
    }

    @Override
    public void onDrmKeysLoaded(
        int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        drmEventDispatcher.drmKeysLoaded();
      }
    }

    @Override
    public void onDrmSessionManagerError(
        int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, Exception error) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        drmEventDispatcher.drmSessionManagerError(error);
      }
    }

    @Override
    public void onDrmKeysRestored(
        int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        drmEventDispatcher.drmKeysRestored();
      }
    }

    @Override
    public void onDrmKeysRemoved(
        int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        drmEventDispatcher.drmKeysRemoved();
      }
    }

    @Override
    public void onDrmSessionReleased(
        int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
        drmEventDispatcher.drmSessionReleased();
      }
    }

    /** Updates the event dispatcher and returns whether the event should be dispatched. */
    private boolean maybeUpdateEventDispatcher(
        int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) {
      @Nullable MediaSource.MediaPeriodId mediaPeriodId = null;
      if (childMediaPeriodId != null) {
        mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId);
        if (mediaPeriodId == null) {
          // Media period not found. Ignore event.
          return false;
        }
      }
      int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex);
      if (mediaSourceEventDispatcher.windowIndex != windowIndex
          || !Util.areEqual(mediaSourceEventDispatcher.mediaPeriodId, mediaPeriodId)) {
        mediaSourceEventDispatcher =
            MediaSourceList.this.mediaSourceEventDispatcher.withParameters(
                windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L);
      }
      if (drmEventDispatcher.windowIndex != windowIndex
          || !Util.areEqual(drmEventDispatcher.mediaPeriodId, mediaPeriodId)) {
        drmEventDispatcher =
            MediaSourceList.this.drmEventDispatcher.withParameters(windowIndex, mediaPeriodId);
      }
      return true;
    }
  }
}
