/*
 * 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;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/** A {@link Scorer} whose number of matches is per-document. */
final class CoveringScorer extends Scorer {

  final int numScorers;
  final int maxDoc;
  final LongValues minMatchValues;

  boolean matches; // if true then the doc matches, otherwise we don't know and need to check
  int doc;  // current doc ID
  DisiWrapper topList; // list of matches
  int freq; // number of scorers on the desired doc ID
  long minMatch; // current required number of matches

  // priority queue that stores all scorers
  final DisiPriorityQueue subScorers;

  final long cost;

  CoveringScorer(Weight weight, Collection<Scorer> scorers, LongValues minMatchValues, int maxDoc) {
    super(weight);

    this.numScorers = scorers.size();
    this.maxDoc = maxDoc;
    this.minMatchValues = minMatchValues;
    this.doc = -1;

    subScorers = new DisiPriorityQueue(scorers.size());

    for (Scorer scorer : scorers) {
      subScorers.add(new DisiWrapper(scorer));
    }

    this.cost = scorers.stream().map(Scorer::iterator).mapToLong(DocIdSetIterator::cost).sum();
  }

  @Override
  public final Collection<ChildScorable> getChildren() throws IOException {
    List<ChildScorable> matchingChildren = new ArrayList<>();
    setTopListAndFreqIfNecessary();
    for (DisiWrapper s = topList; s != null; s = s.next) {
      matchingChildren.add(new ChildScorable(s.scorer, "SHOULD"));
    }
    return matchingChildren;
  }

  private final DocIdSetIterator approximation = new DocIdSetIterator() {

    @Override
    public int docID() {
      return doc;
    }

    @Override
    public int nextDoc() throws IOException {
      return advance(docID() + 1);
    }

    @Override
    public int advance(int target) throws IOException {
      // reset state
      matches = false;
      topList = null;

      doc = target;
      setMinMatch();

      DisiWrapper top = subScorers.top();
      int numMatches = 0;
      int maxPotentialMatches = numScorers;
      while (top.doc < target) {
        if (maxPotentialMatches < minMatch) {
          // No need to keep trying to advance to `target` since no match is possible.
          if (target >= maxDoc - 1) {
            doc = NO_MORE_DOCS;
          } else {
            doc = target + 1;
          }
          setMinMatch();
          return doc;
        }
        top.doc = top.iterator.advance(target);
        boolean match = top.doc == target;
        top = subScorers.updateTop();
        if (match) {
          numMatches++;
          if (numMatches >= minMatch) {
            // success, no need to check other iterators
            matches = true;
            return doc;
          }
        } else {
          maxPotentialMatches--;
        }
      }

      doc = top.doc;
      setMinMatch();
      return doc;
    }

    private void setMinMatch() throws IOException {
      if (doc >= maxDoc) {
        // advanceExact may not be called on out-of-range doc ids
        minMatch = 1;
      } else if (minMatchValues.advanceExact(doc)) {
        // values < 1 are treated as 1: we require at least one match
        minMatch = Math.max(1, minMatchValues.longValue());
      } else {
        // this will make sure the document does not match
        minMatch = Long.MAX_VALUE;
      }
    }

    @Override
    public long cost() {
      return maxDoc;
    }

  };

  private final TwoPhaseIterator twoPhase = new TwoPhaseIterator(approximation) {

    @Override
    public boolean matches() throws IOException {
      if (matches) {
        return true;
      }
      if (topList == null) {
        advanceAll(doc);
      }
      if (subScorers.top().doc != doc) {
        assert subScorers.top().doc > doc;
        return false;
      }
      setTopListAndFreq();
      assert topList.doc == doc;
      return matches = freq >= minMatch;
    }

    @Override
    public float matchCost() {
      return numScorers;
    }

  };

  @Override
  public DocIdSetIterator iterator() {
    return TwoPhaseIterator.asDocIdSetIterator(twoPhase);
  }

  @Override
  public TwoPhaseIterator twoPhaseIterator() {
    return twoPhase;
  }

  private void advanceAll(int target) throws IOException {
    DisiWrapper top = subScorers.top();
    while (top.doc < target) {
      top.doc = top.iterator.advance(target);
      top = subScorers.updateTop();
    }
  }

  private void setTopListAndFreq() {
    topList = subScorers.topList();
    freq = 0;
    for (DisiWrapper w = topList; w != null; w = w.next) {
      freq++;
    }
  }

  private void setTopListAndFreqIfNecessary() throws IOException {
    if (topList == null) {
      advanceAll(doc);
      setTopListAndFreq();
    }
  }

  @Override
  public float score() throws IOException {
    // we need to know about all matches
    setTopListAndFreqIfNecessary();
    double score = 0;
    for (DisiWrapper w = topList; w != null; w = w.next) {
      score += w.scorer.score();
    }
    return (float) score;
  }

  @Override
  public float getMaxScore(int upTo) throws IOException {
    // TODO: implement but beware of floating-point errors
    return Float.POSITIVE_INFINITY;
  }

  @Override
  public int docID() {
    return doc;
  }

}
