class RepeatedPatternRule extends TextLevelRule {

    protected final Language ruleLanguage;
    
    RepeatedPatternRule(List<AbstractPatternRule> rules, Language lang) {
      this.rules = Collections.unmodifiableList(rules);
      this.ruleLanguage = lang;
      setPremium(rules.stream().anyMatch(r -> r.isPremium()));
    }

    private final List<AbstractPatternRule> rules;

    public List<AbstractPatternRule> getWrappedRules() {
      return rules;
    }

    @Override
    public String getId() {
      return rules.get(0).getId();
    }

    @Override
    public String getDescription() {
      return rules.get(0).getDescription();
    }
    
    @Override
    public RuleMatch[] match(List<AnalyzedSentence> sentences) throws IOException {<FILL_FUNCTION_BODY>}

    @Override
    public int minToCheckParagraph() {
      // TODO: what should we use here? calculate based on min_prev_matches?
      return 0;
    }

    @Override
    public boolean supportsLanguage(Language language) {
      return language.equalsConsiderVariantsIfSpecified(this.ruleLanguage);
    }

    private boolean isDistanceValid(List<Integer> distancesBetweenMatches, int maxDistanceTokens, int minPrevMatches) {
      int i = 0;
      int size = distancesBetweenMatches.size();
      int distance = 0;
      while (i < minPrevMatches && i < size) {
        distance += distancesBetweenMatches.get(size - 1 - i);
        i++;
      }
      return distance < maxDistanceTokens;
    }

  }

      List<RuleMatch> matches = new ArrayList<>();
      int offsetChars = 0;
      int offsetTokens = 0;
      int prevFromToken = 0;
      int prevMatches = 0;
      List<Integer> distancesBetweenMatches = new ArrayList<>();
      // we need to adjust offsets since each pattern rule returns offsets relative to the sentence, not text
      for (AnalyzedSentence s : sentences) {
        List<RuleMatch> sentenceMatches = new ArrayList<>();
        for (AbstractPatternRule rule : rules) {
          RuleMatch[] ruleMatches = rule.match(s);
          sentenceMatches.addAll(Arrays.asList(ruleMatches));
        }
        sentenceMatches = new SameRuleGroupFilter().filter(sentenceMatches);
        // no sorting: SameRuleGroupFilter sorts rule matches already
        int sentenceLenghtTokens = s.getTokensWithoutWhitespace().length;
        for (RuleMatch m : sentenceMatches) {
          int fromToken = 0;
          while (fromToken < sentenceLenghtTokens
              && s.getTokensWithoutWhitespace()[fromToken].getStartPos() < m.getFromPos()) {
            fromToken++;
          }
          fromToken += offsetTokens;
          int fromPos = m.getFromPos() + offsetChars;
          int toPos = m.getToPos() + offsetChars;
          m.setOffsetPosition(fromPos, toPos);
          int maxDistanceTokens = m.getRule().getDistanceTokens();
          if (maxDistanceTokens < 1) {
            maxDistanceTokens = defaultMaxDistanceTokens * m.getRule().getMinPrevMatches();
          }
          distancesBetweenMatches.add(fromToken - prevFromToken);
          if (prevMatches >= m.getRule().getMinPrevMatches()
              && isDistanceValid(distancesBetweenMatches, maxDistanceTokens, m.getRule().getMinPrevMatches())) {
            matches.add(m);
          }
          prevFromToken = fromToken;
          prevMatches++;
        }
        offsetChars += s.getText().length();
        offsetTokens += sentenceLenghtTokens - 1; // -1 -> not counting SENT_START
      }
      return matches.toArray(new RuleMatch[0]); 
    
<no_super_class>