// Copyright (c) 2006-2008 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/tools/convert_dict/dic_reader.h"

#include <algorithm>
#include <set>

#include "base/file_util.h"
#include "base/strings/string_util.h"
#include "chrome/tools/convert_dict/aff_reader.h"
#include "chrome/tools/convert_dict/hunspell_reader.h"

namespace convert_dict {

namespace {

// Maps each unique word to the unique affix group IDs associated with it.
typedef std::map<std::string, std::set<int> > WordSet;

void SplitDicLine(const std::string& line, std::vector<std::string>* output) {
  // We split the line on a slash not preceded by a backslash. A slash at the
  // beginning of the line is not a separator either.
  size_t slash_index = line.size();
  for (size_t i = 0; i < line.size(); i++) {
    if (line[i] == '/' && i > 0 && line[i - 1] != '\\') {
      slash_index = i;
      break;
    }
  }

  output->clear();

  // Everything before the slash index is the first term. We also need to
  // convert all escaped slashes ("\/" sequences) to regular slashes.
  std::string word = line.substr(0, slash_index);
  ReplaceSubstringsAfterOffset(&word, 0, "\\/", "/");
  output->push_back(word);

  // Everything (if anything) after the slash is the second.
  if (slash_index < line.size() - 1)
    output->push_back(line.substr(slash_index + 1));
}

// This function reads words from a .dic file, or a .dic_delta file. Note that
// we read 'all' the words in the file, irrespective of the word count given
// in the first non empty line of a .dic file. Also note that, for a .dic_delta
// file, the first line actually does _not_ have the number of words. In order
// to control this, we use the |file_has_word_count_in_the_first_line|
// parameter to tell this method whether the first non empty line in the file
// contains the number of words or not. If it does, skip the first line. If it
// does not, then the first line contains a word.
bool PopulateWordSet(WordSet* word_set, FILE* file, AffReader* aff_reader,
                     const char* file_type, const char* encoding,
                     bool file_has_word_count_in_the_first_line) {
  int line_number = 0;
  while (!feof(file)) {
    std::string line = ReadLine(file);
    line_number++;
    StripComment(&line);
    if (line.empty())
      continue;

    if (file_has_word_count_in_the_first_line) {
      // Skip the first nonempty line, this is the line count. We don't bother
      // with it and just read all the lines.
      file_has_word_count_in_the_first_line = false;
      continue;
    }

    std::vector<std::string> split;
    SplitDicLine(line, &split);
    if (split.empty() || split.size() > 2) {
      printf("Line %d has extra slashes in the %s file\n", line_number,
             file_type);
      return false;
    }

    // The first part is the word, the second (optional) part is the affix. We
    // always use UTF-8 as the encoding to simplify life.
    std::string utf8word;
    std::string encoding_string(encoding);
    if (encoding_string == "UTF-8") {
      utf8word = split[0];
    } else if (!aff_reader->EncodingToUTF8(split[0], &utf8word)) {
      printf("Unable to convert line %d from %s to UTF-8 in the %s file\n",
             line_number, encoding, file_type);
      return false;
    }

    // We always convert the affix to an index. 0 means no affix.
    int affix_index = 0;
    if (split.size() == 2) {
      // Got a rule, which is the stuff after the slash. The line may also have
      // an optional term separated by a tab. This is the morphological
      // description. We don't care about this (it is used in the tests to
      // generate a nice dump), so we remove it.
      size_t split1_tab_offset = split[1].find('\t');
      if (split1_tab_offset != std::string::npos)
        split[1] = split[1].substr(0, split1_tab_offset);

      if (aff_reader->has_indexed_affixes())
        affix_index = atoi(split[1].c_str());
      else
        affix_index = aff_reader->GetAFIndexForAFString(split[1]);
    }

    // Discard the morphological description if it is attached to the first
    // token. (It is attached to the first token if a word doesn't have affix
    // rules.)
    size_t word_tab_offset = utf8word.find('\t');
    if (word_tab_offset != std::string::npos)
      utf8word = utf8word.substr(0, word_tab_offset);

    WordSet::iterator found = word_set->find(utf8word);
    std::set<int> affix_vector;
    affix_vector.insert(affix_index);

    if (found == word_set->end())
      word_set->insert(std::make_pair(utf8word, affix_vector));
    else
      found->second.insert(affix_index);
  }

  return true;
}

}  // namespace

DicReader::DicReader(const base::FilePath& path) {
  file_ = base::OpenFile(path, "r");

  base::FilePath additional_path =
      path.ReplaceExtension(FILE_PATH_LITERAL("dic_delta"));
  additional_words_file_ = base::OpenFile(additional_path, "r");

  if (additional_words_file_)
    printf("Reading %" PRFilePath " ...\n", additional_path.value().c_str());
  else
    printf("%" PRFilePath " not found.\n", additional_path.value().c_str());
}

DicReader::~DicReader() {
  if (file_)
    base::CloseFile(file_);
  if (additional_words_file_)
    base::CloseFile(additional_words_file_);
}

bool DicReader::Read(AffReader* aff_reader) {
  if (!file_)
    return false;

  WordSet word_set;

  // Add words from the dic file to the word set.
  // Note that the first line is the word count in the file.
  if (!PopulateWordSet(&word_set, file_, aff_reader, "dic",
                       aff_reader->encoding(), true))
    return false;

  // Add words from the .dic_delta file to the word set, if it exists.
  // The first line is the first word to add. Word count line is not present.
  // NOTE: These additional words should be encoded as UTF-8.
  if (additional_words_file_ != NULL) {
    PopulateWordSet(&word_set, additional_words_file_, aff_reader, "dic delta",
                    "UTF-8", false);
  }
  // Make sure the words are sorted, they may be unsorted in the input.
  for (WordSet::iterator word = word_set.begin(); word != word_set.end();
       ++word) {
    std::vector<int> affixes;
    for (std::set<int>::iterator aff = word->second.begin();
         aff != word->second.end(); ++aff)
      affixes.push_back(*aff);

    // Double check that the affixes are sorted. This isn't strictly necessary
    // but it's nice for the file to have a fixed layout.
    std::sort(affixes.begin(), affixes.end());
    std::reverse(affixes.begin(), affixes.end());
    words_.push_back(std::make_pair(word->first, affixes));
  }

  // Double-check that the words are sorted.
  std::sort(words_.begin(), words_.end());
  return true;
}

}  // namespace convert_dict