/*
 * Copyright (C) 2016 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.mediacodec;

import android.annotation.TargetApi;
import android.graphics.Point;
import android.media.MediaCodec;
import android.media.MediaCodecInfo.AudioCapabilities;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecInfo.VideoCapabilities;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;

/**
 * Information about a {@link MediaCodec} for a given mime type.
 */
@TargetApi(16)
public final class MediaCodecInfo {

  public static final String TAG = "MediaCodecInfo";

  /**
   * The value returned by {@link #getMaxSupportedInstances()} if the upper bound on the maximum
   * number of supported instances is unknown.
   */
  public static final int MAX_SUPPORTED_INSTANCES_UNKNOWN = -1;

  /**
   * The name of the decoder.
   * <p>
   * May be passed to {@link MediaCodec#createByCodecName(String)} to create an instance of the
   * decoder.
   */
  public final String name;

  /** The MIME type handled by the codec, or {@code null} if this is a passthrough codec. */
  public final @Nullable String mimeType;

  /**
   * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if this
   * is a passthrough codec.
   */
  public final @Nullable CodecCapabilities capabilities;

  /**
   * Whether the decoder supports seamless resolution switches.
   *
   * @see CodecCapabilities#isFeatureSupported(String)
   * @see CodecCapabilities#FEATURE_AdaptivePlayback
   */
  public final boolean adaptive;

  /**
   * Whether the decoder supports tunneling.
   *
   * @see CodecCapabilities#isFeatureSupported(String)
   * @see CodecCapabilities#FEATURE_TunneledPlayback
   */
  public final boolean tunneling;

  /**
   * Whether the decoder is secure.
   *
   * @see CodecCapabilities#isFeatureSupported(String)
   * @see CodecCapabilities#FEATURE_SecurePlayback
   */
  public final boolean secure;

  /** Whether this instance describes a passthrough codec. */
  public final boolean passthrough;

  /**
   * Creates an instance representing an audio passthrough decoder.
   *
   * @param name The name of the {@link MediaCodec}.
   * @return The created instance.
   */
  public static MediaCodecInfo newPassthroughInstance(String name) {
    return new MediaCodecInfo(
        name,
        /* mimeType= */ null,
        /* capabilities= */ null,
        /* passthrough= */ true,
        /* forceDisableAdaptive= */ false,
        /* forceSecure= */ false);
  }

  /**
   * Creates an instance.
   *
   * @param name The name of the {@link MediaCodec}.
   * @param mimeType A mime type supported by the {@link MediaCodec}.
   * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type.
   * @return The created instance.
   */
  public static MediaCodecInfo newInstance(String name, String mimeType,
      CodecCapabilities capabilities) {
    return new MediaCodecInfo(
        name,
        mimeType,
        capabilities,
        /* passthrough= */ false,
        /* forceDisableAdaptive= */ false,
        /* forceSecure= */ false);
  }

  /**
   * Creates an instance.
   *
   * @param name The name of the {@link MediaCodec}.
   * @param mimeType A mime type supported by the {@link MediaCodec}.
   * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type.
   * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}.
   * @param forceSecure Whether {@link #secure} should be forced to {@code true}.
   * @return The created instance.
   */
  public static MediaCodecInfo newInstance(
      String name,
      String mimeType,
      CodecCapabilities capabilities,
      boolean forceDisableAdaptive,
      boolean forceSecure) {
    return new MediaCodecInfo(
        name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure);
  }

  private MediaCodecInfo(
      String name,
      @Nullable String mimeType,
      @Nullable CodecCapabilities capabilities,
      boolean passthrough,
      boolean forceDisableAdaptive,
      boolean forceSecure) {
    this.name = Assertions.checkNotNull(name);
    this.mimeType = mimeType;
    this.capabilities = capabilities;
    this.passthrough = passthrough;
    adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities);
    tunneling = capabilities != null && isTunneling(capabilities);
    secure = forceSecure || (capabilities != null && isSecure(capabilities));
  }

  @Override
  public String toString() {
    return name;
  }

  /**
   * The profile levels supported by the decoder.
   *
   * @return The profile levels supported by the decoder.
   */
  public CodecProfileLevel[] getProfileLevels() {
    return capabilities == null || capabilities.profileLevels == null ? new CodecProfileLevel[0]
        : capabilities.profileLevels;
  }

  /**
   * Returns an upper bound on the maximum number of supported instances, or {@link
   * #MAX_SUPPORTED_INSTANCES_UNKNOWN} if unknown. Applications should not expect to operate more
   * instances than the returned maximum.
   *
   * @see CodecCapabilities#getMaxSupportedInstances()
   */
  public int getMaxSupportedInstances() {
    return (Util.SDK_INT < 23 || capabilities == null)
        ? MAX_SUPPORTED_INSTANCES_UNKNOWN
        : getMaxSupportedInstancesV23(capabilities);
  }

  /**
   * Whether the decoder supports the given {@code codec}. If there is insufficient information to
   * decide, returns true.
   *
   * @param codec Codec string as defined in RFC 6381.
   * @return True if the given codec is supported by the decoder.
   */
  public boolean isCodecSupported(String codec) {
    if (codec == null || mimeType == null) {
      return true;
    }
    String codecMimeType = MimeTypes.getMediaMimeType(codec);
    if (codecMimeType == null) {
      return true;
    }
    if (!mimeType.equals(codecMimeType)) {
      logNoSupport("codec.mime " + codec + ", " + codecMimeType);
      return false;
    }
    Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codec);
    if (codecProfileAndLevel == null) {
      // If we don't know any better, we assume that the profile and level are supported.
      return true;
    }
    for (CodecProfileLevel capabilities : getProfileLevels()) {
      if (capabilities.profile == codecProfileAndLevel.first
          && capabilities.level >= codecProfileAndLevel.second) {
        return true;
      }
    }
    logNoSupport("codec.profileLevel, " + codec + ", " + codecMimeType);
    return false;
  }

  /**
   * Whether the decoder supports video with a given width, height and frame rate.
   * <p>
   * Must not be called if the device SDK version is less than 21.
   *
   * @param width Width in pixels.
   * @param height Height in pixels.
   * @param frameRate Optional frame rate in frames per second. Ignored if set to
   *     {@link Format#NO_VALUE} or any value less than or equal to 0.
   * @return Whether the decoder supports video with the given width, height and frame rate.
   */
  @TargetApi(21)
  public boolean isVideoSizeAndRateSupportedV21(int width, int height, double frameRate) {
    if (capabilities == null) {
      logNoSupport("sizeAndRate.caps");
      return false;
    }
    VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
    if (videoCapabilities == null) {
      logNoSupport("sizeAndRate.vCaps");
      return false;
    }
    if (!areSizeAndRateSupportedV21(videoCapabilities, width, height, frameRate)) {
      // Capabilities are known to be inaccurately reported for vertical resolutions on some devices
      // (b/31387661). If the video is vertical and the capabilities indicate support if the width
      // and height are swapped, we assume that the vertical resolution is also supported.
      if (width >= height
          || !areSizeAndRateSupportedV21(videoCapabilities, height, width, frameRate)) {
        logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate);
        return false;
      }
      logAssumedSupport("sizeAndRate.rotated, " + width + "x" + height + "x" + frameRate);
    }
    return true;
  }

  /**
   * Returns the smallest video size greater than or equal to a specified size that also satisfies
   * the {@link MediaCodec}'s width and height alignment requirements.
   * <p>
   * Must not be called if the device SDK version is less than 21.
   *
   * @param width Width in pixels.
   * @param height Height in pixels.
   * @return The smallest video size greater than or equal to the specified size that also satisfies
   *     the {@link MediaCodec}'s width and height alignment requirements, or null if not a video
   *     codec.
   */
  @TargetApi(21)
  public Point alignVideoSizeV21(int width, int height) {
    if (capabilities == null) {
      logNoSupport("align.caps");
      return null;
    }
    VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
    if (videoCapabilities == null) {
      logNoSupport("align.vCaps");
      return null;
    }
    int widthAlignment = videoCapabilities.getWidthAlignment();
    int heightAlignment = videoCapabilities.getHeightAlignment();
    return new Point(Util.ceilDivide(width, widthAlignment) * widthAlignment,
        Util.ceilDivide(height, heightAlignment) * heightAlignment);
  }

  /**
   * Whether the decoder supports audio with a given sample rate.
   * <p>
   * Must not be called if the device SDK version is less than 21.
   *
   * @param sampleRate The sample rate in Hz.
   * @return Whether the decoder supports audio with the given sample rate.
   */
  @TargetApi(21)
  public boolean isAudioSampleRateSupportedV21(int sampleRate) {
    if (capabilities == null) {
      logNoSupport("sampleRate.caps");
      return false;
    }
    AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities();
    if (audioCapabilities == null) {
      logNoSupport("sampleRate.aCaps");
      return false;
    }
    if (!audioCapabilities.isSampleRateSupported(sampleRate)) {
      logNoSupport("sampleRate.support, " + sampleRate);
      return false;
    }
    return true;
  }

  /**
   * Whether the decoder supports audio with a given channel count.
   * <p>
   * Must not be called if the device SDK version is less than 21.
   *
   * @param channelCount The channel count.
   * @return Whether the decoder supports audio with the given channel count.
   */
  @TargetApi(21)
  public boolean isAudioChannelCountSupportedV21(int channelCount) {
    if (capabilities == null) {
      logNoSupport("channelCount.caps");
      return false;
    }
    AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities();
    if (audioCapabilities == null) {
      logNoSupport("channelCount.aCaps");
      return false;
    }
    int maxInputChannelCount = adjustMaxInputChannelCount(name, mimeType,
        audioCapabilities.getMaxInputChannelCount());
    if (maxInputChannelCount < channelCount) {
      logNoSupport("channelCount.support, " + channelCount);
      return false;
    }
    return true;
  }

  private void logNoSupport(String message) {
    Log.d(TAG, "NoSupport [" + message + "] [" + name + ", " + mimeType + "] ["
        + Util.DEVICE_DEBUG_INFO + "]");
  }

  private void logAssumedSupport(String message) {
    Log.d(TAG, "AssumedSupport [" + message + "] [" + name + ", " + mimeType + "] ["
        + Util.DEVICE_DEBUG_INFO + "]");
  }

  private static int adjustMaxInputChannelCount(String name, String mimeType, int maxChannelCount) {
    if (maxChannelCount > 1 || (Util.SDK_INT >= 26 && maxChannelCount > 0)) {
      // The maximum channel count looks like it's been set correctly.
      return maxChannelCount;
    }
    if (MimeTypes.AUDIO_MPEG.equals(mimeType)
        || MimeTypes.AUDIO_AMR_NB.equals(mimeType)
        || MimeTypes.AUDIO_AMR_WB.equals(mimeType)
        || MimeTypes.AUDIO_AAC.equals(mimeType)
        || MimeTypes.AUDIO_VORBIS.equals(mimeType)
        || MimeTypes.AUDIO_OPUS.equals(mimeType)
        || MimeTypes.AUDIO_RAW.equals(mimeType)
        || MimeTypes.AUDIO_FLAC.equals(mimeType)
        || MimeTypes.AUDIO_ALAW.equals(mimeType)
        || MimeTypes.AUDIO_MLAW.equals(mimeType)
        || MimeTypes.AUDIO_MSGSM.equals(mimeType)) {
      // Platform code should have set a default.
      return maxChannelCount;
    }
    // The maximum channel count looks incorrect. Adjust it to an assumed default.
    int assumedMaxChannelCount;
    if (MimeTypes.AUDIO_AC3.equals(mimeType)) {
      assumedMaxChannelCount = 6;
    } else if (MimeTypes.AUDIO_E_AC3.equals(mimeType)) {
      assumedMaxChannelCount = 16;
    } else {
      // Default to the platform limit, which is 30.
      assumedMaxChannelCount = 30;
    }
    Log.w(TAG, "AssumedMaxChannelAdjustment: " + name + ", [" + maxChannelCount + " to "
        + assumedMaxChannelCount + "]");
    return assumedMaxChannelCount;
  }

  private static boolean isAdaptive(CodecCapabilities capabilities) {
    return Util.SDK_INT >= 19 && isAdaptiveV19(capabilities);
  }

  @TargetApi(19)
  private static boolean isAdaptiveV19(CodecCapabilities capabilities) {
    return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
  }

  private static boolean isTunneling(CodecCapabilities capabilities) {
    return Util.SDK_INT >= 21 && isTunnelingV21(capabilities);
  }

  @TargetApi(21)
  private static boolean isTunnelingV21(CodecCapabilities capabilities) {
    return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback);
  }

  private static boolean isSecure(CodecCapabilities capabilities) {
    return Util.SDK_INT >= 21 && isSecureV21(capabilities);
  }

  @TargetApi(21)
  private static boolean isSecureV21(CodecCapabilities capabilities) {
    return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback);
  }

  @TargetApi(21)
  private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width,
      int height, double frameRate) {
    return frameRate == Format.NO_VALUE || frameRate <= 0
        ? capabilities.isSizeSupported(width, height)
        : capabilities.areSizeAndRateSupported(width, height, frameRate);
  }

  @TargetApi(23)
  private static int getMaxSupportedInstancesV23(CodecCapabilities capabilities) {
    return capabilities.getMaxSupportedInstances();
  }
}