// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <string>
#include <vector>

#include "base/float_util.h"
#include "base/json/json_writer.h"
#include "base/message_loop.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_event_router.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tts_api.h"
#include "chrome/browser/profiles/profile.h"

namespace util = extension_tts_api_util;

namespace {
const char kSpeechInterruptedError[] = "Utterance interrupted.";
const char kSpeechRemovedFromQueueError[] = "Utterance removed from queue.";
const int kSpeechCheckDelayIntervalMs = 100;
};

namespace events {
const char kOnSpeak[] = "experimental.tts.onSpeak";
const char kOnStop[] = "experimental.tts.onStop";
};  // namespace events

//
// ExtensionTtsPlatformImpl
//

std::string ExtensionTtsPlatformImpl::error() {
  return error_;
}

void ExtensionTtsPlatformImpl::clear_error() {
  error_ = std::string();
}

void ExtensionTtsPlatformImpl::set_error(const std::string& error) {
  error_ = error;
}

//
// Utterance
//

// static
int Utterance::next_utterance_id_ = 0;

Utterance::Utterance(Profile* profile,
                     const std::string& text,
                     DictionaryValue* options,
                     Task* completion_task)
    : profile_(profile),
      id_(next_utterance_id_++),
      text_(text),
      rate_(-1.0),
      pitch_(-1.0),
      volume_(-1.0),
      can_enqueue_(false),
      completion_task_(completion_task) {
  if (!options) {
    // Use all default options.
    options_.reset(new DictionaryValue());
    return;
  }

  options_.reset(options->DeepCopy());

  if (options->HasKey(util::kVoiceNameKey))
    options->GetString(util::kVoiceNameKey, &voice_name_);

  if (options->HasKey(util::kLocaleKey))
    options->GetString(util::kLocaleKey, &locale_);

  if (options->HasKey(util::kGenderKey))
    options->GetString(util::kGenderKey, &gender_);

  if (util::ReadNumberByKey(options, util::kRateKey, &rate_)) {
    if (!base::IsFinite(rate_) || rate_ < 0.0 || rate_ > 1.0)
      rate_ = -1.0;
  }

  if (util::ReadNumberByKey(options, util::kPitchKey, &pitch_)) {
    if (!base::IsFinite(pitch_) || pitch_ < 0.0 || pitch_ > 1.0)
      pitch_ = -1.0;
  }

  if (util::ReadNumberByKey(options, util::kVolumeKey, &volume_)) {
    if (!base::IsFinite(volume_) || volume_ < 0.0 || volume_ > 1.0)
      volume_ = -1.0;
  }

  if (options->HasKey(util::kEnqueueKey))
    options->GetBoolean(util::kEnqueueKey, &can_enqueue_);
}

Utterance::~Utterance() {
  DCHECK_EQ(completion_task_, static_cast<Task *>(NULL));
}

void Utterance::FinishAndDestroy() {
  completion_task_->Run();
  completion_task_ = NULL;
  delete this;
}

//
// ExtensionTtsController
//

// static
ExtensionTtsController* ExtensionTtsController::GetInstance() {
  return Singleton<ExtensionTtsController>::get();
}

ExtensionTtsController::ExtensionTtsController()
    : ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)),
      current_utterance_(NULL),
      platform_impl_(NULL) {
}

ExtensionTtsController::~ExtensionTtsController() {
  FinishCurrentUtterance();
  ClearUtteranceQueue();
}

void ExtensionTtsController::SpeakOrEnqueue(Utterance* utterance) {
  if (IsSpeaking() && utterance->can_enqueue()) {
    utterance_queue_.push(utterance);
  } else {
    Stop();
    SpeakNow(utterance);
  }
}

std::string ExtensionTtsController::GetMatchingExtensionId(
    Utterance* utterance) {
  ExtensionService* service = utterance->profile()->GetExtensionService();
  DCHECK(service);
  ExtensionEventRouter* event_router =
      utterance->profile()->GetExtensionEventRouter();
  DCHECK(event_router);

  const ExtensionList* extensions = service->extensions();
  ExtensionList::const_iterator iter;
  for (iter = extensions->begin(); iter != extensions->end(); ++iter) {
    const Extension* extension = *iter;

    if (!event_router->ExtensionHasEventListener(
            extension->id(), events::kOnSpeak) ||
        !event_router->ExtensionHasEventListener(
            extension->id(), events::kOnStop)) {
      continue;
    }

    const std::vector<Extension::TtsVoice>& tts_voices =
        extension->tts_voices();
    for (size_t i = 0; i < tts_voices.size(); ++i) {
      const Extension::TtsVoice& voice = tts_voices[i];
      if (!voice.voice_name.empty() &&
          !utterance->voice_name().empty() &&
          voice.voice_name != utterance->voice_name()) {
        continue;
      }
      if (!voice.locale.empty() &&
          !utterance->locale().empty() &&
          voice.locale != utterance->locale()) {
        continue;
      }
      if (!voice.gender.empty() &&
          !utterance->gender().empty() &&
          voice.gender != utterance->gender()) {
        continue;
      }

      return extension->id();
    }
  }

  return std::string();
}

void ExtensionTtsController::SpeakNow(Utterance* utterance) {
  std::string extension_id = GetMatchingExtensionId(utterance);
  if (!extension_id.empty()) {
    current_utterance_ = utterance;
    utterance->set_extension_id(extension_id);

    ListValue args;
    args.Set(0, Value::CreateStringValue(utterance->text()));

    // Pass through all options to the speech engine, except for
    // "enqueue", which the speech engine doesn't need to handle.
    DictionaryValue* options = static_cast<DictionaryValue*>(
        utterance->options()->DeepCopy());
    if (options->HasKey(util::kEnqueueKey))
      options->Remove(util::kEnqueueKey, NULL);

    args.Set(1, options);
    args.Set(2, Value::CreateIntegerValue(utterance->id()));
    std::string json_args;
    base::JSONWriter::Write(&args, false, &json_args);

    utterance->profile()->GetExtensionEventRouter()->DispatchEventToExtension(
        extension_id,
        events::kOnSpeak,
        json_args,
        utterance->profile(),
        GURL());

    return;
  }

  GetPlatformImpl()->clear_error();
  bool success = GetPlatformImpl()->Speak(
      utterance->text(),
      utterance->locale(),
      utterance->gender(),
      utterance->rate(),
      utterance->pitch(),
      utterance->volume());
  if (!success) {
    utterance->set_error(GetPlatformImpl()->error());
    utterance->FinishAndDestroy();
    return;
  }
  current_utterance_ = utterance;

  // Check to see if it's still speaking; finish the utterance if not and
  // start polling if so. Checking immediately helps to avoid flaky unit
  // tests by forcing them to set expectations for IsSpeaking.
  CheckSpeechStatus();
}

void ExtensionTtsController::Stop() {
  if (current_utterance_ && !current_utterance_->extension_id().empty()) {
    current_utterance_->profile()->GetExtensionEventRouter()->
        DispatchEventToExtension(
            current_utterance_->extension_id(),
            events::kOnStop,
            "[]",
            current_utterance_->profile(),
            GURL());
  } else {
    GetPlatformImpl()->clear_error();
    GetPlatformImpl()->StopSpeaking();
  }

  if (current_utterance_)
    current_utterance_->set_error(kSpeechInterruptedError);
  FinishCurrentUtterance();
  ClearUtteranceQueue();
}

void ExtensionTtsController::OnSpeechFinished(
    int request_id, const std::string& error_message) {
  // We may sometimes receive completion callbacks "late", after we've
  // already finished the utterance (for example because another utterance
  // interrupted or we got a call to Stop). It's also possible that a buggy
  // extension has called this more than once. In either case it's safe to
  // just ignore this call.
  if (!current_utterance_ || request_id != current_utterance_->id())
    return;

  current_utterance_->set_error(error_message);
  FinishCurrentUtterance();
  SpeakNextUtterance();
}

bool ExtensionTtsController::IsSpeaking() const {
  return current_utterance_ != NULL;
}

void ExtensionTtsController::FinishCurrentUtterance() {
  if (current_utterance_) {
    current_utterance_->FinishAndDestroy();
    current_utterance_ = NULL;
  }
}

void ExtensionTtsController::SpeakNextUtterance() {
  // Start speaking the next utterance in the queue.  Keep trying in case
  // one fails but there are still more in the queue to try.
  while (!utterance_queue_.empty() && !current_utterance_) {
    Utterance* utterance = utterance_queue_.front();
    utterance_queue_.pop();
    SpeakNow(utterance);
  }
}

void ExtensionTtsController::ClearUtteranceQueue() {
  while (!utterance_queue_.empty()) {
    Utterance* utterance = utterance_queue_.front();
    utterance_queue_.pop();
    utterance->set_error(kSpeechRemovedFromQueueError);
    utterance->FinishAndDestroy();
  }
}

void ExtensionTtsController::CheckSpeechStatus() {
  if (!current_utterance_)
    return;

  if (!current_utterance_->extension_id().empty())
    return;

  if (GetPlatformImpl()->IsSpeaking() == false) {
    FinishCurrentUtterance();
    SpeakNextUtterance();
  }

  // If we're still speaking something (either the prevoius utterance or
  // a new utterance), keep calling this method after another delay.
  // TODO(dmazzoni): get rid of this as soon as all platform implementations
  // provide completion callbacks rather than only supporting polling.
  if (current_utterance_ && current_utterance_->extension_id().empty()) {
    MessageLoop::current()->PostDelayedTask(
        FROM_HERE, method_factory_.NewRunnableMethod(
            &ExtensionTtsController::CheckSpeechStatus),
        kSpeechCheckDelayIntervalMs);
  }
}

void ExtensionTtsController::SetPlatformImpl(
    ExtensionTtsPlatformImpl* platform_impl) {
  platform_impl_ = platform_impl;
}

ExtensionTtsPlatformImpl* ExtensionTtsController::GetPlatformImpl() {
  if (!platform_impl_)
    platform_impl_ = ExtensionTtsPlatformImpl::GetInstance();
  return platform_impl_;
}

//
// Extension API functions
//

bool ExtensionTtsSpeakFunction::RunImpl() {
  std::string text;
  EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &text));
  DictionaryValue* options = NULL;
  if (args_->GetSize() >= 2)
    EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(1, &options));
  Task* completion_task = NewRunnableMethod(
      this, &ExtensionTtsSpeakFunction::SpeechFinished);
  utterance_ = new Utterance(profile(), text, options, completion_task);

  AddRef();  // Balanced in SpeechFinished().
  ExtensionTtsController::GetInstance()->SpeakOrEnqueue(utterance_);
  return true;
}

void ExtensionTtsSpeakFunction::SpeechFinished() {
  error_ = utterance_->error();
  bool success = error_.empty();
  SendResponse(success);
  Release();  // Balanced in RunImpl().
}

bool ExtensionTtsStopSpeakingFunction::RunImpl() {
  ExtensionTtsController::GetInstance()->Stop();
  return true;
}

bool ExtensionTtsIsSpeakingFunction::RunImpl() {
  result_.reset(Value::CreateBooleanValue(
      ExtensionTtsController::GetInstance()->IsSpeaking()));
  return true;
}

bool ExtensionTtsSpeakCompletedFunction::RunImpl() {
  int request_id;
  std::string error_message;
  EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &request_id));
  if (args_->GetSize() >= 2)
    EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &error_message));
  ExtensionTtsController::GetInstance()->OnSpeechFinished(
      request_id, error_message);

  return true;
}