/*
 * Copyright (C) 2017 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.test.utils;

import static com.google.common.truth.Truth.assertThat;

import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Period;
import androidx.media3.common.Timeline.Window;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import org.checkerframework.checker.nullness.compatqual.NullableType;

/** Assertion methods for {@link Timeline}. */
@UnstableApi
public final class TimelineAsserts {

  private static final int[] REPEAT_MODES = {
    Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL
  };

  /** Assert that timeline is empty (i.e. has no windows or periods). */
  public static void assertEmpty(Timeline timeline) {
    assertWindowTags(timeline);
    assertPeriodCounts(timeline);
    for (boolean shuffled : new boolean[] {false, true}) {
      assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(C.INDEX_UNSET);
      assertThat(timeline.getLastWindowIndex(shuffled)).isEqualTo(C.INDEX_UNSET);
    }
  }

  /**
   * Asserts that window tags are set correctly.
   *
   * @param expectedWindowTags A list of expected window tags. If a tag is unknown or not important
   *     {@code null} can be passed to skip this window.
   */
  public static void assertWindowTags(
      Timeline timeline, @NullableType Object... expectedWindowTags) {
    Window window = new Window();
    assertThat(timeline.getWindowCount()).isEqualTo(expectedWindowTags.length);
    for (int i = 0; i < timeline.getWindowCount(); i++) {
      timeline.getWindow(i, window);
      if (expectedWindowTags[i] != null) {
        MediaItem.LocalConfiguration localConfiguration = window.mediaItem.localConfiguration;
        assertThat(localConfiguration).isNotNull();
        assertThat(Util.castNonNull(localConfiguration).tag).isEqualTo(expectedWindowTags[i]);
      }
    }
  }

  /** Asserts that window properties {@link Window}.isDynamic are set correctly. */
  public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) {
    Window window = new Window();
    for (int i = 0; i < timeline.getWindowCount(); i++) {
      timeline.getWindow(i, window);
      assertThat(window.isDynamic).isEqualTo(windowIsDynamic[i]);
    }
  }

  /**
   * Asserts that previous window indices for each window depending on the repeat mode and the
   * shuffle mode are equal to the given sequence.
   */
  public static void assertPreviousWindowIndices(
      Timeline timeline,
      @Player.RepeatMode int repeatMode,
      boolean shuffleModeEnabled,
      int... expectedPreviousWindowIndices) {
    for (int i = 0; i < timeline.getWindowCount(); i++) {
      assertThat(timeline.getPreviousWindowIndex(i, repeatMode, shuffleModeEnabled))
          .isEqualTo(expectedPreviousWindowIndices[i]);
    }
  }

  /**
   * Asserts that next window indices for each window depending on the repeat mode and the shuffle
   * mode are equal to the given sequence.
   */
  public static void assertNextWindowIndices(
      Timeline timeline,
      @Player.RepeatMode int repeatMode,
      boolean shuffleModeEnabled,
      int... expectedNextWindowIndices) {
    for (int i = 0; i < timeline.getWindowCount(); i++) {
      assertThat(timeline.getNextWindowIndex(i, repeatMode, shuffleModeEnabled))
          .isEqualTo(expectedNextWindowIndices[i]);
    }
  }

  /**
   * Asserts that previous window indices for each window of the actual timeline are equal to the
   * indices of the expected timeline depending on the repeat mode and the shuffle mode.
   */
  public static void assertEqualPreviousWindowIndices(
      Timeline expectedTimeline,
      Timeline actualTimeline,
      @Player.RepeatMode int repeatMode,
      boolean shuffleModeEnabled) {
    for (int windowIndex = 0; windowIndex < actualTimeline.getWindowCount(); windowIndex++) {
      assertThat(actualTimeline.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled))
          .isEqualTo(
              expectedTimeline.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled));
    }
  }

  /**
   * Asserts that next window indices for each window of the actual timeline are equal to the
   * indices of the expected timeline depending on the repeat mode and the shuffle mode.
   */
  public static void assertEqualNextWindowIndices(
      Timeline expectedTimeline,
      Timeline actualTimeline,
      @Player.RepeatMode int repeatMode,
      boolean shuffleModeEnabled) {
    for (int windowIndex = 0; windowIndex < actualTimeline.getWindowCount(); windowIndex++) {
      assertThat(actualTimeline.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled))
          .isEqualTo(
              expectedTimeline.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled));
    }
  }

  /**
   * Asserts that the durations of the periods in the {@link Timeline} and the durations in the
   * given sequence are equal.
   */
  public static void assertPeriodDurations(Timeline timeline, long... durationsUs) {
    int periodCount = timeline.getPeriodCount();
    assertThat(periodCount).isEqualTo(durationsUs.length);
    Period period = new Period();
    for (int i = 0; i < periodCount; i++) {
      assertThat(timeline.getPeriod(i, period).durationUs).isEqualTo(durationsUs[i]);
    }
  }

  /**
   * Asserts that period counts for each window are set correctly. Also asserts that {@link
   * Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it asserts
   * the correct behavior of {@link Timeline#getNextWindowIndex(int, int, boolean)}.
   */
  public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) {
    int windowCount = timeline.getWindowCount();
    assertThat(windowCount).isEqualTo(expectedPeriodCounts.length);
    int[] accumulatedPeriodCounts = new int[windowCount + 1];
    accumulatedPeriodCounts[0] = 0;
    for (int i = 0; i < windowCount; i++) {
      accumulatedPeriodCounts[i + 1] = accumulatedPeriodCounts[i] + expectedPeriodCounts[i];
    }
    assertThat(timeline.getPeriodCount())
        .isEqualTo(accumulatedPeriodCounts[accumulatedPeriodCounts.length - 1]);
    Window window = new Window();
    Period period = new Period();
    for (int i = 0; i < windowCount; i++) {
      timeline.getWindow(i, window);
      assertThat(window.firstPeriodIndex).isEqualTo(accumulatedPeriodCounts[i]);
      assertThat(window.lastPeriodIndex).isEqualTo(accumulatedPeriodCounts[i + 1] - 1);
    }
    int expectedWindowIndex = 0;
    for (int i = 0; i < timeline.getPeriodCount(); i++) {
      timeline.getPeriod(i, period, true);
      while (i >= accumulatedPeriodCounts[expectedWindowIndex + 1]) {
        expectedWindowIndex++;
      }
      assertThat(period.windowIndex).isEqualTo(expectedWindowIndex);
      Object periodUid = Assertions.checkNotNull(period.uid);
      assertThat(timeline.getIndexOfPeriod(periodUid)).isEqualTo(i);
      assertThat(timeline.getUidOfPeriod(i)).isEqualTo(periodUid);
      for (int repeatMode : REPEAT_MODES) {
        if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) {
          assertThat(timeline.getNextPeriodIndex(i, period, window, repeatMode, false))
              .isEqualTo(i + 1);
        } else {
          int nextWindow = timeline.getNextWindowIndex(expectedWindowIndex, repeatMode, false);
          int nextPeriod =
              nextWindow == C.INDEX_UNSET ? C.INDEX_UNSET : accumulatedPeriodCounts[nextWindow];
          assertThat(timeline.getNextPeriodIndex(i, period, window, repeatMode, false))
              .isEqualTo(nextPeriod);
        }
      }
    }
  }

  /** Asserts that periods' {@link Period#getAdGroupCount()} are set correctly. */
  public static void assertAdGroupCounts(Timeline timeline, int... expectedAdGroupCounts) {
    Period period = new Period();
    for (int i = 0; i < timeline.getPeriodCount(); i++) {
      timeline.getPeriod(i, period);
      assertThat(period.getAdGroupCount()).isEqualTo(expectedAdGroupCounts[i]);
    }
  }

  /**
   * Asserts that {@link Timeline timelines} are equal except {@link Window#uid}, {@link
   * Window#manifest}, {@link Period#id}, and {@link Period#uid}.
   */
  public static void assertEqualsExceptIdsAndManifest(
      Timeline expectedTimeline, Timeline actualTimeline) {
    assertThat(actualTimeline.getWindowCount()).isEqualTo(expectedTimeline.getWindowCount());
    for (int i = 0; i < actualTimeline.getWindowCount(); i++) {
      Window expectedWindow = new Window();
      Window actualWindow = new Window();
      assertWindowEqualsExceptUidAndManifest(
          expectedTimeline.getWindow(i, expectedWindow, /* defaultPositionProjectionUs= */ 0),
          actualTimeline.getWindow(i, actualWindow, /* defaultPositionProjectionUs= */ 0));
    }
    assertThat(actualTimeline.getPeriodCount()).isEqualTo(expectedTimeline.getPeriodCount());
    for (int i = 0; i < actualTimeline.getPeriodCount(); i++) {
      Period expectedPeriod = new Period();
      Period actualPeriod = new Period();
      assertPeriodEqualsExceptIds(
          expectedTimeline.getPeriod(i, expectedPeriod, /* setIds= */ false),
          actualTimeline.getPeriod(i, actualPeriod, /* setIds= */ false));
    }
  }

  /**
   * Asserts that {@link Window windows} are equal except {@link Window#uid} and {@link
   * Window#manifest}.
   */
  public static void assertWindowEqualsExceptUidAndManifest(
      Window expectedWindow, Window actualWindow) {
    Object uid = expectedWindow.uid;
    @Nullable Object manifest = expectedWindow.manifest;
    try {
      expectedWindow.uid = actualWindow.uid;
      expectedWindow.manifest = actualWindow.manifest;
      assertThat(actualWindow).isEqualTo(expectedWindow);
    } finally {
      expectedWindow.uid = uid;
      expectedWindow.manifest = manifest;
    }
  }

  /**
   * Asserts that {@link Period periods} are equal except {@link Period#id} and {@link Period#uid}.
   */
  public static void assertPeriodEqualsExceptIds(Period expectedPeriod, Period actualPeriod) {
    @Nullable Object id = expectedPeriod.id;
    @Nullable Object uid = expectedPeriod.uid;
    try {
      expectedPeriod.id = actualPeriod.id;
      expectedPeriod.uid = actualPeriod.uid;
      assertThat(actualPeriod).isEqualTo(expectedPeriod);
    } finally {
      expectedPeriod.id = id;
      expectedPeriod.uid = uid;
    }
  }

  private TimelineAsserts() {}
}
