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

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

import android.os.Bundle;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable;
import androidx.media3.common.Rating;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaLibraryService.LibraryParams;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * A command that a {@link MediaController} can send to a {@link MediaSession}.
 *
 * <p>If {@link #commandCode} isn't {@link #COMMAND_CODE_CUSTOM}), it's a predefined command. If
 * {@link #commandCode} is {@link #COMMAND_CODE_CUSTOM}), it's a custom command and {@link
 * #customAction} must not be {@code null}.
 */
public final class SessionCommand implements Bundleable {

  /** Command codes of session commands. */
  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @IntDef({
    COMMAND_CODE_CUSTOM,
    COMMAND_CODE_SESSION_SET_RATING,
    COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT,
    COMMAND_CODE_LIBRARY_SUBSCRIBE,
    COMMAND_CODE_LIBRARY_UNSUBSCRIBE,
    COMMAND_CODE_LIBRARY_GET_CHILDREN,
    COMMAND_CODE_LIBRARY_GET_ITEM,
    COMMAND_CODE_LIBRARY_SEARCH,
    COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT
  })
  public @interface CommandCode {}

  /**
   * Command code for the custom command which can be defined by string action in the {@link
   * SessionCommand}.
   */
  public static final int COMMAND_CODE_CUSTOM = 0;

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Session commands (i.e. commands to {@link MediaSession#SessionCallback})
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Command code for {@link MediaController#setRating(String, Rating)}. */
  public static final int COMMAND_CODE_SESSION_SET_RATING = 40010;

  /* package */ static final ImmutableList<Integer> SESSION_COMMANDS =
      ImmutableList.of(COMMAND_CODE_SESSION_SET_RATING);

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Library commands (i.e. commands to {@link MediaLibrarySession#MediaLibrarySessionCallback})
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Command code for {@link MediaBrowser#getLibraryRoot(LibraryParams)}. */
  public static final int COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT = 50000;

  /** Command code for {@link MediaBrowser#subscribe(String, LibraryParams)}. */
  public static final int COMMAND_CODE_LIBRARY_SUBSCRIBE = 50001;

  /** Command code for {@link MediaBrowser#unsubscribe(String)}. */
  public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 50002;

  /** Command code for {@link MediaBrowser#getChildren(String, int, int, LibraryParams)}. */
  public static final int COMMAND_CODE_LIBRARY_GET_CHILDREN = 50003;

  /** Command code for {@link MediaBrowser#getItem(String)}. */
  public static final int COMMAND_CODE_LIBRARY_GET_ITEM = 50004;

  /** Command code for {@link MediaBrowser#search(String, LibraryParams)}. */
  public static final int COMMAND_CODE_LIBRARY_SEARCH = 50005;

  /** Command code for {@link MediaBrowser#getSearchResult(String, int, int, LibraryParams)}. */
  public static final int COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT = 50006;

  /* package */ static final ImmutableList<Integer> LIBRARY_COMMANDS =
      ImmutableList.of(
          COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT,
          COMMAND_CODE_LIBRARY_SUBSCRIBE,
          COMMAND_CODE_LIBRARY_UNSUBSCRIBE,
          COMMAND_CODE_LIBRARY_GET_CHILDREN,
          COMMAND_CODE_LIBRARY_GET_ITEM,
          COMMAND_CODE_LIBRARY_SEARCH,
          COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT);

  /**
   * The command code of a predefined command. It will be {@link #COMMAND_CODE_CUSTOM} for a custom
   * command.
   */
  public final @CommandCode int commandCode;

  /** The action of a custom command. It will be an empty string for a predefined command. */
  public final String customAction;

  /**
   * The extra bundle of a custom command. It will be {@link Bundle#EMPTY} for a predefined command.
   */
  public final Bundle customExtras;

  /**
   * Creates a predefined command.
   *
   * @param commandCode A command code for a predefined command.
   */
  public SessionCommand(@CommandCode int commandCode) {
    checkArgument(
        commandCode != COMMAND_CODE_CUSTOM, "commandCode shouldn't be COMMAND_CODE_CUSTOM");
    this.commandCode = commandCode;
    customAction = "";
    customExtras = Bundle.EMPTY;
  }

  /**
   * Creates a custom command.
   *
   * @param action The action of this custom command.
   * @param extras An extra bundle for this custom command.
   */
  public SessionCommand(String action, Bundle extras) {
    commandCode = COMMAND_CODE_CUSTOM;
    customAction = checkNotNull(action);
    customExtras = new Bundle(checkNotNull(extras));
  }

  @Override
  public boolean equals(@Nullable Object obj) {
    if (!(obj instanceof SessionCommand)) {
      return false;
    }
    SessionCommand other = (SessionCommand) obj;
    return commandCode == other.commandCode && TextUtils.equals(customAction, other.customAction);
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(customAction, commandCode);
  }

  // Bundleable implementation.

  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @IntDef({FIELD_COMMAND_CODE, FIELD_CUSTOM_ACTION, FIELD_CUSTOM_EXTRAS})
  private @interface FieldNumber {}

  private static final int FIELD_COMMAND_CODE = 0;
  private static final int FIELD_CUSTOM_ACTION = 1;
  private static final int FIELD_CUSTOM_EXTRAS = 2;

  @UnstableApi
  @Override
  public Bundle toBundle() {
    Bundle bundle = new Bundle();
    bundle.putInt(keyForField(FIELD_COMMAND_CODE), commandCode);
    bundle.putString(keyForField(FIELD_CUSTOM_ACTION), customAction);
    bundle.putBundle(keyForField(FIELD_CUSTOM_EXTRAS), customExtras);
    return bundle;
  }

  /** Object that can restore a {@link SessionCommand} from a {@link Bundle}. */
  @UnstableApi
  public static final Creator<SessionCommand> CREATOR =
      bundle -> {
        int commandCode =
            bundle.getInt(keyForField(FIELD_COMMAND_CODE), /* defaultValue= */ COMMAND_CODE_CUSTOM);
        if (commandCode != COMMAND_CODE_CUSTOM) {
          return new SessionCommand(commandCode);
        } else {
          String customAction = checkNotNull(bundle.getString(keyForField(FIELD_CUSTOM_ACTION)));
          @Nullable Bundle customExtras = bundle.getBundle(keyForField(FIELD_CUSTOM_EXTRAS));
          return new SessionCommand(
              customAction, customExtras == null ? Bundle.EMPTY : customExtras);
        }
      };

  private static String keyForField(@FieldNumber int field) {
    return Integer.toString(field, Character.MAX_RADIX);
  }
}
