/**
 * Copyright (c) 2015, Samsung Electronics Co., Ltd
 * All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 *  * 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.
 *
 *  * Neither the name of Samsung Electronics nor the names of its
 *    contributors may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
 *
 * @file  audio.cc
 *
 * @brief
 * This module demonstrates usage of pp::Audio interface. Besides simple
 * WAVE file loading and playing, it also shows how to mix together more
 * than one sound source.
 * More information about WAVE header can be found on:
 * http://soundfile.sapp.org/doc/WaveFormat/
 */

#include "audio.h"
#include <math.h>
#include <algorithm>
#include <limits>
#include <sstream>
#include <utility>
#include "logger.h"

// constant literals
const char* kPlayCommand = "play";
const char* kPauseCommand = "pause";
const char* kStopCommand = "stop";
const char* kLoadCommand = "load";
const char* kFileLoadedMessage = "-file loaded";
const char* kErrorMessage = "-error";
const char* kPlayMessage = "-play";
const char* kPauseMessage = "-pause";
const char* kStopMessage = "-stop";
const char* kReturnStopMessage = "-returnStop";
const char* kSoundsFolder = "resources/sounds/";
const char* kHeaderChunkId = "RIFF";
const char* kHeaderFormat = "WAVE";
const char* kHeaderSubchunk1Id = "fmt ";
const char* kHeaderSubchunk2Id = "data";

/** Utility functions */
namespace {

bool StartsWith(const std::string& s, const char* prefix) {
  return (s.find(prefix) == 0);
}

}

SoundInstance::SoundInstance(int id, uint16_t channels,
      uint32_t sample_data_size, std::unique_ptr<uint16_t>* sample_data) :
    id_(id),
    channels_(channels),
    sample_data_size_(sample_data_size),
    sample_data_offset_(0) {
  sample_data_.reset(new uint16_t[sample_data_size]);
  memcpy(sample_data_.get(), sample_data->get(),
      sample_data_size_ * sizeof(uint16_t));
}

AudioInstance::AudioInstance(PP_Instance instance) :
    pp::Instance(instance),
    file_number_(0),
    active_sounds_number_(0),
    callback_factory_(this) {
  file_names_.reserve(kNumberOfInputSounds);
  Logger::InitializeInstance(this);
}

AudioInstance::~AudioInstance() {
}

bool AudioInstance::Init(uint32_t /*argc*/, const char* /*argn*/[],
    const char* /*argv*/[]) {
  // getting sample frame count from the browser
  sample_frame_count_ =
    pp::AudioConfig::RecommendSampleFrameCount(this,
      PP_AUDIOSAMPLERATE_44100,
      SAMPLE_FRAME_COUNT);
  // creating an audio resource with configuration using 44,1kHz samplerate
  // and sample frame count form the browser
  pp::AudioConfig audio_config = pp::AudioConfig(this,
      PP_AUDIOSAMPLERATE_44100,
      sample_frame_count_);
  audio_ = pp::Audio(this,
      audio_config,
      AudioCallback,
      this);
  return true;
}

void AudioInstance::HandleMessage(const pp::Var& var_message) {
  if (var_message.is_string()) {
    const std::string message = var_message.AsString();
    if (StartsWith(message, kPlayCommand)) {
      // start playing
      Play(GetIdFromMessage(message, kPlayCommand));
    } else if (StartsWith(message, kPauseCommand)) {
      // pause
      Pause(GetIdFromMessage(message, kPauseCommand));
    } else if (StartsWith(message, kStopCommand)) {
      // stop
      Stop(GetIdFromMessage(message, kStopCommand));
    } else if (StartsWith(message, kLoadCommand)) {
      // load file
      PrepareReadingFile(message.substr(strlen(kLoadCommand)));
    }
  }
}

std::string AudioInstance::CreateCommandMessage(const char* command,
    int number) {
  std::stringstream message;
  message << command << number;
  return message.str();
}

int AudioInstance::GetIdFromMessage(const std::string& message,
    const char* command) {
  int retVal = -1;
  std::istringstream iss(message.substr(strlen(command)));
  iss >> retVal;
  return retVal;
}

std::string AudioInstance::CheckForWAVEHeaderErrors(
    const WAVEFileHeader& header) {
  std::stringstream error;
  if (strncmp(header.chunk_id, kHeaderChunkId, strlen(kHeaderChunkId)) != 0) {
    error << "\tHeader should start with \"RIFF\" letters.\n";
  }
  if (strncmp(header.format, kHeaderFormat, strlen(kHeaderFormat)) != 0) {
    error << "\tFormat should be \"WAVE\".\n";
  }
  if (strncmp(header.subchunk1_id, kHeaderSubchunk1Id,
      strlen(kHeaderSubchunk1Id)) != 0) {
    error << "\tSubchunk1ID should contain letters \"fmt \".\n";
  }
  if (strncmp(header.subchunk2_id, kHeaderSubchunk2Id,
      strlen(kHeaderSubchunk2Id)) != 0) {
    error << "\tSubchunk2ID should contain letters \"data\".\n";
  }
  if (header.audio_format != 1) {
    error << "\tAudio format should be PCM, "
        << "but it is " << header.audio_format << ".\n";
  }
  if (header.bits_per_sample != 16) {
    error << "\tBits per sample should be 16, but it is "
        << header.bits_per_sample << ".\n";
  }
  if (header.subchunk1_size != 16) {
    error << "\tSize of subchunk1 should be 16, but is "
        << header.subchunk1_size << ".\n";
  }
  if (header.chunk_size != 4 + (8 + header.subchunk1_size)
      + (8 + header.subchunk2_size)) {
    error << "\tChunk size should be " << (8 + header.subchunk1_size) +
        (8 + header.subchunk2_size) << ", but is "<< header.chunk_size
        << ".\n";
  }
  if (header.num_channels != 1 && header.num_channels != 2) {
    error << "\tSupporting only mono and stereo sound, but number of "
        "channels equals " << header.num_channels << ".\n";
  }
  if (header.byte_rate != header.sample_rate * header.num_channels *
      header.bits_per_sample / 8) {
    error << "\tByte rate should be " << header.sample_rate *
        header.num_channels * header.bits_per_sample / 8 << ", but is "
        << header.byte_rate << ".\n";
  }
  if (header.block_align != header.num_channels * header.bits_per_sample / 8) {
    error << "\tBlock align should be " << header.num_channels *
        header.bits_per_sample / 8 << ", but is " << header.block_align
        << ".\n";
  }
  return error.str();
}

void AudioInstance::ReadWAVE(const std::string& data) {
  Logger::Log("Interpreting WAVE data of file %s",
      file_names_[file_number_].c_str());

  // clean and create buffer for interpreting data read from file
  file_data_bytes_.reset(new char[data.size() + 1]);

  // copying data read from file to buffer
  std::copy(data.begin(), data.end(), file_data_bytes_.get());
  file_data_bytes_[data.size()] = '\0';

  // reading header data
  header_ = reinterpret_cast<WAVEFileHeader*>(file_data_bytes_.get());

  // check for malformed header
  std::string header_error = CheckForWAVEHeaderErrors(*header_);
  if (!header_error.empty()) {
    Logger::Error("Unsupported file %s - bad header:\n%s",
        file_names_[file_number_].c_str(), header_error.c_str());

    PostMessage(CreateCommandMessage(kErrorMessage, file_number_));
    return;
  }
  std::unique_ptr<uint16_t> sample_data(reinterpret_cast<uint16_t*>(
      file_data_bytes_.get() + sizeof(WAVEFileHeader)));

  // create new sound instance, fill it's fields and add it
  // to a sound instances vector
  std::shared_ptr<SoundInstance> instance(new SoundInstance(file_number_,
      header_->num_channels, (data.size() - sizeof(WAVEFileHeader)) /
      sizeof(uint16_t), &sample_data));
  sound_instances_.insert(std::pair<int, std::shared_ptr<SoundInstance> >(
      instance->id_, instance));

  // depending on current file reading send appropriate control message
  PostMessage(CreateCommandMessage(kFileLoadedMessage, file_number_));

  Logger::Log("File %s data loaded",
      file_names_[file_number_].c_str());
}

void AudioInstance::Play(int number) {
  // start playing if nothing is active
  if (active_sounds_number_ == 0) {
    audio_.StartPlayback();
    Logger::Log("Playing started");
  }
  assert((number - 1 >= 0) || (number - 1 < kNumberOfInputSounds));
  active_sounds_[number - 1] = true;
  ++active_sounds_number_;

  // send message to browser, that playing started
  PostMessage(CreateCommandMessage(kPlayMessage, number));
}

void AudioInstance::Pause(int number) {
  // set clicked sound as not active
  --active_sounds_number_;
  assert((number - 1 >= 0) || (number - 1 < kNumberOfInputSounds));
  active_sounds_[number - 1] = false;

  // stop playing if nothing is active
  if (active_sounds_number_ == 0) {
    audio_.StopPlayback();
    Logger::Log("Playing stopped");
  }
  // send message to browser, that playing is paused
  PostMessage(CreateCommandMessage(kPauseMessage, number));
}

void AudioInstance::Stop(int number) {
  // set clicked sound as not active
  assert((number - 1 >= 0) || (number - 1 < kNumberOfInputSounds));
  if (active_sounds_[number - 1]) {
    --active_sounds_number_;
    active_sounds_[number - 1] = false;
  }
  // stop playing if nothing is active
  if (active_sounds_number_ == 0) {
    audio_.StopPlayback();
    Logger::Log("Playing stopped");
  }

  // set offset to 0 to start playing from the beginning
  // the next time play is requested
  sound_instances_[number]->sample_data_offset_ = 0;

  // send message to browser, that playing stopped
  PostMessage(CreateCommandMessage(kStopMessage, number));
}

void AudioInstance::AudioCallback(void* samples, uint32_t buffer_size,
    void* data) {

  // instance pointer is passed when creating Audio resource
  AudioInstance* audio_instance = reinterpret_cast<AudioInstance*>(data);

  // browser audio buffer to fill
  int16_t* buff = reinterpret_cast<int16_t*>(samples);
  memset(buff, 0, buffer_size);

  assert(audio_instance->active_sounds_number_ > 0);
  // compute volume of sound instances
  const double volume = 1.0 / sqrt(audio_instance->active_sounds_number_);
  // Make sure we can't write outside the buffer
  assert(buffer_size >= (sizeof(*buff) * MAX_CHANNELS_NUMBER *
      audio_instance->sample_frame_count_));

  for (SoundInstances::iterator it =
      audio_instance->sound_instances_.begin(); it !=
          audio_instance->sound_instances_.end(); ++it) {
    std::shared_ptr<SoundInstance> instance(it->second);

    // main loop for writing samples to audio buffer
    for (size_t i = 0;  i < audio_instance->sample_frame_count_; ++i) {
      // if there are samples to play
      if (audio_instance->active_sounds_[instance->id_ - 1] &&
        instance->sample_data_offset_ < instance->sample_data_size_) {
        audio_instance->SafeAdd(&buff[2 * i], volume *
            (int16_t)instance->sample_data_[instance->sample_data_offset_]);
        // for mono sound each sample is passed to both channels, but for
        // stereo samples are written successively to all channels
        if (instance->channels_ == 2) {
          ++instance->sample_data_offset_;
        }
        audio_instance->SafeAdd(&buff[2 * i + 1],  volume *
            (int16_t)instance->sample_data_[instance->sample_data_offset_]);
        ++instance->sample_data_offset_;
      }
    }

    // playing finished so stop it
    if (instance->sample_data_offset_ == instance->sample_data_size_) {
      // Normally we should avoid Pepper API calls in audio callback,
      // as it may result in an audio callback thread being swapped out
      // which leads to audio dropouts. Here the Pepper API is called
      // when audio data ends, so a dropout wouldn't bother us.
      //
      audio_instance->PostMessage(audio_instance->CreateCommandMessage(
          kReturnStopMessage, instance->id_));
    }
  }
}

void AudioInstance::SafeAdd(int16_t *dest, int16_t arg) const {
  int32_t value = *dest;
  value += arg;
  // make sure we don't write to big or too small value
  value = std::min(static_cast<int32_t>(std::numeric_limits<int16_t>::max()),
      static_cast<int32_t>(value));
  value = std::max(static_cast<int32_t>(std::numeric_limits<int16_t>::min()),
      static_cast<int32_t>(value));
  *dest = value;
}

void AudioInstance::PrepareReadingFile(const std::string &file_name_command) {
  // clean old and create new resources for reading from file
  url_request_.reset(new pp::URLRequestInfo(this));
  url_loader_.reset(new pp::URLLoader(this));
  buffer_.reset(new char[READ_BUFFER_SIZE]);
  file_data_ = "";

  // extract file id and name
  std::string file_number = file_name_command.substr(0,
      file_name_command.find(" "));
  std::string file_name = file_name_command.substr(file_number.length() + 1,
      file_name_command.size() - file_number.size() - 1);
  file_number_ = atoi(file_number.c_str());
  if (file_names_.size() <= file_number_)
    file_names_.resize(file_number_ + 1);
  file_names_[file_number_] = file_name;

  // set file to download
  url_ = kSoundsFolder + file_names_[file_number_];
  url_request_->SetURL(url_);
  url_request_->SetMethod("GET");
  url_request_->SetRecordDownloadProgress(true);

  // start reading from file
  pp::CompletionCallback open_callback =
      callback_factory_.NewCallback(&AudioInstance::OnOpenURLCallback);
  url_loader_->Open(*url_request_, open_callback);
  Logger::Log("Reading file " + file_name);
}

void AudioInstance::OnOpenURLCallback(int32_t result) {
  // if file haven't opened correctly
  if (result != PP_OK) {
    FileReadCompleted(false);
    return;
  }
  Logger::Log("Opened file %s", file_names_[file_number_].c_str());
  // start reading file data
  ReadFile();
}

void AudioInstance::ReadFile(void) {
  pp::CompletionCallback callback =
      callback_factory_.NewOptionalCallback(&AudioInstance::OnReadFileCallback);
  int32_t result = PP_OK;

  // read parts of file until whole file is read
  while (true) {
    result = url_loader_->ReadResponseBody(buffer_.get(), READ_BUFFER_SIZE,
        callback);
    if (result > 0) {
      AppendData(result);
    } else {
      break;
    }
  }

  // if something went wrong
  if (result != PP_OK_COMPLETIONPENDING) {
    callback.Run(result);
  }
}
void AudioInstance::OnReadFileCallback(int32_t result) {
  // whole file was read
  if (result == PP_OK) {
    FileReadCompleted(true);
  } else if (result > 0) {  // still there is something to read
    AppendData(result);
    ReadFile();
  } else {  // an error occurred
    FileReadCompleted(false);
  }
}

void AudioInstance::AppendData(int32_t bytes_count) {
  // save data read from file
  file_data_.insert(file_data_.end(), buffer_.get(),
      buffer_.get() + bytes_count);
}

void AudioInstance::FileReadCompleted(bool result) {
  // clear the buffer
  buffer_.reset();

  // an error occurred
  if (!result) {
    Logger::Error("Could not read file %s",
        file_names_[file_number_].c_str());

    PostMessage(CreateCommandMessage(kErrorMessage, file_number_));
  } else {  // file was read correctly
    Logger::Log("File %s reading complete",
        file_names_[file_number_].c_str());
    // interpret data of audio file
    ReadWAVE(file_data_);
  }
}


class AudioModule: public pp::Module {
 public:
  AudioModule() : pp::Module() {
  }
  virtual ~AudioModule() {
  }

  virtual pp::Instance* CreateInstance(PP_Instance instance) {
    return new AudioInstance(instance);
  }
};

namespace pp {

Module* CreateModule() {
  return new AudioModule();
}

}  // namespace pp
