// Copyright (c) 2012 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 "chrome/renderer/spellchecker/hunspell_engine.h"

#include <algorithm>
#include <iterator>

#include "base/files/memory_mapped_file.h"
#include "base/metrics/histogram.h"
#include "base/time/time.h"
#include "chrome/common/spellcheck_common.h"
#include "chrome/common/spellcheck_messages.h"
#include "content/public/renderer/render_thread.h"
#include "third_party/hunspell/src/hunspell/hunspell.hxx"

using base::TimeTicks;
using content::RenderThread;

namespace {
  // Maximum length of words we actually check.
  // 64 is the observed limits for OSX system checker.
  const size_t kMaxCheckedLen = 64;

  // Maximum length of words we provide suggestions for.
  // 24 is the observed limits for OSX system checker.
  const size_t kMaxSuggestLen = 24;

  COMPILE_ASSERT(kMaxCheckedLen <= size_t(MAXWORDLEN), MaxCheckedLen_too_long);
  COMPILE_ASSERT(kMaxSuggestLen <= kMaxCheckedLen, MaxSuggestLen_too_long);
}

#if !defined(OS_MACOSX)
SpellingEngine* CreateNativeSpellingEngine() {
  return new HunspellEngine();
}
#endif

HunspellEngine::HunspellEngine()
    : file_(base::kInvalidPlatformFileValue),
      initialized_(false),
      dictionary_requested_(false) {
  // Wait till we check the first word before doing any initializing.
}

HunspellEngine::~HunspellEngine() {
}

void HunspellEngine::Init(base::PlatformFile file) {
  initialized_ = true;
  hunspell_.reset();
  bdict_file_.reset();
  file_ = file;
  // Delay the actual initialization of hunspell until it is needed.
}

void HunspellEngine::InitializeHunspell() {
  if (hunspell_.get())
    return;

  bdict_file_.reset(new base::MemoryMappedFile);

  if (bdict_file_->Initialize(file_)) {
    TimeTicks debug_start_time = base::Histogram::DebugNow();

    hunspell_.reset(
        new Hunspell(bdict_file_->data(), bdict_file_->length()));

    DHISTOGRAM_TIMES("Spellcheck.InitTime",
                     base::Histogram::DebugNow() - debug_start_time);
  } else {
    NOTREACHED() << "Could not mmap spellchecker dictionary.";
  }
}

bool HunspellEngine::CheckSpelling(const base::string16& word_to_check,
                                   int tag) {
  // Assume all words that cannot be checked are valid. Since Chrome can't
  // offer suggestions on them, either, there's no point in flagging them to
  // the user.
  bool word_correct = true;
  std::string word_to_check_utf8(UTF16ToUTF8(word_to_check));

  // Limit the size of checked words.
  if (word_to_check_utf8.length() <= kMaxCheckedLen) {
    // If |hunspell_| is NULL here, an error has occurred, but it's better
    // to check rather than crash.
    if (hunspell_.get()) {
      // |hunspell_->spell| returns 0 if the word is misspelled.
      word_correct = (hunspell_->spell(word_to_check_utf8.c_str()) != 0);
    }
  }

  return word_correct;
}

void HunspellEngine::FillSuggestionList(
    const base::string16& wrong_word,
    std::vector<base::string16>* optional_suggestions) {
  std::string wrong_word_utf8(UTF16ToUTF8(wrong_word));
  if (wrong_word_utf8.length() > kMaxSuggestLen)
    return;

  // If |hunspell_| is NULL here, an error has occurred, but it's better
  // to check rather than crash.
  // TODO(groby): Technically, it's not. We should track down the issue.
  if (!hunspell_.get())
    return;

  char** suggestions = NULL;
  int number_of_suggestions =
      hunspell_->suggest(&suggestions, wrong_word_utf8.c_str());

  // Populate the vector of WideStrings.
  for (int i = 0; i < number_of_suggestions; ++i) {
    if (i < chrome::spellcheck_common::kMaxSuggestions)
      optional_suggestions->push_back(UTF8ToUTF16(suggestions[i]));
    free(suggestions[i]);
  }
  if (suggestions != NULL)
    free(suggestions);
}

bool HunspellEngine::InitializeIfNeeded() {
  if (!initialized_ && !dictionary_requested_) {
    // RenderThread will not exist in test.
    if (RenderThread::Get())
      RenderThread::Get()->Send(new SpellCheckHostMsg_RequestDictionary);
    dictionary_requested_ = true;
    return true;
  }

  // Don't initialize if hunspell is disabled.
  if (file_ != base::kInvalidPlatformFileValue)
    InitializeHunspell();

  return !initialized_;
}

bool HunspellEngine::IsEnabled() {
  return file_ != base::kInvalidPlatformFileValue;
}