/* LanguageTool, a natural language style checker
 * Copyright (C) 2012 Marcin Miłkowski (http://www.languagetool.org)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 */
package org.languagetool.rules.spelling.morfologik;

import com.google.common.cache.*;
import morfologik.speller.Speller;
import morfologik.stemming.Dictionary;
import org.jetbrains.annotations.NotNull;
import org.languagetool.JLanguageTool;
import org.languagetool.broker.ResourceDataBroker;
import org.languagetool.rules.patterns.StringMatcher;
import org.languagetool.rules.spelling.SpellingCheckRule;
import org.languagetool.tools.StringTools;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Morfologik-based spell checker.
 */
public class MorfologikSpeller {
  
  // Speed up the server use case, where rules get initialized for every call.
  // See https://github.com/morfologik/morfologik-stemming/issues/69 for confirmation that
  // Dictionary is thread-safe:
  private static final LoadingCache<String, Dictionary> dictCache = CacheBuilder.newBuilder()
      //.maximumSize(0)
      .expireAfterWrite(10, TimeUnit.MINUTES)
      .build(new CacheLoader<String, Dictionary>() {
        @Override
        public Dictionary load(@NotNull String fileInClassPath) throws IOException {
          ResourceDataBroker dataBroker = JLanguageTool.getDataBroker();
          if (dataBroker.resourceExists(fileInClassPath)) {
            return Dictionary.read(dataBroker.getFromResourceDirAsUrl(fileInClassPath));
          } else {
            return Dictionary.read(Paths.get(fileInClassPath));
          }
        }
      });

  private final Dictionary dictionary;
  private final Speller speller;
  private final int maxEditDistance;

  /**
   * Creates a speller with the given maximum edit distance.
   * @param fileInClassPath path in classpath to morfologik dictionary
   */
  public MorfologikSpeller(String fileInClassPath, int maxEditDistance) {
    this(getDictionaryWithCaching(fileInClassPath), maxEditDistance);
  }

  /**
   * Creates a speller with a maximum edit distance of one.
   * @param fileInClassPath path in classpath to morfologik dictionary
   */
  public MorfologikSpeller(String fileInClassPath) throws IOException {
    this(fileInClassPath, 1);
  }

  /** @since 2.9 */
  MorfologikSpeller(Dictionary dictionary, int maxEditDistance) {
    if (maxEditDistance <= 0) {
      throw new RuntimeException("maxEditDistance must be > 0: " + maxEditDistance);
    }
    this.dictionary = dictionary;
    this.maxEditDistance = maxEditDistance;
    speller = new Speller(dictionary, maxEditDistance);
  }

  /**
   * Load a dictionary from the given path or reuse a cached instance, if present.
   * @param fileInClassPath path in classpath to morfologik dictionary
   */
  public static Dictionary getDictionaryWithCaching(@NotNull String fileInClassPath) {
    return dictCache.getUnchecked(fileInClassPath);
  }

  public boolean isMisspelled(String word) {
    if (word.isEmpty() || SpellingCheckRule.LANGUAGETOOL.equals(word) || SpellingCheckRule.LANGUAGETOOLER.equals(word)) {
      return false;
    }
    synchronized (this) {
      return speller.isMisspelled(word);
    }
  }

  public synchronized List<String> findReplacements(String word) {
    return speller.findReplacements(word);
  }
  
  public synchronized List<String> findSimilarWords(String word) {
    return speller.findSimilarWords(word);
  }

  /**
   * @deprecated use (or introduce) other methods to this class which would take care of the necessary synchronization
   */
  @Deprecated
  public Speller getSpeller() {
    return speller;
  }

  public List<WeightedSuggestion> getSuggestions(String word) {
    List<WeightedSuggestion> suggestions = new ArrayList<>();
    if (word.length() > StringMatcher.MAX_MATCH_LENGTH) {
      return suggestions;
    }
    // needs to be reset every time, possible bug: HMatrix for distance computation is not reset;
    // output changes when reused
    Speller speller = new Speller(dictionary, maxEditDistance);
    List<Speller.CandidateData> replacementCandidates;
    if (word.length() < 50) {   // slow for long words (the limit is arbitrary)
      replacementCandidates = speller.findReplacementCandidates(word);
      for (Speller.CandidateData candidate : replacementCandidates) {
        suggestions.add(new WeightedSuggestion(candidate.getWord(), candidate.getDistance()));
      }
    }
    List<Speller.CandidateData> runOnCandidates = speller.replaceRunOnWordCandidates(word);
    for (Speller.CandidateData runOnCandidate : runOnCandidates) {
      suggestions.add(new WeightedSuggestion(runOnCandidate.getWord(), runOnCandidate.getDistance()));
    }
    
    // all upper-case suggestions if necessary
    if (dictionary.metadata.isConvertingCase() && StringTools.isAllUppercase(word)) {
      for (int i = 0; i < suggestions.size(); i++) {
        WeightedSuggestion sugg = suggestions.get(i);
        String allUppercase = sugg.getWord().toUpperCase();
        // do not use capitalized word if it matches the original word or it's mixed case
        if (allUppercase.equals(word) || StringTools.isMixedCase(suggestions.get(i).getWord())) {
          allUppercase = sugg.getWord();
        }
        // remove duplicates
        int auxIndex = getSuggestionIndex(suggestions, allUppercase);
        if (auxIndex > i) {
          suggestions.remove(auxIndex);
        }
        if (auxIndex > -1 && auxIndex < i) {
          suggestions.remove(i);
          i--;
        } else {
          suggestions.set(i, new WeightedSuggestion(allUppercase, sugg.getWeight()));
        }
      }
    }
    // capitalize suggestions if necessary
    else if (dictionary.metadata.isConvertingCase() && StringTools.startsWithUppercase(word)) {
      for (int i = 0; i < suggestions.size(); i++) {
        WeightedSuggestion sugg = suggestions.get(i);
        String uppercaseFirst = StringTools.uppercaseFirstChar(sugg.getWord());
        // do not use capitalized word if it matches the original word or it's mixed case
        if (uppercaseFirst.equals(word) || StringTools.isMixedCase(suggestions.get(i).getWord())) {
          uppercaseFirst = sugg.getWord();
        }
        // remove capitalized duplicates
        int auxIndex = getSuggestionIndex(suggestions, uppercaseFirst);
        if (auxIndex > i) {
          suggestions.remove(auxIndex);
        }
        if (auxIndex > -1 && auxIndex < i) {
          suggestions.remove(i);
          i--;
        } else {
          suggestions.set(i, new WeightedSuggestion(uppercaseFirst, sugg.getWeight()));
        }
      }
    }
    return suggestions;
  }

  private int getSuggestionIndex(List<WeightedSuggestion> suggestions, String uppercaseFirst) {
    int i = 0;
    for (WeightedSuggestion suggestion : suggestions) {
      if (suggestion.getWord().equals(uppercaseFirst)) {
        return i;
      }
      i++;
    }
    return -1;
  }

  /**
   * Determines whether the dictionary uses case conversions.
   * @return True when the speller uses spell conversions.
   * @since 2.5
   */
  public boolean convertsCase() {
    return speller.convertsCase();
  }

  @Override
  public String toString() {
    return "dist=" + maxEditDistance;
  }

  public synchronized int getFrequency(String word) {
    int freq = speller.getFrequency(word);
    if (freq == 0 && !word.equals(word.toLowerCase())) {
      freq = speller.getFrequency(word.toLowerCase());
    }
    return freq;
  }
}
