/*
 * libjingle
 * Copyright 2004--2010, Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "talk/sound/alsasoundsystem.h"

#include "talk/sound/sounddevicelocator.h"
#include "talk/sound/soundinputstreaminterface.h"
#include "talk/sound/soundoutputstreaminterface.h"
#include "webrtc/base/common.h"
#include "webrtc/base/logging.h"
#include "webrtc/base/scoped_ptr.h"
#include "webrtc/base/stringutils.h"
#include "webrtc/base/timeutils.h"
#include "webrtc/base/worker.h"

namespace cricket {

// Lookup table from the cricket format enum in soundsysteminterface.h to
// ALSA's enums.
static const snd_pcm_format_t kCricketFormatToAlsaFormatTable[] = {
  // The order here must match the order in soundsysteminterface.h
  SND_PCM_FORMAT_S16_LE,
};

// Lookup table for the size of a single sample of a given format.
static const size_t kCricketFormatToSampleSizeTable[] = {
  // The order here must match the order in soundsysteminterface.h
  sizeof(int16_t),  // 2
};

// Minimum latency we allow, in microseconds. This is more or less arbitrary,
// but it has to be at least large enough to be able to buffer data during a
// missed context switch, and the typical Linux scheduling quantum is 10ms.
static const int kMinimumLatencyUsecs = 20 * 1000;

// The latency we'll use for kNoLatencyRequirements (chosen arbitrarily).
static const int kDefaultLatencyUsecs = kMinimumLatencyUsecs * 2;

// We translate newlines in ALSA device descriptions to hyphens.
static const char kAlsaDescriptionSearch[] = "\n";
static const char kAlsaDescriptionReplace[] = " - ";

class AlsaDeviceLocator : public SoundDeviceLocator {
 public:
  AlsaDeviceLocator(const std::string &name,
                    const std::string &device_name)
      : SoundDeviceLocator(name, device_name) {
    // The ALSA descriptions have newlines in them, which won't show up in
    // a drop-down box. Replace them with hyphens.
    rtc::replace_substrs(kAlsaDescriptionSearch,
                               sizeof(kAlsaDescriptionSearch) - 1,
                               kAlsaDescriptionReplace,
                               sizeof(kAlsaDescriptionReplace) - 1,
                               &name_);
  }

  virtual SoundDeviceLocator *Copy() const {
    return new AlsaDeviceLocator(*this);
  }
};

// Functionality that is common to both AlsaInputStream and AlsaOutputStream.
class AlsaStream {
 public:
  AlsaStream(AlsaSoundSystem *alsa,
             snd_pcm_t *handle,
             size_t frame_size,
             int wait_timeout_ms,
             int flags,
             int freq)
      : alsa_(alsa),
        handle_(handle),
        frame_size_(frame_size),
        wait_timeout_ms_(wait_timeout_ms),
        flags_(flags),
        freq_(freq) {
  }

  ~AlsaStream() {
    Close();
  }

  // Waits for the stream to be ready to accept/return more data, and returns
  // how much can be written/read, or 0 if we need to Wait() again.
  snd_pcm_uframes_t Wait() {
    snd_pcm_sframes_t frames;
    // Ideally we would not use snd_pcm_wait() and instead hook snd_pcm_poll_*
    // into PhysicalSocketServer, but PhysicalSocketServer is nasty enough
    // already and the current clients of SoundSystemInterface do not run
    // anything else on their worker threads, so snd_pcm_wait() is good enough.
    frames = symbol_table()->snd_pcm_avail_update()(handle_);
    if (frames < 0) {
      LOG(LS_ERROR) << "snd_pcm_avail_update(): " << GetError(frames);
      Recover(frames);
      return 0;
    } else if (frames > 0) {
      // Already ready, so no need to wait.
      return frames;
    }
    // Else no space/data available, so must wait.
    int ready = symbol_table()->snd_pcm_wait()(handle_, wait_timeout_ms_);
    if (ready < 0) {
      LOG(LS_ERROR) << "snd_pcm_wait(): " << GetError(ready);
      Recover(ready);
      return 0;
    } else if (ready == 0) {
      // Timeout, so nothing can be written/read right now.
      // We set the timeout to twice the requested latency, so continuous
      // timeouts are indicative of a problem, so log as a warning.
      LOG(LS_WARNING) << "Timeout while waiting on stream";
      return 0;
    }
    // Else ready > 0 (i.e., 1), so it's ready. Get count.
    frames = symbol_table()->snd_pcm_avail_update()(handle_);
    if (frames < 0) {
      LOG(LS_ERROR) << "snd_pcm_avail_update(): " << GetError(frames);
      Recover(frames);
      return 0;
    } else if (frames == 0) {
      // wait() said we were ready, so this ought to have been positive. Has
      // been observed to happen in practice though.
      LOG(LS_WARNING) << "Spurious wake-up";
    }
    return frames;
  }

  int CurrentDelayUsecs() {
    if (!(flags_ & SoundSystemInterface::FLAG_REPORT_LATENCY)) {
      return 0;
    }

    snd_pcm_sframes_t delay;
    int err = symbol_table()->snd_pcm_delay()(handle_, &delay);
    if (err != 0) {
      LOG(LS_ERROR) << "snd_pcm_delay(): " << GetError(err);
      Recover(err);
      // We'd rather continue playout/capture with an incorrect delay than stop
      // it altogether, so return a valid value.
      return 0;
    }
    // The delay is in frames. Convert to microseconds.
    return delay * rtc::kNumMicrosecsPerSec / freq_;
  }

  // Used to recover from certain recoverable errors, principally buffer overrun
  // or underrun (identified as EPIPE). Without calling this the stream stays
  // in the error state forever.
  bool Recover(int error) {
    int err;
    err = symbol_table()->snd_pcm_recover()(
        handle_,
        error,
        // Silent; i.e., no logging on stderr.
        1);
    if (err != 0) {
      // Docs say snd_pcm_recover returns the original error if it is not one
      // of the recoverable ones, so this log message will probably contain the
      // same error twice.
      LOG(LS_ERROR) << "Unable to recover from \"" << GetError(error) << "\": "
                    << GetError(err);
      return false;
    }
    if (error == -EPIPE &&  // Buffer underrun/overrun.
        symbol_table()->snd_pcm_stream()(handle_) == SND_PCM_STREAM_CAPTURE) {
      // For capture streams we also have to repeat the explicit start() to get
      // data flowing again.
      err = symbol_table()->snd_pcm_start()(handle_);
      if (err != 0) {
        LOG(LS_ERROR) << "snd_pcm_start(): " << GetError(err);
        return false;
      }
    }
    return true;
  }

  bool Close() {
    if (handle_) {
      int err;
      err = symbol_table()->snd_pcm_drop()(handle_);
      if (err != 0) {
        LOG(LS_ERROR) << "snd_pcm_drop(): " << GetError(err);
        // Continue anyways.
      }
      err = symbol_table()->snd_pcm_close()(handle_);
      if (err != 0) {
        LOG(LS_ERROR) << "snd_pcm_close(): " << GetError(err);
        // Continue anyways.
      }
      handle_ = NULL;
    }
    return true;
  }

  AlsaSymbolTable *symbol_table() {
    return &alsa_->symbol_table_;
  }

  snd_pcm_t *handle() {
    return handle_;
  }

  const char *GetError(int err) {
    return alsa_->GetError(err);
  }

  size_t frame_size() {
    return frame_size_;
  }

 private:
  AlsaSoundSystem *alsa_;
  snd_pcm_t *handle_;
  size_t frame_size_;
  int wait_timeout_ms_;
  int flags_;
  int freq_;

  DISALLOW_COPY_AND_ASSIGN(AlsaStream);
};

// Implementation of an input stream. See soundinputstreaminterface.h regarding
// thread-safety.
class AlsaInputStream :
    public SoundInputStreamInterface,
    private rtc::Worker {
 public:
  AlsaInputStream(AlsaSoundSystem *alsa,
                  snd_pcm_t *handle,
                  size_t frame_size,
                  int wait_timeout_ms,
                  int flags,
                  int freq)
      : stream_(alsa, handle, frame_size, wait_timeout_ms, flags, freq),
        buffer_size_(0) {
  }

  virtual ~AlsaInputStream() {
    bool success = StopReading();
    // We need that to live.
    VERIFY(success);
  }

  virtual bool StartReading() {
    return StartWork();
  }

  virtual bool StopReading() {
    return StopWork();
  }

  virtual bool GetVolume(int *volume) {
    // TODO: Implement this.
    return false;
  }

  virtual bool SetVolume(int volume) {
    // TODO: Implement this.
    return false;
  }

  virtual bool Close() {
    return StopReading() && stream_.Close();
  }

  virtual int LatencyUsecs() {
    return stream_.CurrentDelayUsecs();
  }

 private:
  // Inherited from Worker.
  virtual void OnStart() {
    HaveWork();
  }

  // Inherited from Worker.
  virtual void OnHaveWork() {
    // Block waiting for data.
    snd_pcm_uframes_t avail = stream_.Wait();
    if (avail > 0) {
      // Data is available.
      size_t size = avail * stream_.frame_size();
      if (size > buffer_size_) {
        // Must increase buffer size.
        buffer_.reset(new char[size]);
        buffer_size_ = size;
      }
      // Read all the data.
      snd_pcm_sframes_t read = stream_.symbol_table()->snd_pcm_readi()(
          stream_.handle(),
          buffer_.get(),
          avail);
      if (read < 0) {
        LOG(LS_ERROR) << "snd_pcm_readi(): " << GetError(read);
        stream_.Recover(read);
      } else if (read == 0) {
        // Docs say this shouldn't happen.
        ASSERT(false);
        LOG(LS_ERROR) << "No data?";
      } else {
        // Got data. Pass it off to the app.
        SignalSamplesRead(buffer_.get(),
                          read * stream_.frame_size(),
                          this);
      }
    }
    // Check for more data with no delay, after any pending messages are
    // dispatched.
    HaveWork();
  }

  // Inherited from Worker.
  virtual void OnStop() {
    // Nothing to do.
  }

  const char *GetError(int err) {
    return stream_.GetError(err);
  }

  AlsaStream stream_;
  rtc::scoped_ptr<char[]> buffer_;
  size_t buffer_size_;

  DISALLOW_COPY_AND_ASSIGN(AlsaInputStream);
};

// Implementation of an output stream. See soundoutputstreaminterface.h
// regarding thread-safety.
class AlsaOutputStream :
    public SoundOutputStreamInterface,
    private rtc::Worker {
 public:
  AlsaOutputStream(AlsaSoundSystem *alsa,
                   snd_pcm_t *handle,
                   size_t frame_size,
                   int wait_timeout_ms,
                   int flags,
                   int freq)
      : stream_(alsa, handle, frame_size, wait_timeout_ms, flags, freq) {
  }

  virtual ~AlsaOutputStream() {
    bool success = DisableBufferMonitoring();
    // We need that to live.
    VERIFY(success);
  }

  virtual bool EnableBufferMonitoring() {
    return StartWork();
  }

  virtual bool DisableBufferMonitoring() {
    return StopWork();
  }

  virtual bool WriteSamples(const void *sample_data,
                            size_t size) {
    if (size % stream_.frame_size() != 0) {
      // No client of SoundSystemInterface does this, so let's not support it.
      // (If we wanted to support it, we'd basically just buffer the fractional
      // frame until we get more data.)
      ASSERT(false);
      LOG(LS_ERROR) << "Writes with fractional frames are not supported";
      return false;
    }
    snd_pcm_uframes_t frames = size / stream_.frame_size();
    snd_pcm_sframes_t written = stream_.symbol_table()->snd_pcm_writei()(
        stream_.handle(),
        sample_data,
        frames);
    if (written < 0) {
      LOG(LS_ERROR) << "snd_pcm_writei(): " << GetError(written);
      stream_.Recover(written);
      return false;
    } else if (static_cast<snd_pcm_uframes_t>(written) < frames) {
      // Shouldn't happen. Drop the rest of the data.
      LOG(LS_ERROR) << "Stream wrote only " << written << " of " << frames
                    << " frames!";
      return false;
    }
    return true;
  }

  virtual bool GetVolume(int *volume) {
    // TODO: Implement this.
    return false;
  }

  virtual bool SetVolume(int volume) {
    // TODO: Implement this.
    return false;
  }

  virtual bool Close() {
    return DisableBufferMonitoring() && stream_.Close();
  }

  virtual int LatencyUsecs() {
    return stream_.CurrentDelayUsecs();
  }

 private:
  // Inherited from Worker.
  virtual void OnStart() {
    HaveWork();
  }

  // Inherited from Worker.
  virtual void OnHaveWork() {
    snd_pcm_uframes_t avail = stream_.Wait();
    if (avail > 0) {
      size_t space = avail * stream_.frame_size();
      SignalBufferSpace(space, this);
    }
    HaveWork();
  }

  // Inherited from Worker.
  virtual void OnStop() {
    // Nothing to do.
  }

  const char *GetError(int err) {
    return stream_.GetError(err);
  }

  AlsaStream stream_;

  DISALLOW_COPY_AND_ASSIGN(AlsaOutputStream);
};

AlsaSoundSystem::AlsaSoundSystem() : initialized_(false) {}

AlsaSoundSystem::~AlsaSoundSystem() {
  // Not really necessary, because Terminate() doesn't really do anything.
  Terminate();
}

bool AlsaSoundSystem::Init() {
  if (IsInitialized()) {
    return true;
  }

  // Load libasound.
  if (!symbol_table_.Load()) {
    // Very odd for a Linux machine to not have a working libasound ...
    LOG(LS_ERROR) << "Failed to load symbol table";
    return false;
  }

  initialized_ = true;

  return true;
}

void AlsaSoundSystem::Terminate() {
  if (!IsInitialized()) {
    return;
  }

  initialized_ = false;

  // We do not unload the symbol table because we may need it again soon if
  // Init() is called again.
}

bool AlsaSoundSystem::EnumeratePlaybackDevices(
    SoundDeviceLocatorList *devices) {
  return EnumerateDevices(devices, false);
}

bool AlsaSoundSystem::EnumerateCaptureDevices(
    SoundDeviceLocatorList *devices) {
  return EnumerateDevices(devices, true);
}

bool AlsaSoundSystem::GetDefaultPlaybackDevice(SoundDeviceLocator **device) {
  return GetDefaultDevice(device);
}

bool AlsaSoundSystem::GetDefaultCaptureDevice(SoundDeviceLocator **device) {
  return GetDefaultDevice(device);
}

SoundOutputStreamInterface *AlsaSoundSystem::OpenPlaybackDevice(
    const SoundDeviceLocator *device,
    const OpenParams &params) {
  return OpenDevice<SoundOutputStreamInterface>(
      device,
      params,
      SND_PCM_STREAM_PLAYBACK,
      &AlsaSoundSystem::StartOutputStream);
}

SoundInputStreamInterface *AlsaSoundSystem::OpenCaptureDevice(
    const SoundDeviceLocator *device,
    const OpenParams &params) {
  return OpenDevice<SoundInputStreamInterface>(
      device,
      params,
      SND_PCM_STREAM_CAPTURE,
      &AlsaSoundSystem::StartInputStream);
}

const char *AlsaSoundSystem::GetName() const {
  return "ALSA";
}

bool AlsaSoundSystem::EnumerateDevices(
    SoundDeviceLocatorList *devices,
    bool capture_not_playback) {
  ClearSoundDeviceLocatorList(devices);

  if (!IsInitialized()) {
    return false;
  }

  const char *type = capture_not_playback ? "Input" : "Output";
  // dmix and dsnoop are only for playback and capture, respectively, but ALSA
  // stupidly includes them in both lists.
  const char *ignore_prefix = capture_not_playback ? "dmix:" : "dsnoop:";
  // (ALSA lists many more "devices" of questionable interest, but we show them
  // just in case the weird devices may actually be desirable for some
  // users/systems.)
  const char *ignore_default = "default";
  const char *ignore_null = "null";
  const char *ignore_pulse = "pulse";
  // The 'pulse' entry has a habit of mysteriously disappearing when you query
  // a second time. Remove it from our list. (GIPS lib did the same thing.)
  int err;

  void **hints;
  err = symbol_table_.snd_device_name_hint()(-1,     // All cards
                                             "pcm",  // Only PCM devices
                                             &hints);
  if (err != 0) {
    LOG(LS_ERROR) << "snd_device_name_hint(): " << GetError(err);
    return false;
  }

  for (void **list = hints; *list != NULL; ++list) {
    char *actual_type = symbol_table_.snd_device_name_get_hint()(*list, "IOID");
    if (actual_type) {  // NULL means it's both.
      bool wrong_type = (strcmp(actual_type, type) != 0);
      free(actual_type);
      if (wrong_type) {
        // Wrong type of device (i.e., input vs. output).
        continue;
      }
    }

    char *name = symbol_table_.snd_device_name_get_hint()(*list, "NAME");
    if (!name) {
      LOG(LS_ERROR) << "Device has no name???";
      // Skip it.
      continue;
    }

    // Now check if we actually want to show this device.
    if (strcmp(name, ignore_default) != 0 &&
        strcmp(name, ignore_null) != 0 &&
        strcmp(name, ignore_pulse) != 0 &&
        !rtc::starts_with(name, ignore_prefix)) {

      // Yes, we do.
      char *desc = symbol_table_.snd_device_name_get_hint()(*list, "DESC");
      if (!desc) {
        // Virtual devices don't necessarily have descriptions. Use their names
        // instead (not pretty!).
        desc = name;
      }

      AlsaDeviceLocator *device = new AlsaDeviceLocator(desc, name);

      devices->push_back(device);

      if (desc != name) {
        free(desc);
      }
    }

    free(name);
  }

  err = symbol_table_.snd_device_name_free_hint()(hints);
  if (err != 0) {
    LOG(LS_ERROR) << "snd_device_name_free_hint(): " << GetError(err);
    // Continue and return true anyways, since we did get the whole list.
  }

  return true;
}

bool AlsaSoundSystem::GetDefaultDevice(SoundDeviceLocator **device) {
  if (!IsInitialized()) {
    return false;
  }
  *device = new AlsaDeviceLocator("Default device", "default");
  return true;
}

inline size_t AlsaSoundSystem::FrameSize(const OpenParams &params) {
  ASSERT(static_cast<int>(params.format) <
         ARRAY_SIZE(kCricketFormatToSampleSizeTable));
  return kCricketFormatToSampleSizeTable[params.format] * params.channels;
}

template <typename StreamInterface>
StreamInterface *AlsaSoundSystem::OpenDevice(
    const SoundDeviceLocator *device,
    const OpenParams &params,
    snd_pcm_stream_t type,
    StreamInterface *(AlsaSoundSystem::*start_fn)(
        snd_pcm_t *handle,
        size_t frame_size,
        int wait_timeout_ms,
        int flags,
        int freq)) {

  if (!IsInitialized()) {
    return NULL;
  }

  StreamInterface *stream;
  int err;

  const char *dev = static_cast<const AlsaDeviceLocator *>(device)->
      device_name().c_str();

  snd_pcm_t *handle = NULL;
  err = symbol_table_.snd_pcm_open()(
      &handle,
      dev,
      type,
      // No flags.
      0);
  if (err != 0) {
    LOG(LS_ERROR) << "snd_pcm_open(" << dev << "): " << GetError(err);
    return NULL;
  }
  LOG(LS_VERBOSE) << "Opening " << dev;
  ASSERT(handle);  // If open succeeded, handle ought to be valid

  // Compute requested latency in microseconds.
  int latency;
  if (params.latency == kNoLatencyRequirements) {
    latency = kDefaultLatencyUsecs;
  } else {
    // kLowLatency is 0, so we treat it the same as a request for zero latency.
    // Compute what the user asked for.
    latency = rtc::kNumMicrosecsPerSec *
        params.latency /
        params.freq /
        FrameSize(params);
    // And this is what we'll actually use.
    latency = rtc::_max(latency, kMinimumLatencyUsecs);
  }

  ASSERT(static_cast<int>(params.format) <
         ARRAY_SIZE(kCricketFormatToAlsaFormatTable));

  err = symbol_table_.snd_pcm_set_params()(
      handle,
      kCricketFormatToAlsaFormatTable[params.format],
      // SoundSystemInterface only supports interleaved audio.
      SND_PCM_ACCESS_RW_INTERLEAVED,
      params.channels,
      params.freq,
      1,  // Allow ALSA to resample.
      latency);
  if (err != 0) {
    LOG(LS_ERROR) << "snd_pcm_set_params(): " << GetError(err);
    goto fail;
  }

  err = symbol_table_.snd_pcm_prepare()(handle);
  if (err != 0) {
    LOG(LS_ERROR) << "snd_pcm_prepare(): " << GetError(err);
    goto fail;
  }

  stream = (this->*start_fn)(
      handle,
      FrameSize(params),
      // We set the wait time to twice the requested latency, so that wait
      // timeouts should be rare.
      2 * latency / rtc::kNumMicrosecsPerMillisec,
      params.flags,
      params.freq);
  if (stream) {
    return stream;
  }
  // Else fall through.

 fail:
  err = symbol_table_.snd_pcm_close()(handle);
  if (err != 0) {
    LOG(LS_ERROR) << "snd_pcm_close(): " << GetError(err);
  }
  return NULL;
}

SoundOutputStreamInterface *AlsaSoundSystem::StartOutputStream(
    snd_pcm_t *handle,
    size_t frame_size,
    int wait_timeout_ms,
    int flags,
    int freq) {
  // Nothing to do here but instantiate the stream.
  return new AlsaOutputStream(
      this, handle, frame_size, wait_timeout_ms, flags, freq);
}

SoundInputStreamInterface *AlsaSoundSystem::StartInputStream(
    snd_pcm_t *handle,
    size_t frame_size,
    int wait_timeout_ms,
    int flags,
    int freq) {
  // Output streams start automatically once enough data has been written, but
  // input streams must be started manually or else snd_pcm_wait() will never
  // return true.
  int err;
  err = symbol_table_.snd_pcm_start()(handle);
  if (err != 0) {
    LOG(LS_ERROR) << "snd_pcm_start(): " << GetError(err);
    return NULL;
  }
  return new AlsaInputStream(
      this, handle, frame_size, wait_timeout_ms, flags, freq);
}

inline const char *AlsaSoundSystem::GetError(int err) {
  return symbol_table_.snd_strerror()(err);
}

}  // namespace cricket
