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

import static androidx.media3.common.C.INDEX_UNSET;
import static com.google.common.truth.Truth.assertThat;

import androidx.media3.common.C;
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder;
import androidx.media3.exoplayer.source.ShuffleOrder.UnshuffledShuffleOrder;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Unit test for {@link ShuffleOrder}. */
@RunWith(AndroidJUnit4.class)
public final class ShuffleOrderTest {

  public static final long RANDOM_SEED = 1234567890L;

  @Test
  public void defaultShuffleOrder() {
    assertShuffleOrderCorrectness(new DefaultShuffleOrder(0, RANDOM_SEED), 0);
    assertShuffleOrderCorrectness(new DefaultShuffleOrder(1, RANDOM_SEED), 1);
    assertShuffleOrderCorrectness(new DefaultShuffleOrder(5, RANDOM_SEED), 5);
    for (int initialLength = 0; initialLength < 4; initialLength++) {
      for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) {
        testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 0);
        testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 1);
        testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 5);
      }
    }
    testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0, 1);
    testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2, 3);
    testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4, 5);
    testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0, 1);
    testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 0, 1000);
    testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 0, 999);
    testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 0, 500);
    testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 100, 600);
    testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 500, 1000);
  }

  @Test
  public void defaultShuffleOrderSideloaded() {
    int[] shuffledIndices = new int[] {2, 1, 0, 4, 3};
    ShuffleOrder shuffleOrder = new DefaultShuffleOrder(shuffledIndices, RANDOM_SEED);
    assertThat(shuffleOrder.getFirstIndex()).isEqualTo(2);
    assertThat(shuffleOrder.getLastIndex()).isEqualTo(3);
    for (int i = 0; i < 4; i++) {
      assertThat(shuffleOrder.getNextIndex(shuffledIndices[i])).isEqualTo(shuffledIndices[i + 1]);
    }
    assertThat(shuffleOrder.getNextIndex(3)).isEqualTo(C.INDEX_UNSET);
    for (int i = 4; i > 0; i--) {
      assertThat(shuffleOrder.getPreviousIndex(shuffledIndices[i]))
          .isEqualTo(shuffledIndices[i - 1]);
    }
    assertThat(shuffleOrder.getPreviousIndex(2)).isEqualTo(C.INDEX_UNSET);
  }

  @Test
  public void unshuffledShuffleOrder() {
    assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(0), 0);
    assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(1), 1);
    assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(5), 5);
    for (int initialLength = 0; initialLength < 4; initialLength++) {
      for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) {
        testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 0);
        testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 1);
        testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 5);
      }
    }
    testCloneAndRemove(new UnshuffledShuffleOrder(5), 0, 1);
    testCloneAndRemove(new UnshuffledShuffleOrder(5), 2, 3);
    testCloneAndRemove(new UnshuffledShuffleOrder(5), 4, 5);
    testCloneAndRemove(new UnshuffledShuffleOrder(1), 0, 1);
    testCloneAndRemove(new UnshuffledShuffleOrder(1000), 0, 1000);
    testCloneAndRemove(new UnshuffledShuffleOrder(1000), 0, 999);
    testCloneAndRemove(new UnshuffledShuffleOrder(1000), 0, 500);
    testCloneAndRemove(new UnshuffledShuffleOrder(1000), 100, 600);
    testCloneAndRemove(new UnshuffledShuffleOrder(1000), 500, 1000);
  }

  @Test
  public void unshuffledShuffleOrderIsUnshuffled() {
    ShuffleOrder shuffleOrder = new UnshuffledShuffleOrder(5);
    assertThat(shuffleOrder.getFirstIndex()).isEqualTo(0);
    assertThat(shuffleOrder.getLastIndex()).isEqualTo(4);
    for (int i = 0; i < 4; i++) {
      assertThat(shuffleOrder.getNextIndex(i)).isEqualTo(i + 1);
    }
  }

  private static void assertShuffleOrderCorrectness(ShuffleOrder shuffleOrder, int length) {
    assertThat(shuffleOrder.getLength()).isEqualTo(length);
    if (length == 0) {
      assertThat(shuffleOrder.getFirstIndex()).isEqualTo(INDEX_UNSET);
      assertThat(shuffleOrder.getLastIndex()).isEqualTo(INDEX_UNSET);
    } else {
      int[] indices = new int[length];
      indices[0] = shuffleOrder.getFirstIndex();
      assertThat(shuffleOrder.getPreviousIndex(indices[0])).isEqualTo(INDEX_UNSET);
      for (int i = 1; i < length; i++) {
        indices[i] = shuffleOrder.getNextIndex(indices[i - 1]);
        assertThat(shuffleOrder.getPreviousIndex(indices[i])).isEqualTo(indices[i - 1]);
        for (int j = 0; j < i; j++) {
          assertThat(indices[i] != indices[j]).isTrue();
        }
      }
      assertThat(shuffleOrder.getLastIndex()).isEqualTo(indices[length - 1]);
      assertThat(shuffleOrder.getNextIndex(indices[length - 1])).isEqualTo(INDEX_UNSET);
      for (int i = 0; i < length; i++) {
        assertThat(indices[i] >= 0).isTrue();
        assertThat(indices[i] < length).isTrue();
      }
    }
  }

  private static void testCloneAndInsert(ShuffleOrder shuffleOrder, int position, int count) {
    ShuffleOrder newOrder = shuffleOrder.cloneAndInsert(position, count);
    assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() + count);
    // Assert all elements still have the relative same order
    for (int i = 0; i < shuffleOrder.getLength(); i++) {
      int expectedNextIndex = shuffleOrder.getNextIndex(i);
      if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) {
        expectedNextIndex += count;
      }
      int newNextIndex = newOrder.getNextIndex(i < position ? i : i + count);
      while (newNextIndex >= position && newNextIndex < position + count) {
        newNextIndex = newOrder.getNextIndex(newNextIndex);
      }
      assertThat(newNextIndex).isEqualTo(expectedNextIndex);
    }
  }

  private static void testCloneAndRemove(
      ShuffleOrder shuffleOrder, int indexFrom, int indexToExclusive) {
    int numberOfElementsToRemove = indexToExclusive - indexFrom;
    ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(indexFrom, indexToExclusive);
    assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - numberOfElementsToRemove);
    // Assert all elements still have the relative same order
    for (int i = 0; i < shuffleOrder.getLength(); i++) {
      if (i >= indexFrom && i < indexToExclusive) {
        continue;
      }
      int expectedNextIndex = shuffleOrder.getNextIndex(i);
      while (expectedNextIndex >= indexFrom && expectedNextIndex < indexToExclusive) {
        expectedNextIndex = shuffleOrder.getNextIndex(expectedNextIndex);
      }
      if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= indexFrom) {
        expectedNextIndex -= numberOfElementsToRemove;
      }
      int newNextIndex = newOrder.getNextIndex(i < indexFrom ? i : i - numberOfElementsToRemove);
      assertThat(newNextIndex).isEqualTo(expectedNextIndex);
    }
  }
}
