/* LanguageTool, a natural language style checker
 * Copyright (C) 2018 Fred Kruse
 *
 * 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.de;

import morfologik.speller.Speller;
import org.apache.commons.lang3.StringUtils;
import org.languagetool.*;
import org.languagetool.rules.*;
import org.languagetool.rules.patterns.PatternToken;
import org.languagetool.rules.patterns.PatternTokenBuilder;
import org.languagetool.rules.spelling.morfologik.MorfologikSpeller;
import org.languagetool.tagging.disambiguation.rules.DisambiguationPatternRule;
import org.languagetool.tools.StringTools;
import org.languagetool.tools.Tools;

import static org.languagetool.rules.patterns.PatternRuleBuilderHelper.posRegex;
import static org.languagetool.rules.patterns.PatternRuleBuilderHelper.tokenRegex;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ResourceBundle;
import java.util.function.Supplier;

/**
 * Checks the compound spelling of infinitive clause (Erweiterter Infinitiv mit zu)
 *
 * @author Fred Kruse
 * @since 4.4
 */
public class CompoundInfinitivRule extends Rule {

  private final LinguServices linguServices;
  private final Language lang;
  private final Supplier<List<DisambiguationPatternRule>> antiPatterns;

  private Speller speller = null;

  private static final List<List<PatternToken>> ANTI_PATTERNS = Arrays.asList(
    //
    // NOTE: antipatterns only work when they cover "zu":
    //
    Arrays.asList(
      token("auf"),
      token("Nummer"),
      token("sicher"),
      token("zu")
    ),
    Arrays.asList(  // "Der diensthabende Kollege hatte ganz schön zu tun."
      token("ganz"),
      token("schön"),
      token("zu")
    ),
    Arrays.asList(  // "Fang dort an zu lesen, wo du aufgehört hast."
      tokenRegex("fang|fängst|fängt|fangt|fangen|fing|fingen"),
      posRegex("ADV.*"),
      token("an"),
      token("zu")
    ),
    Arrays.asList(  // "Hab keine Lust, mir Gedanken darüber zu machen."
      token("Gedanken"),
      tokenRegex("dazu|darüber"),
      token("zu"),
      token("machen")
    ),
    Arrays.asList(  // "um dort die Nacht über zu stehen."
      tokenRegex("Spiel|Tag|Nacht|Morgen|Nachmittag|Abend|Zeit|.+zeit"),
      token("über"),
      token("zu"),
      token("stehen")
    ),
    Arrays.asList(
      token("kurz"),
      token("zu"),
      token("machen")
    ),
    Arrays.asList(
      // "ohne die Erlaubnis dazu zu haben"
      token("dazu"),
      token("zu"),
      token("haben")
    ),
    Arrays.asList(
      // "Er hatte nichts weiter zu sagen"
      tokenRegex("deutlich|viel|Stück|nichts|nix|noch"),
      token("weiter"),
      token("zu")
    ),
    Arrays.asList(
      // "Auf und ab zu schreiten"
      token("auf"),
      tokenRegex("und|&|oder|\\/"),
      new PatternTokenBuilder().posRegex("ADV.*").min(0).build(),
      token("ab"),
      token("zu")
    ),
    Arrays.asList(
      // "Hin und her zu laufen"
      token("hin"),
      tokenRegex("und|&|oder|\\/"),
      new PatternTokenBuilder().posRegex("ADV.*").min(0).build(),
      token("her"),
      token("zu")
    ),
    Arrays.asList(
      // "Hoch und runter zu laufen"
      tokenRegex("rauf|hoch"),
      tokenRegex("und|&|oder|\\/"),
      new PatternTokenBuilder().posRegex("ADV.*").min(0).build(),
      token("runter"),
      token("zu")
    ),
    Arrays.asList(
      // Sie scheint aus Spanien heraus zu kaufen
      new PatternTokenBuilder().token("aus").setSkip(3).build(),
      token("heraus"),
      token("zu")
    )
  );
  
  private static final String[] ADJ_EXCEPTION = {
    "schwer",
    "klar",
    "verloren",
    "bekannt",
    "rot",
    "blau",
    "gelb",
    "grün",
    "schwarz",
    "weiß",
    "fertig",
    "neu"
  };

  private static PatternToken token(String s) {
    return new PatternTokenBuilder().token(s).build();
  }

  public CompoundInfinitivRule(ResourceBundle messages, Language lang, UserConfig userConfig) throws IOException {
    super.setCategory(Categories.COMPOUNDING.getCategory(messages));
    setLocQualityIssueType(ITSIssueType.Misspelling);
    addExamplePair(Example.wrong("Er überprüfte die Rechnungen noch einmal, um ganz <marker>sicher zu gehen</marker>."),
                   Example.fixed("Er überprüfte die Rechnungen noch einmal, um ganz <marker>sicherzugehen</marker>."));
    this.lang = lang;
    if (userConfig != null) {
      linguServices = userConfig.getLinguServices();
    } else {
      linguServices = null;
    }
    setUrl(Tools.getUrl("https://languagetool.org/insights/de/beitrag/zu-zusammen-oder-getrennt/"));
    antiPatterns = cacheAntiPatterns(lang, ANTI_PATTERNS);
  }

  @Override
  public String getId() {
    return "COMPOUND_INFINITIV_RULE";
  }

  @Override
  public String getDescription() {
    return "Erweiterter Infinitiv mit zu (Zusammenschreibung)";
  }

  private static boolean isInfinitiv(AnalyzedTokenReadings token) {
    return token.hasPosTagStartingWith("VER:INF");
  }

  private boolean isMisspelled(String word) {
    word = StringTools.lowercaseFirstChar(word);
    if (linguServices == null && speller != null) {
      return speller.isMisspelled(word);
    } else if (linguServices != null) {
      return !linguServices.isCorrectSpell(word, lang);
    }
    throw new IllegalStateException("LinguServices or Speller must be not null to check spelling in CompoundInfinitivRule");
  }

  private boolean isRelevant(AnalyzedTokenReadings token) {
    return token.hasPosTag("ZUS") && !"um".equalsIgnoreCase(token.getToken());
  }

  private String getLemma(AnalyzedTokenReadings token) {
    if (token != null) {
      List<AnalyzedToken> readings = token.getReadings();
      for (AnalyzedToken reading : readings) {
        String lemma = reading.getLemma();
        if (lemma != null) {
          return lemma;
        }
      }
    }
    return null;
  }
  
  private boolean isException(AnalyzedTokenReadings[] tokens, int n) {
    if (tokens[n - 2].hasPosTagStartingWith("VER")) {
      return true;
    }
    for (String word : ADJ_EXCEPTION) {
      if(tokens[n - 1].getToken().equals(word)) {
        return true;
      }
    }
    if ("sagen".equals(tokens[n + 1].getToken()) &&
            ("weiter".equals(tokens[n - 1].getToken()) || "dazu".equals(tokens[n - 1].getToken()))) {
      return true;
    }
    if (("tragen".equals(tokens[n + 1].getToken()) || "machen".equals(tokens[n + 1].getToken()))
            && "davon".equals(tokens[n - 1].getToken())) {
      return true;
    }
    if ("geben".equals(tokens[n + 1].getToken()) && "daran".equals(tokens[n - 1].getToken())) {
      return true;
    }
    if ("gehen".equals(tokens[n + 1].getToken()) && "ab".equals(tokens[n - 1].getToken())) {
      return true;
    }
    if ("errichten".equals(tokens[n + 1].getToken()) && "wieder".equals(tokens[n - 1].getToken())) {
      return true;
    }
    String verb = null;
    for (int i = n - 2; i > 0 && !isPunctuation(tokens[i].getToken()); i--) {
      if (tokens[i].hasPosTagStartingWith("VER:IMP")) {
        verb = StringUtils.lowerCase(getLemma(tokens[i]));
      } else if (tokens[i].hasPosTagStartingWith("VER")) {
        verb = tokens[i].getToken().toLowerCase();
      } else if ("Fang".equals(tokens[i].getToken())) {
        verb = "fangen";
      }
      if (verb != null) {
        if (!isMisspelled(tokens[n - 1].getToken() + verb)) {
          return true;
        } else {
          break;
        }
      }
    }
    if ("aus".equals(tokens[n - 1].getToken()) || "an".equals(tokens[n - 1].getToken())) {
      for (int i = n - 2; i > 0 && !isPunctuation(tokens[i].getToken()); i--) {
        if ("von".equals(tokens[i].getToken()) || "vom".equals(tokens[i].getToken())) {
          return true;
        }
      }
    }
    if ("her".equals(tokens[n - 1].getToken())) {
      for (int i = n - 2; i > 0 && !isPunctuation(tokens[i].getToken()); i--) {
        if ("vor".equals(tokens[i].getToken())) {
          return true;
        }
      }
    }
    return false;
  }

  @Override
  public List<DisambiguationPatternRule> getAntiPatterns() {
    return antiPatterns.get();
  }

  @Override
  public RuleMatch[] match(AnalyzedSentence sentence) throws IOException {
    if (linguServices == null && speller == null) {
      // speller can not initialized by constructor because of temporary initialization of LanguageTool in other rules,
      // which leads to problems in LO/OO extension
      speller = new Speller(MorfologikSpeller.getDictionaryWithCaching("/de/hunspell/de_DE.dict"));
    }
    List<RuleMatch> ruleMatches = new ArrayList<>();
    AnalyzedTokenReadings[] tokens = getSentenceWithImmunization(sentence).getTokensWithoutWhitespace();
    for (int i = 2; i < tokens.length - 1; i++) {
      if ("zu".equals(tokens[i].getToken())
        && isInfinitiv(tokens[i + 1])
        && isRelevant(tokens[i - 1])
        && !tokens[i].isImmunized()
        && !isException(tokens, i)
        && !isMisspelled(tokens[i - 1].getToken() + tokens[i + 1].getToken())) {
        String msg = "Wenn der erweiterte Infinitiv von dem Verb '" + tokens[i - 1].getToken() + tokens[i + 1].getToken()
                   + "' abgeleitet ist, sollte er zusammengeschrieben werden.";
        RuleMatch ruleMatch = new RuleMatch(this, sentence, tokens[i - 1].getStartPos(), tokens[i + 1].getEndPos(), msg);
        List<String> suggestions = new ArrayList<>();
        suggestions.add(tokens[i - 1].getToken() + tokens[i].getToken() + tokens[i + 1].getToken());
        ruleMatch.setSuggestedReplacements(suggestions);
        ruleMatches.add(ruleMatch);
      }
    }
    return toRuleMatchArray(ruleMatches);
  }

  private boolean isPunctuation(String word) {
    return word != null
           && word.length() == 1
           && StringUtils.equalsAny(word, ".", "?", "!", "…", ":", ";", ",", "(", ")", "[", "]");
  }
}
