// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/audio/alsa/alsa_util.h"

#include <stddef.h>

#include <functional>
#include <memory>

#include "base/logging.h"
#include "base/time/time.h"
#include "media/audio/alsa/alsa_wrapper.h"

namespace alsa_util {

namespace {

// Set hardware parameters of PCM. It does the same thing as the corresponding
// part in snd_pcm_set_params() (https://www.alsa-project.org, source code:
// https://github.com/tiwai/alsa-lib/blob/master/src/pcm/pcm.c#L8459), except
// that it configures buffer size and period size both to closest available
// values instead of forcing the buffer size be 4 times of the period size.
int ConfigureHwParams(media::AlsaWrapper* wrapper,
                      snd_pcm_t* handle,
                      snd_pcm_format_t format,
                      snd_pcm_access_t access,
                      unsigned int channels,
                      unsigned int sample_rate,
                      int soft_resample,
                      snd_pcm_uframes_t frames_per_buffer,
                      snd_pcm_uframes_t frames_per_period) {
  int error = 0;

  snd_pcm_hw_params_t* hw_params = nullptr;
  error = wrapper->PcmHwParamsMalloc(&hw_params);
  if (error < 0) {
    LOG(ERROR) << "PcmHwParamsMalloc: " << wrapper->StrError(error);
    return error;
  }
  // |snd_pcm_hw_params_t| is not exposed and requires memory allocation through
  // ALSA API. Therefore, use a smart pointer to pointer to insure freeing
  // memory when the function returns.
  std::unique_ptr<snd_pcm_hw_params_t*,
                  std::function<void(snd_pcm_hw_params_t**)>>
      params_holder(&hw_params, [wrapper](snd_pcm_hw_params_t** params) {
        wrapper->PcmHwParamsFree(*params);
      });

  error = wrapper->PcmHwParamsAny(handle, hw_params);
  if (error < 0) {
    LOG(ERROR) << "PcmHwParamsAny: " << wrapper->StrError(error);
    return error;
  }

  error = wrapper->PcmHwParamsSetRateResample(handle, hw_params, soft_resample);
  if (error < 0) {
    LOG(ERROR) << "PcmHwParamsSetRateResample: " << wrapper->StrError(error);
    return error;
  }

  error = wrapper->PcmHwParamsSetAccess(handle, hw_params, access);
  if (error < 0) {
    LOG(ERROR) << "PcmHwParamsSetAccess: " << wrapper->StrError(error);
    return error;
  }

  error = wrapper->PcmHwParamsSetFormat(handle, hw_params, format);
  if (error < 0) {
    LOG(ERROR) << "PcmHwParamsSetFormat: " << wrapper->StrError(error);
    return error;
  }

  error = wrapper->PcmHwParamsSetChannels(handle, hw_params, channels);
  if (error < 0) {
    LOG(ERROR) << "PcmHwParamsSetChannels: " << wrapper->StrError(error);
    return error;
  }

  unsigned int rate = sample_rate;
  error = wrapper->PcmHwParamsSetRateNear(handle, hw_params, &rate, nullptr);
  if (error < 0) {
    LOG(ERROR) << "PcmHwParamsSetRateNear: " << wrapper->StrError(error);
    return error;
  }
  if (rate != sample_rate) {
    LOG(ERROR) << "Rate doesn't match, required: " << sample_rate
               << "Hz, but get: " << rate << "Hz.";
    return -EINVAL;
  }

  error = wrapper->PcmHwParamsSetBufferSizeNear(handle, hw_params,
                                                &frames_per_buffer);
  if (error < 0) {
    LOG(ERROR) << "PcmHwParamsSetBufferSizeNear: " << wrapper->StrError(error);
    return error;
  }

  int direction = 0;
  error = wrapper->PcmHwParamsSetPeriodSizeNear(handle, hw_params,
                                                &frames_per_period, &direction);
  if (error < 0) {
    LOG(ERROR) << "PcmHwParamsSetPeriodSizeNear: " << wrapper->StrError(error);
    return error;
  }

  if (frames_per_period > frames_per_buffer / 2) {
    LOG(ERROR) << "Period size (" << frames_per_period
               << ") is too big; buffer size = " << frames_per_buffer;
    return -EINVAL;
  }

  error = wrapper->PcmHwParams(handle, hw_params);
  if (error < 0)
    LOG(ERROR) << "PcmHwParams: " << wrapper->StrError(error);

  return error;
}

// Set software parameters of PCM. It does the same thing as the corresponding
// part in snd_pcm_set_params()
// (https://github.com/tiwai/alsa-lib/blob/master/src/pcm/pcm.c#L8603).
int ConfigureSwParams(media::AlsaWrapper* wrapper,
                      snd_pcm_t* handle,
                      snd_pcm_uframes_t frames_per_buffer,
                      snd_pcm_uframes_t frames_per_period) {
  int error = 0;

  snd_pcm_sw_params_t* sw_params = nullptr;
  error = wrapper->PcmSwParamsMalloc(&sw_params);
  if (error < 0) {
    LOG(ERROR) << "PcmSwParamsMalloc: " << wrapper->StrError(error);
    return error;
  }
  // |snd_pcm_sw_params_t| is not exposed and thus use a smart pointer to
  // pointer to insure freeing memory when the function returns.
  std::unique_ptr<snd_pcm_sw_params_t*,
                  std::function<void(snd_pcm_sw_params_t**)>>
      params_holder(&sw_params, [wrapper](snd_pcm_sw_params_t** params) {
        wrapper->PcmSwParamsFree(*params);
      });

  error = wrapper->PcmSwParamsCurrent(handle, sw_params);
  if (error < 0) {
    LOG(ERROR) << "PcmSwParamsCurrent: " << wrapper->StrError(error);
    return error;
  }

  // For playback, start the transfer when the buffer is almost full.
  int start_threshold =
      (frames_per_buffer / frames_per_period) * frames_per_period;
  error =
      wrapper->PcmSwParamsSetStartThreshold(handle, sw_params, start_threshold);
  if (error < 0) {
    LOG(ERROR) << "PcmSwParamsSetStartThreshold: " << wrapper->StrError(error);
    return error;
  }

  // For capture, wake capture thread as soon as possible (1 period).
  error = wrapper->PcmSwParamsSetAvailMin(handle, sw_params, frames_per_period);
  if (error < 0) {
    LOG(ERROR) << "PcmSwParamsSetAvailMin: " << wrapper->StrError(error);
    return error;
  }

  error = wrapper->PcmSwParams(handle, sw_params);
  if (error < 0)
    LOG(ERROR) << "PcmSwParams: " << wrapper->StrError(error);

  return error;
}

int SetParams(media::AlsaWrapper* wrapper,
              snd_pcm_t* handle,
              snd_pcm_format_t format,
              unsigned int channels,
              unsigned int rate,
              unsigned int frames_per_buffer,
              unsigned int frames_per_period) {
  int error = ConfigureHwParams(
      wrapper, handle, format, SND_PCM_ACCESS_RW_INTERLEAVED, channels, rate,
      1 /* Enable resampling */, frames_per_buffer, frames_per_period);
  if (error == 0) {
    error = ConfigureSwParams(wrapper, handle, frames_per_buffer,
                              frames_per_period);
  }
  return error;
}

}  // namespace

static snd_pcm_t* OpenDevice(media::AlsaWrapper* wrapper,
                             const char* device_name,
                             snd_pcm_stream_t type,
                             int channels,
                             int sample_rate,
                             snd_pcm_format_t pcm_format,
                             int buffer_us,
                             int period_us = 0) {
  snd_pcm_t* handle = nullptr;
  int error = wrapper->PcmOpen(&handle, device_name, type, SND_PCM_NONBLOCK);
  if (error < 0) {
    LOG(ERROR) << "PcmOpen: " << device_name << "," << wrapper->StrError(error);
    return nullptr;
  }

  error =
      wrapper->PcmSetParams(handle, pcm_format, SND_PCM_ACCESS_RW_INTERLEAVED,
                            channels, sample_rate, 1, buffer_us);
  if (error < 0) {
    LOG(WARNING) << "PcmSetParams: " << device_name << ", "
                 << wrapper->StrError(error);
    // Default parameter setting function failed, try again with the customized
    // one if |period_us| is set, which is the case for capture but not for
    // playback.
    if (period_us > 0) {
      const unsigned int frames_per_buffer = static_cast<unsigned int>(
          static_cast<int64_t>(buffer_us) * sample_rate /
          base::Time::kMicrosecondsPerSecond);
      const unsigned int frames_per_period = static_cast<unsigned int>(
          static_cast<int64_t>(period_us) * sample_rate /
          base::Time::kMicrosecondsPerSecond);
      LOG(WARNING) << "SetParams: " << device_name
                   << " - Format: " << pcm_format << " Channels: " << channels
                   << " Sample rate: " << sample_rate
                   << " Buffer size: " << frames_per_buffer
                   << " Period size: " << frames_per_period;
      error = SetParams(wrapper, handle, pcm_format, channels, sample_rate,
                        frames_per_buffer, frames_per_period);
    }
  }
  if (error < 0) {
    if (alsa_util::CloseDevice(wrapper, handle) < 0) {
      // TODO(ajwong): Retry on certain errors?
      LOG(WARNING) << "Unable to close audio device. Leaking handle.";
    }
    return nullptr;
  }

  return handle;
}

static std::string DeviceNameToControlName(const std::string& device_name) {
  const char kMixerPrefix[] = "hw";
  std::string control_name;
  size_t pos1 = device_name.find(':');
  if (pos1 == std::string::npos) {
    control_name = device_name;
  } else {
    // Examples:
    // deviceName: "front:CARD=Intel,DEV=0", controlName: "hw:CARD=Intel".
    // deviceName: "default:CARD=Intel", controlName: "CARD=Intel".
    size_t pos2 = device_name.find(',');
    control_name = (pos2 == std::string::npos)
                       ? device_name.substr(pos1 + 1)
                       : kMixerPrefix + device_name.substr(pos1, pos2 - pos1);
  }

  return control_name;
}

int CloseDevice(media::AlsaWrapper* wrapper, snd_pcm_t* handle) {
  std::string device_name = wrapper->PcmName(handle);
  int error = wrapper->PcmClose(handle);
  if (error < 0) {
    LOG(ERROR) << "PcmClose: " << device_name << ", "
               << wrapper->StrError(error);
  }

  return error;
}

snd_pcm_t* OpenCaptureDevice(media::AlsaWrapper* wrapper,
                             const char* device_name,
                             int channels,
                             int sample_rate,
                             snd_pcm_format_t pcm_format,
                             int buffer_us,
                             int period_us) {
  return OpenDevice(wrapper, device_name, SND_PCM_STREAM_CAPTURE, channels,
                    sample_rate, pcm_format, buffer_us, period_us);
}

snd_pcm_t* OpenPlaybackDevice(media::AlsaWrapper* wrapper,
                              const char* device_name,
                              int channels,
                              int sample_rate,
                              snd_pcm_format_t pcm_format,
                              int buffer_us) {
  return OpenDevice(wrapper, device_name, SND_PCM_STREAM_PLAYBACK, channels,
                    sample_rate, pcm_format, buffer_us);
}

snd_mixer_t* OpenMixer(media::AlsaWrapper* wrapper,
                       const std::string& device_name) {
  snd_mixer_t* mixer = nullptr;

  int error = wrapper->MixerOpen(&mixer, 0);
  if (error < 0) {
    LOG(ERROR) << "MixerOpen: " << device_name << ", "
               << wrapper->StrError(error);
    return nullptr;
  }

  std::string control_name = DeviceNameToControlName(device_name);
  error = wrapper->MixerAttach(mixer, control_name.c_str());
  if (error < 0) {
    LOG(ERROR) << "MixerAttach, " << control_name << ", "
               << wrapper->StrError(error);
    alsa_util::CloseMixer(wrapper, mixer, device_name);
    return nullptr;
  }

  error = wrapper->MixerElementRegister(mixer, nullptr, nullptr);
  if (error < 0) {
    LOG(ERROR) << "MixerElementRegister: " << control_name << ", "
               << wrapper->StrError(error);
    alsa_util::CloseMixer(wrapper, mixer, device_name);
    return nullptr;
  }

  return mixer;
}

void CloseMixer(media::AlsaWrapper* wrapper, snd_mixer_t* mixer,
                const std::string& device_name) {
  if (!mixer)
    return;

  wrapper->MixerFree(mixer);

  int error = 0;
  if (!device_name.empty()) {
    std::string control_name = DeviceNameToControlName(device_name);
    error = wrapper->MixerDetach(mixer, control_name.c_str());
    if (error < 0) {
      LOG(WARNING) << "MixerDetach: " << control_name << ", "
                   << wrapper->StrError(error);
    }
  }

  error = wrapper->MixerClose(mixer);
  if (error < 0) {
    LOG(WARNING) << "MixerClose: " << wrapper->StrError(error);
  }
}

snd_mixer_elem_t* LoadCaptureMixerElement(media::AlsaWrapper* wrapper,
                                          snd_mixer_t* mixer) {
  if (!mixer)
    return nullptr;

  int error = wrapper->MixerLoad(mixer);
  if (error < 0) {
    LOG(ERROR) << "MixerLoad: " << wrapper->StrError(error);
    return nullptr;
  }

  snd_mixer_elem_t* elem = nullptr;
  snd_mixer_elem_t* mic_elem = nullptr;
  static constexpr std::string_view kCaptureElemName = "Capture";
  static constexpr std::string_view kMicElemName = "Mic";
  for (elem = wrapper->MixerFirstElem(mixer);
       elem;
       elem = wrapper->MixerNextElem(elem)) {
    if (wrapper->MixerSelemIsActive(elem)) {
      auto elem_name = wrapper->MixerSelemName(elem);
      if (kCaptureElemName == elem_name) {
        return elem;
      } else if (kMicElemName == elem_name) {
        mic_elem = elem;
      }
    }
  }

  // Did not find any Capture handle, use the Mic handle.
  return mic_elem;
}

}  // namespace alsa_util
