/*
 * 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 java.lang.annotation.ElementType.TYPE_USE;

import android.os.Bundle;
import android.os.SystemClock;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable;
import androidx.media3.common.util.UnstableApi;
import com.google.common.util.concurrent.ListenableFuture;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * A result to be used with {@link ListenableFuture} for asynchronous calls between {@link
 * MediaSession} and {@link MediaController}.
 */
public final class SessionResult implements Bundleable {

  /**
   * Result codes.
   *
   * <ul>
   *   <li>Error code: Negative integer
   *   <li>Success code: 0
   *   <li>Info code: Positive integer
   * </ul>
   *
   * <ul>
   *   <li>{@code 0 < |code| < 100} : Reserved for Player specific code.
   *   <li>{@code 100 <= |code| < 500} : Session/Controller specific code.
   *   <li>{@code 500 <= |code| < 1000} : Browser/Library session specific code.
   *   <li>{@code 1000 <= |code|} : Reserved for Player custom code.
   * </ul>
   */
  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @IntDef({
    RESULT_SUCCESS,
    RESULT_ERROR_UNKNOWN,
    RESULT_ERROR_INVALID_STATE,
    RESULT_ERROR_BAD_VALUE,
    RESULT_ERROR_PERMISSION_DENIED,
    RESULT_ERROR_IO,
    RESULT_INFO_SKIPPED,
    RESULT_ERROR_SESSION_DISCONNECTED,
    RESULT_ERROR_NOT_SUPPORTED,
    RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED,
    RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED,
    RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT,
    RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED,
    RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION,
    RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED,
    RESULT_ERROR_SESSION_SETUP_REQUIRED
  })
  public @interface Code {}

  /**
   * Result code representing that the command is successfully completed.
   *
   * <p>Interoperability: This code is also used to tell that the command was successfully sent, but
   * the result is unknown when connected with {@link MediaSessionCompat} or {@link
   * MediaControllerCompat}.
   */
  public static final int RESULT_SUCCESS = 0;

  /** Result code representing that the command is ended with an unknown error. */
  public static final int RESULT_ERROR_UNKNOWN = -1;

  /**
   * Result code representing that the command cannot be completed because the current state is not
   * valid for the command.
   */
  public static final int RESULT_ERROR_INVALID_STATE = -2;

  /** Result code representing that an argument is illegal. */
  public static final int RESULT_ERROR_BAD_VALUE = -3;

  /** Result code representing that the command is not allowed. */
  public static final int RESULT_ERROR_PERMISSION_DENIED = -4;

  /** Result code representing that a file or network related error happened. */
  public static final int RESULT_ERROR_IO = -5;

  /** Result code representing that the command is not supported. */
  public static final int RESULT_ERROR_NOT_SUPPORTED = -6;

  /** Result code representing that the command is skipped. */
  public static final int RESULT_INFO_SKIPPED = 1;

  /** Result code representing that the session and controller were disconnected. */
  public static final int RESULT_ERROR_SESSION_DISCONNECTED = -100;

  /** Result code representing that the authentication has expired. */
  public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED = -102;

  /** Result code representing that a premium account is required. */
  public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = -103;

  /** Result code representing that too many concurrent streams are detected. */
  public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT = -104;

  /** Result code representing that the content is blocked due to parental controls. */
  public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = -105;

  /** Result code representing that the content is blocked due to being regionally unavailable. */
  public static final int RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION = -106;

  /**
   * Result code representing that the application cannot skip any more because the skip limit is
   * reached.
   */
  public static final int RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED = -107;

  /** Result code representing that the session needs user's manual intervention. */
  public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED = -108;

  /** The {@link Code} of this result. */
  public final @Code int resultCode;

  /** The extra {@link Bundle} for the result. */
  public final Bundle extras;

  /**
   * The completion time of the command. It's the same as {@link SystemClock#elapsedRealtime()} when
   * the command is completed.
   */
  public final long completionTimeMs;

  /**
   * Creates an instance with a result code.
   *
   * @param resultCode The result code.
   */
  public SessionResult(@Code int resultCode) {
    this(resultCode, /* extras= */ Bundle.EMPTY);
  }

  /**
   * Creates an instance with a result code and an extra {@link Bundle}.
   *
   * @param resultCode The result code.
   * @param extras The extra {@link Bundle}.
   */
  public SessionResult(@Code int resultCode, Bundle extras) {
    this(resultCode, extras, SystemClock.elapsedRealtime());
  }

  private SessionResult(@Code int resultCode, Bundle extras, long completionTimeMs) {
    this.resultCode = resultCode;
    this.extras = new Bundle(extras);
    this.completionTimeMs = completionTimeMs;
  }

  // Bundleable implementation.

  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @IntDef({FIELD_RESULT_CODE, FIELD_EXTRAS, FIELD_COMPLETION_TIME_MS})
  private @interface FieldNumber {}

  private static final int FIELD_RESULT_CODE = 0;
  private static final int FIELD_EXTRAS = 1;
  private static final int FIELD_COMPLETION_TIME_MS = 2;

  @UnstableApi
  @Override
  public Bundle toBundle() {
    Bundle bundle = new Bundle();
    bundle.putInt(keyForField(FIELD_RESULT_CODE), resultCode);
    bundle.putBundle(keyForField(FIELD_EXTRAS), extras);
    bundle.putLong(keyForField(FIELD_COMPLETION_TIME_MS), completionTimeMs);
    return bundle;
  }

  /** Object that can restore a {@link SessionResult} from a {@link Bundle}. */
  @UnstableApi public static final Creator<SessionResult> CREATOR = SessionResult::fromBundle;

  private static SessionResult fromBundle(Bundle bundle) {
    int resultCode =
        bundle.getInt(keyForField(FIELD_RESULT_CODE), /* defaultValue= */ RESULT_ERROR_UNKNOWN);
    @Nullable Bundle extras = bundle.getBundle(keyForField(FIELD_EXTRAS));
    long completionTimeMs =
        bundle.getLong(
            keyForField(FIELD_COMPLETION_TIME_MS),
            /* defaultValue= */ SystemClock.elapsedRealtime());
    return new SessionResult(resultCode, extras == null ? Bundle.EMPTY : extras, completionTimeMs);
  }

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