/* LanguageTool, a natural language style checker
 * Copyright (C) 2009 Marcin Miłkowski
 *
 * 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.synthesis;

import morfologik.stemming.Dictionary;
import morfologik.stemming.DictionaryLookup;
import morfologik.stemming.IStemmer;
import morfologik.stemming.WordData;
import org.languagetool.AnalyzedToken;
import org.languagetool.JLanguageTool;
import org.languagetool.Language;
import org.languagetool.rules.spelling.morfologik.MorfologikSpeller;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

public class BaseSynthesizer implements Synthesizer {

  public final String SPELLNUMBER_TAG = "_spell_number_";
  public final String SPELLNUMBER_FEMININE_TAG = "_spell_number_:feminine";
  public final String SPELLNUMBER_ROMAN_TAG = "_spell_number_:Roman";

  protected volatile List<String> possibleTags;

  private final String tagFileName;
  private final String resourceFileName;
  private final IStemmer stemmer;
  private final ManualSynthesizer manualSynthesizer;
  private final ManualSynthesizer removalSynthesizer;
  private final ManualSynthesizer removalSynthesizer2;
  private final String sorosFileName;
  private final Soros numberSpeller;
  private final Soros romanNumberer;
  
  private volatile Dictionary dictionary;

  /**
   * @param resourceFileName The dictionary file name.
   * @param tagFileName The name of a file containing all possible tags.
   * @deprecated use {@link #BaseSynthesizer(String, String, String, String)}
   */
  public BaseSynthesizer(String sorosFileName, String resourceFileName, String tagFileName, Language lang) {
    this(sorosFileName, resourceFileName, tagFileName, lang.getShortCode());
  }

  /**
   * @param resourceFileName The dictionary file name.
   * @param tagFileName The name of a file containing all possible tags.
   * @param langShortCode the language short code used to find the data files
   */
  public BaseSynthesizer(String sorosFileName, String resourceFileName, String tagFileName, String langShortCode) {
    this.resourceFileName = resourceFileName;
    this.tagFileName = tagFileName;
    this.stemmer = createStemmer();
    this.sorosFileName = sorosFileName;
    this.numberSpeller = createNumberSpeller(langShortCode);
    this.romanNumberer = createRomanNumberer();
    try {
      String path = "/" + langShortCode + "/added.txt";
      if (JLanguageTool.getDataBroker().resourceExists(path)) {
        try (InputStream stream = JLanguageTool.getDataBroker().getFromResourceDirAsStream(path)) {
          this.manualSynthesizer = new ManualSynthesizer(stream);
        }
      } else {
        this.manualSynthesizer = null;
      }
      String removalPath = "/" + langShortCode + "/removed.txt";
      if (JLanguageTool.getDataBroker().resourceExists(removalPath)) {
        try (InputStream stream = JLanguageTool.getDataBroker().getFromResourceDirAsStream(removalPath)) {
          this.removalSynthesizer = new ManualSynthesizer(stream);
        }
      } else {
        this.removalSynthesizer = null;
      }
      String removalPath2 = "/" + langShortCode + "/do-not-synthesize.txt";
      if (JLanguageTool.getDataBroker().resourceExists(removalPath2)) {
        try (InputStream stream = JLanguageTool.getDataBroker().getFromResourceDirAsStream(removalPath2)) {
          this.removalSynthesizer2 = new ManualSynthesizer(stream);
        }
      } else {
        this.removalSynthesizer2 = null;
      }
      
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * @deprecated use {@link #BaseSynthesizer(String, String, String)}
   */
  public BaseSynthesizer(String resourceFileName, String tagFileName, Language lang) {
    this(resourceFileName, tagFileName, lang.getShortCode());
  }

  public BaseSynthesizer(String resourceFileName, String tagFileName, String langShortCode) {
    this(null, resourceFileName, tagFileName, langShortCode);
  }

  /**
   * Returns the {@link Dictionary} used for this synthesizer.
   * The dictionary file can be defined in the {@link #BaseSynthesizer(String, String, Language) constructor}.
   * @throws IOException In case the dictionary cannot be loaded.
   */
  protected Dictionary getDictionary() throws IOException {
    Dictionary dict = this.dictionary;
    if (dict == null) {
      synchronized (this) {
        dict = this.dictionary;
        if (dict == null) {
          dictionary = dict = MorfologikSpeller.getDictionaryWithCaching(resourceFileName);
        }
      }
    }
    return dict;
  }

  /**
   * Creates a new {@link IStemmer} based on the configured {@link #getDictionary() dictionary}.
   * The result must not be shared among threads.
   * @since 2.3
   */
  protected IStemmer createStemmer() {
    try {
      return new DictionaryLookup(getDictionary());
    } catch (IOException e) {
      throw new RuntimeException("Could not load dictionary", e);
    }
  }
  
  private Soros createNumberSpeller(String langcode) {
    Soros s;
    try {
      URL url = JLanguageTool.getDataBroker().getFromResourceDirAsUrl(sorosFileName);
      BufferedReader f = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8));
      StringBuilder sb = new StringBuilder();
      String line;
      while ((line = f.readLine()) != null) {
        sb.append(line);
        sb.append('\n');
      }
      s = new Soros(new String(sb), langcode);
    } catch (Exception e) {
      return null;
    }
    return s;
  }
  
  private Soros createRomanNumberer() {
    Soros s;
    try {
      URL url = JLanguageTool.getDataBroker().getFromResourceDirAsUrl("Roman.sor");
      BufferedReader f = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8));
      StringBuilder sb = new StringBuilder();
      String line;
      while ((line = f.readLine()) != null) {
        sb.append(line);
        sb.append('\n');
      }
      s = new Soros(new String(sb), "Roman");
    } catch (Exception e) {
      return null;
    }
    return s;
  }

  /**
   * Lookup the inflected forms of a lemma defined by a part-of-speech tag.
   * @param lemma the lemma to be inflected.
   * @param posTag the desired part-of-speech tag.
   */
  protected List<String> lookup(String lemma, String posTag) {
    List<String> results = new ArrayList<>();
    synchronized (this) { // the stemmer is not thread-safe
      List<WordData> wordForms = stemmer.lookup(lemma + "|" + posTag);
      for (WordData wd : wordForms) {
        results.add(wd.getStem().toString());
      }
    }
    if (manualSynthesizer != null) {
      List<String> manualForms = manualSynthesizer.lookup(lemma, posTag);
      if (manualForms != null) {
        results.addAll(manualForms);
      }
    }
    if (removalSynthesizer != null) {
      List<String> removeForms = removalSynthesizer.lookup(lemma, posTag);
      if (removeForms != null) {
        results.removeAll(removeForms);
      }
    }
    if (removalSynthesizer2 != null) {
      List<String> removeForms = removalSynthesizer2.lookup(lemma, posTag);
      if (removeForms != null) {
        results.removeAll(removeForms);
      }
    }
    return results;
  }

  /**
   * Get a form of a given AnalyzedToken, where the form is defined by a
   * part-of-speech tag.
   * @param token AnalyzedToken to be inflected.
   * @param posTag The desired part-of-speech tag.
   * @return inflected words, or an empty array if no forms were found
   */
  @Override
  public String[] synthesize(AnalyzedToken token, String posTag) throws IOException {
    if (posTag.equals(SPELLNUMBER_TAG)) {
      return new String[] {getSpelledNumber(token.getToken())};
    }
    if (posTag.equals(SPELLNUMBER_FEMININE_TAG)) {
      return new String[] {getSpelledNumber("feminine " + token.getToken())};
    }
    if (posTag.equals(SPELLNUMBER_ROMAN_TAG)) {
      return new String[] {getRomanNumber(token.getToken())};
    }
    List<String> wordForms = lookup(token.getLemma(), posTag);
    return removeExceptions(wordForms.toArray(new String[0]));
  }

  @Override
  public String[] synthesize(AnalyzedToken token, String posTag, boolean posTagRegExp) throws IOException {
    if (posTagRegExp) {
      try {
        Pattern p = Pattern.compile(posTag);
        return synthesizeForPosTags(token.getLemma(), tag -> p.matcher(tag).matches());
      } catch (PatternSyntaxException e) {
        throw new RuntimeException("Error trying to synthesize POS tag " + posTag +
                " (posTagRegExp: true) from token " + token.getToken(), e);
      }
    }
    return removeExceptions(synthesize(token, posTag));
  }

  /**
   * Synthesize forms for the given lemma and for all POS tags satisfying the given predicate.
   * @since 5.3
   */
  public String[] synthesizeForPosTags(String lemma, Predicate<String> acceptTag) throws IOException {
    initPossibleTags();
    List<String> results = new ArrayList<>();
    for (String tag : possibleTags) {
      if (acceptTag.test(tag)) {
        results.addAll(lookup(lemma, tag));
      }
    }
    return removeExceptions(results.toArray(new String[0]));
  }

  @Override
  public String getPosTagCorrection(String posTag) {
    return posTag;
  }

  /**
   * @since 2.5
   * @return the stemmer interface to be used.
   */
  public IStemmer getStemmer() {
    return stemmer;
  }

  protected void initPossibleTags() throws IOException {
    if (possibleTags == null) {
      synchronized (this) {
        if (possibleTags == null) {
          possibleTags = loadTags();
        }
      }
    }
  }

  private List<String> loadTags() throws IOException {
    List<String> tags;
    try (InputStream stream = JLanguageTool.getDataBroker().getFromResourceDirAsStream(tagFileName)) {
      tags = SynthesizerTools.loadWords(stream);
    }
    if (manualSynthesizer != null) {
      for (String tag : manualSynthesizer.getPossibleTags()) {
        if (!tags.contains(tag)) {
          tags.add(tag);
        }
      }
    }
    return tags;
  }

  @Override
  public String getSpelledNumber(String arabicNumeral) {
    if (numberSpeller != null) {
      return numberSpeller.run(arabicNumeral);
    }
    return arabicNumeral;
  }
  
  public String getRomanNumber(String arabicNumeral) {
    if (romanNumberer != null) {
      return romanNumberer.run(arabicNumeral);
    }
    return arabicNumeral;
  }

  protected boolean isException(String w) {
    return false;  
  }

  protected String[] removeExceptions(String[] words) {
    List<String> results = new ArrayList<>();
    for (String word : words) {
      if (!isException(word)) {
        results.add(word);
      }
    }
    return results.toArray(new String[0]);
  }

  @Override
  public String getTargetPosTag(List<String> posTags, String targetPosTag) {
    if (posTags.isEmpty()) {
      return targetPosTag;
    }
    // return the last one to keep the previous results
    return posTags.get(posTags.size() - 1);
  }

}
