/*
 * Copyright 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 com.google.android.exoplayer2.audio;

import android.os.Bundle;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.Bundleable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;

/**
 * Attributes for audio playback, which configure the underlying platform {@link
 * android.media.AudioTrack}.
 *
 * <p>To set the audio attributes, create an instance using the {@link Builder} and either pass it
 * to the player or send a message of type {@code Renderer#MSG_SET_AUDIO_ATTRIBUTES} to the audio
 * renderers.
 *
 * <p>This class is based on {@link android.media.AudioAttributes}, but can be used on all supported
 * API versions.
 */
public final class AudioAttributes implements Bundleable {

  public static final AudioAttributes DEFAULT = new Builder().build();

  /** Builder for {@link AudioAttributes}. */
  public static final class Builder {

    private @C.AudioContentType int contentType;
    private @C.AudioFlags int flags;
    private @C.AudioUsage int usage;
    private @C.AudioAllowedCapturePolicy int allowedCapturePolicy;
    private @C.SpatializationBehavior int spatializationBehavior;

    /**
     * Creates a new builder for {@link AudioAttributes}.
     *
     * <p>By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is {@link
     * C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are set.
     */
    public Builder() {
      contentType = C.CONTENT_TYPE_UNKNOWN;
      flags = 0;
      usage = C.USAGE_MEDIA;
      allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL;
      spatializationBehavior = C.SPATIALIZATION_BEHAVIOR_AUTO;
    }

    /** @see android.media.AudioAttributes.Builder#setContentType(int) */
    public Builder setContentType(@C.AudioContentType int contentType) {
      this.contentType = contentType;
      return this;
    }

    /** @see android.media.AudioAttributes.Builder#setFlags(int) */
    public Builder setFlags(@C.AudioFlags int flags) {
      this.flags = flags;
      return this;
    }

    /** @see android.media.AudioAttributes.Builder#setUsage(int) */
    public Builder setUsage(@C.AudioUsage int usage) {
      this.usage = usage;
      return this;
    }

    /** See {@link android.media.AudioAttributes.Builder#setAllowedCapturePolicy(int)}. */
    public Builder setAllowedCapturePolicy(@C.AudioAllowedCapturePolicy int allowedCapturePolicy) {
      this.allowedCapturePolicy = allowedCapturePolicy;
      return this;
    }

    // TODO[b/190759307] Update javadoc to link to AudioAttributes.Builder#setSpatializationBehavior
    //  once compile SDK target is set to 32.
    /** See AudioAttributes.Builder#setSpatializationBehavior(int). */
    public Builder setSpatializationBehavior(@C.SpatializationBehavior int spatializationBehavior) {
      this.spatializationBehavior = spatializationBehavior;
      return this;
    }

    /** Creates an {@link AudioAttributes} instance from this builder. */
    public AudioAttributes build() {
      return new AudioAttributes(
          contentType, flags, usage, allowedCapturePolicy, spatializationBehavior);
    }
  }

  public final @C.AudioContentType int contentType;
  public final @C.AudioFlags int flags;
  public final @C.AudioUsage int usage;
  public final @C.AudioAllowedCapturePolicy int allowedCapturePolicy;
  public final @C.SpatializationBehavior int spatializationBehavior;

  @Nullable private android.media.AudioAttributes audioAttributesV21;

  private AudioAttributes(
      @C.AudioContentType int contentType,
      @C.AudioFlags int flags,
      @C.AudioUsage int usage,
      @C.AudioAllowedCapturePolicy int allowedCapturePolicy,
      @C.SpatializationBehavior int spatializationBehavior) {
    this.contentType = contentType;
    this.flags = flags;
    this.usage = usage;
    this.allowedCapturePolicy = allowedCapturePolicy;
    this.spatializationBehavior = spatializationBehavior;
  }

  /**
   * Returns a {@link android.media.AudioAttributes} from this instance.
   *
   * <p>Field {@link AudioAttributes#allowedCapturePolicy} is ignored for API levels prior to 29.
   */
  @RequiresApi(21)
  public android.media.AudioAttributes getAudioAttributesV21() {
    if (audioAttributesV21 == null) {
      android.media.AudioAttributes.Builder builder =
          new android.media.AudioAttributes.Builder()
              .setContentType(contentType)
              .setFlags(flags)
              .setUsage(usage);
      if (Util.SDK_INT >= 29) {
        Api29.setAllowedCapturePolicy(builder, allowedCapturePolicy);
      }
      if (Util.SDK_INT >= 32) {
        Api32.setSpatializationBehavior(builder, spatializationBehavior);
      }
      audioAttributesV21 = builder.build();
    }
    return audioAttributesV21;
  }

  @Override
  public boolean equals(@Nullable Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null || getClass() != obj.getClass()) {
      return false;
    }
    AudioAttributes other = (AudioAttributes) obj;
    return this.contentType == other.contentType
        && this.flags == other.flags
        && this.usage == other.usage
        && this.allowedCapturePolicy == other.allowedCapturePolicy
        && this.spatializationBehavior == other.spatializationBehavior;
  }

  @Override
  public int hashCode() {
    int result = 17;
    result = 31 * result + contentType;
    result = 31 * result + flags;
    result = 31 * result + usage;
    result = 31 * result + allowedCapturePolicy;
    result = 31 * result + spatializationBehavior;
    return result;
  }

  // Bundleable implementation.

  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @IntDef({
    FIELD_CONTENT_TYPE,
    FIELD_FLAGS,
    FIELD_USAGE,
    FIELD_ALLOWED_CAPTURE_POLICY,
    FIELD_SPATIALIZATION_BEHAVIOR
  })
  private @interface FieldNumber {}

  private static final int FIELD_CONTENT_TYPE = 0;
  private static final int FIELD_FLAGS = 1;
  private static final int FIELD_USAGE = 2;
  private static final int FIELD_ALLOWED_CAPTURE_POLICY = 3;
  private static final int FIELD_SPATIALIZATION_BEHAVIOR = 4;

  @Override
  public Bundle toBundle() {
    Bundle bundle = new Bundle();
    bundle.putInt(keyForField(FIELD_CONTENT_TYPE), contentType);
    bundle.putInt(keyForField(FIELD_FLAGS), flags);
    bundle.putInt(keyForField(FIELD_USAGE), usage);
    bundle.putInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY), allowedCapturePolicy);
    bundle.putInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR), spatializationBehavior);
    return bundle;
  }

  /** Object that can restore {@link AudioAttributes} from a {@link Bundle}. */
  public static final Creator<AudioAttributes> CREATOR =
      bundle -> {
        Builder builder = new Builder();
        if (bundle.containsKey(keyForField(FIELD_CONTENT_TYPE))) {
          builder.setContentType(bundle.getInt(keyForField(FIELD_CONTENT_TYPE)));
        }
        if (bundle.containsKey(keyForField(FIELD_FLAGS))) {
          builder.setFlags(bundle.getInt(keyForField(FIELD_FLAGS)));
        }
        if (bundle.containsKey(keyForField(FIELD_USAGE))) {
          builder.setUsage(bundle.getInt(keyForField(FIELD_USAGE)));
        }
        if (bundle.containsKey(keyForField(FIELD_ALLOWED_CAPTURE_POLICY))) {
          builder.setAllowedCapturePolicy(bundle.getInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY)));
        }
        if (bundle.containsKey(keyForField(FIELD_SPATIALIZATION_BEHAVIOR))) {
          builder.setSpatializationBehavior(
              bundle.getInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR)));
        }
        return builder.build();
      };

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

  @RequiresApi(29)
  private static final class Api29 {
    private Api29() {}

    @DoNotInline
    public static void setAllowedCapturePolicy(
        android.media.AudioAttributes.Builder builder,
        @C.AudioAllowedCapturePolicy int allowedCapturePolicy) {
      builder.setAllowedCapturePolicy(allowedCapturePolicy);
    }
  }

  @RequiresApi(32)
  private static final class Api32 {
    private Api32() {}

    @DoNotInline
    public static void setSpatializationBehavior(
        android.media.AudioAttributes.Builder builder,
        @C.SpatializationBehavior int spatializationBehavior) {
      try {
        // TODO[b/190759307]: Remove reflection once compile SDK target is set to 32.
        Method setSpatializationBehavior =
            builder.getClass().getMethod("setSpatializationBehavior", Integer.TYPE);
        setSpatializationBehavior.invoke(builder, spatializationBehavior);
      } catch (Exception e) {
        // Do nothing if reflection fails.
      }
    }
  }
}
