class MorfologikUkrainianSpellerRule extends MorfologikSpellerRule {

  private static final String ABBREVIATION_CHAR = ".";
  private static final String RESOURCE_FILENAME = "/uk/hunspell/uk_UA.dict";
  private static final Pattern UKRAINIAN_LETTERS = Pattern.compile(".*[а-яіїєґА-ЯІЇЄҐ].*");
  private static final Pattern DO_NOT_SUGGEST_SPACED_PATTERN = Pattern.compile(
        "(авіа|авто|анти|аудіо|відео|водо|гідро|екстра|квазі|кіно|лже|мета|моно|мото|псевдо|пост|радіо|стерео|супер|ультра|фото) .*");
//  private static final Pattern INFIX_PATTERN = Pattern.compile("-[а-яіїєґ]{1,5}-");
  private static final Pattern PATTERN = Pattern.compile("[А-ЯІЇЄҐ]");
  private static final Map<String, String> dashPrefixes2019;

  static {
    dashPrefixes2019 = ExtraDictionaryLoader.loadMap("/uk/dash_prefixes.txt");
    dashPrefixes2019.entrySet().removeIf(entry -> entry.getValue().matches(":(ua_1992|bad|alt|slang)") || ! entry.getKey().matches("[а-яіїєґ]{3,}") );
  }

  public MorfologikUkrainianSpellerRule(ResourceBundle messages,
                                        Language language, UserConfig userConfig, List<Language> altLanguages) throws IOException {
    super(messages, language, userConfig, altLanguages);
//    setCheckCompound(true);
  }

  @Override
  public String getFileName() {
    return RESOURCE_FILENAME;
  }

  @Override
  public String getId() {
    return "MORFOLOGIK_RULE_UK_UA";
  }
  
  @Override
  protected List<RuleMatch> getRuleMatches(String word, int startPos, AnalyzedSentence sentence,
      List<RuleMatch> ruleMatchesSoFar, int idx, AnalyzedTokenReadings[] tokens) throws IOException {
    List<RuleMatch> ruleMatches = super.getRuleMatches(word, startPos, sentence, ruleMatchesSoFar, idx, tokens);

    // disambig may remove v_kly leaving token with no good tags
    if( ruleMatches.isEmpty() && ! hasGoodTag(tokens[idx]) ) {
      ruleMatches.add(new RuleMatch(this, sentence, startPos, startPos+word.length(), startPos, startPos+word.length(), 
          "Потенційна орфографічна помилка", "Орфографічна помилка", false, ""));
    }
    return ruleMatches;
  }
  
  @Override
  protected boolean isMisspelled(MorfologikMultiSpeller speller, String word) {
    if( word.endsWith("-") ) {
      return !word.startsWith("-");
    }

    // we're tokenizing them separately now
//    if( word.endsWith("²") || word.endsWith("³") ) {
//      word = word.substring(0, word.length() - 1); 
//    }

    // in some places disambiguator may leave tokens with ignored characters
    // so we have to ignore them here
    // we fixed "filter" action but if this problem still happens we can uncomment code below
//    Matcher matcher = language.getIgnoredCharactersRegex().matcher(word);
//    if( matcher.find() ) {
//      word = matcher.replaceAll("");
//    }
    
    return super.isMisspelled(speller, word);
  }

  @Override
  protected List<SuggestedReplacement> getAdditionalSuggestions(List<SuggestedReplacement> suggestions, String word) {
    boolean isCapitalized = StringTools.isCapitalizedWord(word);
    if( isCapitalized ) {
      word = word.toLowerCase();
    }
    for(String key: dashPrefixes2019.keySet()) {
      if( word.startsWith(key) 
          && word.length() > key.length() + 2 
          && word.charAt(key.length()) != '-' ) {
        String second = word.substring(key.length());
        suggestions.add(new SuggestedReplacement(key + "-" + second));
      }
    }
    return suggestions;
  }
  
  @Override
  protected boolean ignoreToken(AnalyzedTokenReadings[] tokens, int idx) throws IOException {
    String word = tokens[idx].getToken();

    // don't check words that don't have Ukrainian letters
    if( ! UKRAINIAN_LETTERS.matcher(word).matches() )
      return true;

    if( super.ignoreToken(tokens, idx) )
      return true;

    if( idx < tokens.length - 1 && tokens[idx+1].getToken().equals(ABBREVIATION_CHAR) ) {
      if( super.ignoreWord(word + ABBREVIATION_CHAR) ) {
        return true;
      }
      if(PATTERN.matcher(word).matches()) {  //TODO: only do this for initials when last name is followed
        return true;
      }
    }
    
//    if( word.contains("-") || word.contains("\u2011") || word.endsWith(".") 
//            || word.equalsIgnoreCase("раза") ) {
      return hasGoodTag(tokens[idx]); // && ! PosTagHelper.hasPosTagPart(tokens[idx], ":bad");
//    }
//
//    return false;
  }

  private static boolean hasGoodTag(AnalyzedTokenReadings tokens) {
    for (AnalyzedToken analyzedToken : tokens) {
      String posTag = analyzedToken.getPOSTag();
      if( posTag != null 
            && ! posTag.equals(JLanguageTool.SENTENCE_END_TAGNAME) 
            && ! posTag.equals(JLanguageTool.PARAGRAPH_END_TAGNAME) ) 
//            && (! posTag.contains(IPOSTag.bad.getText()) || posTag.contains(":latin"))  
//            && ! (posTag.contains(":inanim") && posTag.contains(":v_kly")) )
        return true;
    }
    return false;
  }

  @Override
  protected List<SuggestedReplacement> filterSuggestions(List<SuggestedReplacement> suggestions) {<FILL_FUNCTION_BODY>}

  // workaround to allow other rules generate spelling suggestions without invoking match()
  MorfologikMultiSpeller getSpeller1() {
    if( speller1 == null ) {
      try {
        // we can't call initSpellers() as it's private so we're calling method we can
        isMisspelled("1");
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
    return speller1;
  }

  @Override
  protected boolean isLatinScript() {
    return false;
  }
}

    suggestions = super.filterSuggestions(suggestions);
    // do not suggest "кіно прокат, вело- прогулянка...":
    suggestions.removeIf(item -> item.getReplacement().contains(" ") &&
        DO_NOT_SUGGEST_SPACED_PATTERN.matcher(item.getReplacement()).matches() ||
        item.getReplacement().contains("- "));
    return suggestions;
  
<methods>public void <init>(java.util.ResourceBundle, org.languagetool.Language) throws java.io.IOException,public void <init>(java.util.ResourceBundle, org.languagetool.Language, org.languagetool.UserConfig) throws java.io.IOException,public void <init>(java.util.ResourceBundle, org.languagetool.Language, org.languagetool.UserConfig, List<org.languagetool.Language>) throws java.io.IOException,public void <init>(java.util.ResourceBundle, org.languagetool.Language, org.languagetool.GlobalConfig, org.languagetool.UserConfig, List<org.languagetool.Language>, org.languagetool.languagemodel.LanguageModel, org.languagetool.Language) throws java.io.IOException,public java.lang.String getDescription() ,public abstract java.lang.String getFileName() ,public abstract java.lang.String getId() ,public List<java.lang.String> getSpellingSuggestions(java.lang.String) throws java.io.IOException,public boolean isMisspelled(java.lang.String) throws java.io.IOException,public org.languagetool.rules.RuleMatch[] match(org.languagetool.AnalyzedSentence) throws java.io.IOException,public void setIgnoreTaggedWords() ,public void setLocale(java.util.Locale) ,public java.util.regex.Pattern tokenizingPattern() <variables>static final int MAX_FREQUENCY_FOR_SPLITTING,private boolean checkCompound,private java.util.regex.Pattern compoundRegex,protected java.util.Locale conversionLocale,protected final non-sealed org.languagetool.GlobalConfig globalConfig,private boolean ignoreTaggedWords,private static final Logger logger,protected final non-sealed org.languagetool.Language motherTongue,private static final java.util.regex.Pattern pStartsWithNumbersBullets,private static final java.util.regex.Pattern pStartsWithNumbersBulletsExceptions,protected org.languagetool.rules.spelling.morfologik.MorfologikMultiSpeller speller1,protected org.languagetool.rules.spelling.morfologik.MorfologikMultiSpeller speller2,protected org.languagetool.rules.spelling.morfologik.MorfologikMultiSpeller speller3,private final non-sealed org.languagetool.UserConfig userConfig