/*
 *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package org.webrtc.audio;

import static android.media.AudioManager.MODE_IN_CALL;
import static android.media.AudioManager.MODE_IN_COMMUNICATION;
import static android.media.AudioManager.MODE_NORMAL;
import static android.media.AudioManager.MODE_RINGTONE;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.MediaRecorder.AudioSource;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import java.util.Arrays;
import org.webrtc.Logging;

/** Utilities for implementations of {@code AudioDeviceModule}, mostly for logging. */
public final class WebRtcAudioUtils {
  private static final String TAG = "WebRtcAudioUtilsExternal";

  /** Helper method for building a string of thread information. */
  public static String getThreadInfo() {
    Thread current = Thread.currentThread();
    return "@[name=" + current.getName() + ", id=" + current.getId() + "]";
  }

  /** Returns true if we're running on emulator. */
  public static boolean runningOnEmulator() {
    // Hardware type of qemu1 is goldfish and qemu2 is ranchu.
    return Build.HARDWARE.equals("goldfish") || Build.HARDWARE.equals("ranchu");
  }

  /** Information about the current build, taken from system properties. */
  private static void logDeviceInfo(String tag) {
    Logging.d(
        tag,
        ("Android SDK: " + Build.VERSION.SDK_INT)
            + (", Release: " + Build.VERSION.RELEASE)
            + (", Brand: " + Build.BRAND)
            + (", Device: " + Build.DEVICE)
            + (", Id: " + Build.ID)
            + (", Hardware: " + Build.HARDWARE)
            + (", Manufacturer: " + Build.MANUFACTURER)
            + (", Model: " + Build.MODEL)
            + (", Product: " + Build.PRODUCT));
  }

  /**
   * Logs information about the current audio state. The idea is to call this method when errors are
   * detected to log under what conditions the error occurred. Hopefully it will provide clues to
   * what might be the root cause.
   */
  public static void logAudioState(String tag, Context context, AudioManager audioManager) {
    logDeviceInfo(tag);
    logAudioStateBasic(tag, context, audioManager);
    logAudioStateVolume(tag, audioManager);
    logAudioDeviceInfo(tag, audioManager);
  }

  /** Converts AudioDeviceInfo types to local string representation. */
  public static String deviceTypeToString(int type) {
    switch (type) {
      case AudioDeviceInfo.TYPE_AUX_LINE:
        return "TYPE_AUX_LINE";
      case AudioDeviceInfo.TYPE_BLE_BROADCAST:
        return "TYPE_BLE_BROADCAST";
      case AudioDeviceInfo.TYPE_BLE_HEADSET:
        return "TYPE_BLE_HEADSET";
      case AudioDeviceInfo.TYPE_BLE_SPEAKER:
        return "TYPE_BLE_SPEAKER";
      case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
        return "TYPE_BLUETOOTH_A2DP";
      case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
        return "TYPE_BLUETOOTH_SCO";
      case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
        return "TYPE_BUILTIN_EARPIECE";
      case AudioDeviceInfo.TYPE_BUILTIN_MIC:
        return "TYPE_BUILTIN_MIC";
      case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
        return "TYPE_BUILTIN_SPEAKER";
      case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE:
        return "TYPE_BUILTIN_SPEAKER_SAFE";
      case AudioDeviceInfo.TYPE_BUS:
        return "TYPE_BUS";
      case AudioDeviceInfo.TYPE_DOCK:
        return "TYPE_DOCK";
      case AudioDeviceInfo.TYPE_DOCK_ANALOG:
        return "TYPE_DOCK_ANALOG";
      case AudioDeviceInfo.TYPE_FM:
        return "TYPE_FM";
      case AudioDeviceInfo.TYPE_FM_TUNER:
        return "TYPE_FM_TUNER";
      case AudioDeviceInfo.TYPE_HDMI:
        return "TYPE_HDMI";
      case AudioDeviceInfo.TYPE_HDMI_ARC:
        return "TYPE_HDMI_ARC";
      case AudioDeviceInfo.TYPE_HDMI_EARC:
        return "TYPE_HDMI_EARC";
      case AudioDeviceInfo.TYPE_HEARING_AID:
        return "TYPE_HEARING_AID";
      case AudioDeviceInfo.TYPE_IP:
        return "TYPE_IP";
      case AudioDeviceInfo.TYPE_LINE_ANALOG:
        return "TYPE_LINE_ANALOG";
      case AudioDeviceInfo.TYPE_LINE_DIGITAL:
        return "TYPE_LINE_DIGITAL";
      case AudioDeviceInfo.TYPE_REMOTE_SUBMIX:
        return "TYPE_REMOTE_SUBMIX";
      case AudioDeviceInfo.TYPE_TELEPHONY:
        return "TYPE_TELEPHONY";
      case AudioDeviceInfo.TYPE_TV_TUNER:
        return "TYPE_TV_TUNER";
      case AudioDeviceInfo.TYPE_UNKNOWN:
        return "TYPE_UNKNOWN";
      case AudioDeviceInfo.TYPE_USB_ACCESSORY:
        return "TYPE_USB_ACCESSORY";
      case AudioDeviceInfo.TYPE_USB_DEVICE:
        return "TYPE_USB_DEVICE";
      case AudioDeviceInfo.TYPE_USB_HEADSET:
        return "TYPE_USB_HEADSET";
      case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
        return "TYPE_WIRED_HEADPHONES";
      case AudioDeviceInfo.TYPE_WIRED_HEADSET:
        return "TYPE_WIRED_HEADSET";
      default:
        return "TYPE_UNKNOWN(" + type + ")";
    }
  }

  public static String audioSourceToString(int source) {
    switch (source) {
      case AudioSource.DEFAULT:
        return "DEFAULT";
      case AudioSource.MIC:
        return "MIC";
      case AudioSource.VOICE_UPLINK:
        return "VOICE_UPLINK";
      case AudioSource.VOICE_DOWNLINK:
        return "VOICE_DOWNLINK";
      case AudioSource.VOICE_CALL:
        return "VOICE_CALL";
      case AudioSource.CAMCORDER:
        return "CAMCORDER";
      case AudioSource.VOICE_RECOGNITION:
        return "VOICE_RECOGNITION";
      case AudioSource.VOICE_COMMUNICATION:
        return "VOICE_COMMUNICATION";
      case AudioSource.UNPROCESSED:
        return "UNPROCESSED";
      case AudioSource.VOICE_PERFORMANCE:
        return "VOICE_PERFORMANCE";
      default:
        return "INVALID";
    }
  }

  public static String channelMaskToString(int mask) {
    // For input or AudioRecord, the mask should be AudioFormat#CHANNEL_IN_MONO or
    // AudioFormat#CHANNEL_IN_STEREO. AudioFormat#CHANNEL_IN_MONO is guaranteed to work on all
    // devices.
    switch (mask) {
      case AudioFormat.CHANNEL_IN_STEREO:
        return "IN_STEREO";
      case AudioFormat.CHANNEL_IN_MONO:
        return "IN_MONO";
      default:
        return "INVALID";
    }
  }

  @TargetApi(VERSION_CODES.N)
  public static String audioEncodingToString(int enc) {
    switch (enc) {
      case AudioFormat.ENCODING_INVALID:
        return "INVALID";
      case AudioFormat.ENCODING_PCM_16BIT:
        return "PCM_16BIT";
      case AudioFormat.ENCODING_PCM_8BIT:
        return "PCM_8BIT";
      case AudioFormat.ENCODING_PCM_FLOAT:
        return "PCM_FLOAT";
      case AudioFormat.ENCODING_AC3:
        return "AC3";
      case AudioFormat.ENCODING_E_AC3:
        return "AC3";
      case AudioFormat.ENCODING_DTS:
        return "DTS";
      case AudioFormat.ENCODING_DTS_HD:
        return "DTS_HD";
      case AudioFormat.ENCODING_MP3:
        return "MP3";
      default:
        return "Invalid encoding: " + enc;
    }
  }

  /** Reports basic audio statistics. */
  private static void logAudioStateBasic(String tag, Context context, AudioManager audioManager) {
    Logging.d(
        tag,
        "Audio State: "
            + ("audio mode: " + modeToString(audioManager.getMode()))
            + (", has mic: " + hasMicrophone(context))
            + (", mic muted: " + audioManager.isMicrophoneMute())
            + (", music active: " + audioManager.isMusicActive())
            + (", speakerphone: " + audioManager.isSpeakerphoneOn())
            + (", BT SCO: " + audioManager.isBluetoothScoOn()));
  }

  /** Adds volume information for all possible stream types. */
  private static void logAudioStateVolume(String tag, AudioManager audioManager) {
    final int[] streams = {
      AudioManager.STREAM_VOICE_CALL,
      AudioManager.STREAM_MUSIC,
      AudioManager.STREAM_RING,
      AudioManager.STREAM_ALARM,
      AudioManager.STREAM_NOTIFICATION,
      AudioManager.STREAM_SYSTEM
    };
    Logging.d(tag, "Audio State: ");
    // Some devices may not have volume controls and might use a fixed volume.
    boolean fixedVolume = audioManager.isVolumeFixed();
    Logging.d(tag, "  fixed volume=" + fixedVolume);
    if (!fixedVolume) {
      for (int stream : streams) {
        StringBuilder info = new StringBuilder();
        info.append("  " + streamTypeToString(stream) + ": ");
        info.append("volume=").append(audioManager.getStreamVolume(stream));
        info.append(", max=").append(audioManager.getStreamMaxVolume(stream));
        if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
          info.append(", muted=").append(audioManager.isStreamMute(stream));
        }
        Logging.d(tag, info.toString());
      }
    }
  }

  // Moz linting complains even though AudioManager.GET_DEVICES_ALL is
  // listed in the docs here:
  // https://developer.android.com/reference/android/media/AudioManager#GET_DEVICES_ALL
  @SuppressLint("WrongConstant")
  private static void logAudioDeviceInfo(String tag, AudioManager audioManager) {
    if (Build.VERSION.SDK_INT < VERSION_CODES.M) {
      return;
    }
    final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
    if (devices.length == 0) {
      return;
    }
    Logging.d(tag, "Audio Devices: ");
    for (AudioDeviceInfo device : devices) {
      StringBuilder info = new StringBuilder();
      info.append("  ").append(deviceTypeToString(device.getType()));
      info.append(device.isSource() ? "(in): " : "(out): ");
      // An empty array indicates that the device supports arbitrary channel counts.
      if (device.getChannelCounts().length > 0) {
        info.append("channels=").append(Arrays.toString(device.getChannelCounts()));
        info.append(", ");
      }
      if (device.getEncodings().length > 0) {
        // Examples: ENCODING_PCM_16BIT = 2, ENCODING_PCM_FLOAT = 4.
        info.append("encodings=").append(Arrays.toString(device.getEncodings()));
        info.append(", ");
      }
      if (device.getSampleRates().length > 0) {
        info.append("sample rates=").append(Arrays.toString(device.getSampleRates()));
        info.append(", ");
      }
      info.append("id=").append(device.getId());
      Logging.d(tag, info.toString());
    }
  }

  /** Converts media.AudioManager modes into local string representation. */
  static String modeToString(int mode) {
    switch (mode) {
      case MODE_IN_CALL:
        return "MODE_IN_CALL";
      case MODE_IN_COMMUNICATION:
        return "MODE_IN_COMMUNICATION";
      case MODE_NORMAL:
        return "MODE_NORMAL";
      case MODE_RINGTONE:
        return "MODE_RINGTONE";
      default:
        return "MODE_INVALID";
    }
  }

  private static String streamTypeToString(int stream) {
    switch (stream) {
      case AudioManager.STREAM_VOICE_CALL:
        return "STREAM_VOICE_CALL";
      case AudioManager.STREAM_MUSIC:
        return "STREAM_MUSIC";
      case AudioManager.STREAM_RING:
        return "STREAM_RING";
      case AudioManager.STREAM_ALARM:
        return "STREAM_ALARM";
      case AudioManager.STREAM_NOTIFICATION:
        return "STREAM_NOTIFICATION";
      case AudioManager.STREAM_SYSTEM:
        return "STREAM_SYSTEM";
      default:
        return "STREAM_INVALID";
    }
  }

  /** Returns true if the device can record audio via a microphone. */
  private static boolean hasMicrophone(Context context) {
    return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MICROPHONE);
  }
}
