/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.search.vectorhighlight;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.lucene.search.vectorhighlight.FieldPhraseList.WeightedPhraseInfo;

/** A abstract implementation of {@link FragListBuilder}. */
public abstract class BaseFragListBuilder implements FragListBuilder {

  public static final int MARGIN_DEFAULT = 6;
  public static final int MIN_FRAG_CHAR_SIZE_FACTOR = 3;

  final int margin;
  final int minFragCharSize;

  public BaseFragListBuilder(int margin) {
    if (margin < 0)
      throw new IllegalArgumentException(
          "margin(" + margin + ") is too small. It must be 0 or higher.");

    this.margin = margin;
    this.minFragCharSize = Math.max(1, margin * MIN_FRAG_CHAR_SIZE_FACTOR);
  }

  public BaseFragListBuilder() {
    this(MARGIN_DEFAULT);
  }

  protected FieldFragList createFieldFragList(
      FieldPhraseList fieldPhraseList, FieldFragList fieldFragList, int fragCharSize) {
    if (fragCharSize < minFragCharSize)
      throw new IllegalArgumentException(
          "fragCharSize("
              + fragCharSize
              + ") is too small. It must be "
              + minFragCharSize
              + " or higher.");

    List<WeightedPhraseInfo> wpil = new ArrayList<>();
    IteratorQueue<WeightedPhraseInfo> queue =
        new IteratorQueue<>(fieldPhraseList.getPhraseList().iterator());
    WeightedPhraseInfo phraseInfo = null;
    int startOffset = 0;
    while ((phraseInfo = queue.top()) != null) {
      // if the phrase violates the border of previous fragment, discard it and try next phrase
      if (phraseInfo.getStartOffset() < startOffset) {
        queue.removeTop();
        continue;
      }

      wpil.clear();
      final int currentPhraseStartOffset = phraseInfo.getStartOffset();
      int currentPhraseEndOffset = phraseInfo.getEndOffset();
      int spanStart = Math.max(currentPhraseStartOffset - margin, startOffset);
      int spanEnd = Math.max(currentPhraseEndOffset, spanStart + fragCharSize);
      if (acceptPhrase(
          queue.removeTop(), currentPhraseEndOffset - currentPhraseStartOffset, fragCharSize)) {
        wpil.add(phraseInfo);
      }
      while ((phraseInfo = queue.top()) != null) { // pull until we crossed the current spanEnd
        if (phraseInfo.getEndOffset() <= spanEnd) {
          currentPhraseEndOffset = phraseInfo.getEndOffset();
          if (acceptPhrase(
              queue.removeTop(), currentPhraseEndOffset - currentPhraseStartOffset, fragCharSize)) {
            wpil.add(phraseInfo);
          }
        } else {
          break;
        }
      }
      if (wpil.isEmpty()) {
        continue;
      }

      final int matchLen = currentPhraseEndOffset - currentPhraseStartOffset;
      // now recalculate the start and end position to "center" the result
      // matchLen can be > fragCharSize prevent IAOOB here
      final int newMargin = Math.max(0, (fragCharSize - matchLen) / 2);
      spanStart = currentPhraseStartOffset - newMargin;
      if (spanStart < startOffset) {
        spanStart = startOffset;
      }
      // whatever is bigger here we grow this out
      spanEnd = spanStart + Math.max(matchLen, fragCharSize);
      startOffset = spanEnd;
      fieldFragList.add(spanStart, spanEnd, wpil);
    }
    return fieldFragList;
  }

  /**
   * A predicate to decide if the given {@link WeightedPhraseInfo} should be accepted as a
   * highlighted phrase or if it should be discarded.
   *
   * <p>The default implementation discards phrases that are composed of more than one term and
   * where the matchLength exceeds the fragment character size.
   *
   * @param info the phrase info to accept
   * @param matchLength the match length of the current phrase
   * @param fragCharSize the configured fragment character size
   * @return <code>true</code> if this phrase info should be accepted as a highligh phrase
   */
  protected boolean acceptPhrase(WeightedPhraseInfo info, int matchLength, int fragCharSize) {
    return info.getTermsOffsets().size() <= 1 || matchLength <= fragCharSize;
  }

  private static final class IteratorQueue<T> {
    private final Iterator<T> iter;
    private T top;

    public IteratorQueue(Iterator<T> iter) {
      this.iter = iter;
      T removeTop = removeTop();
      assert removeTop == null;
    }

    public T top() {
      return top;
    }

    public T removeTop() {
      T currentTop = top;
      if (iter.hasNext()) {
        top = iter.next();
      } else {
        top = null;
      }
      return currentTop;
    }
  }
}
