/*
 * 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.Collection;
import java.util.Collections;

/** A Scorer for queries with a required subscorer
 * and an excluding (prohibited) sub {@link Scorer}.
 */
class ReqExclScorer extends Scorer {

  private final Scorer reqScorer;
  // approximations of the scorers, or the scorers themselves if they don't support approximations
  private final DocIdSetIterator reqApproximation;
  private final DocIdSetIterator exclApproximation;
  // two-phase views of the scorers, or null if they do not support approximations
  private final TwoPhaseIterator reqTwoPhaseIterator;
  private final TwoPhaseIterator exclTwoPhaseIterator;

  /** Construct a <code>ReqExclScorer</code>.
   * @param reqScorer The scorer that must match, except where
   * @param exclScorer indicates exclusion.
   */
  public ReqExclScorer(Scorer reqScorer, Scorer exclScorer) {
    super(reqScorer.weight);
    this.reqScorer = reqScorer;
    reqTwoPhaseIterator = reqScorer.twoPhaseIterator();
    if (reqTwoPhaseIterator == null) {
      reqApproximation = reqScorer.iterator();
    } else {
      reqApproximation = reqTwoPhaseIterator.approximation();
    }
    exclTwoPhaseIterator = exclScorer.twoPhaseIterator();
    if (exclTwoPhaseIterator == null) {
      exclApproximation = exclScorer.iterator();
    } else {
      exclApproximation = exclTwoPhaseIterator.approximation();
    }
  }

  /** Confirms whether or not the given {@link TwoPhaseIterator}
   *  matches on the current document. */
  private static boolean matchesOrNull(TwoPhaseIterator it) throws IOException {
    return it == null || it.matches();
  }

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

  @Override
  public int docID() {
    return reqApproximation.docID();
  }

  @Override
  public float score() throws IOException {
    return reqScorer.score(); // reqScorer may be null when next() or skipTo() already return false
  }

  @Override
  public int advanceShallow(int target) throws IOException {
    return reqScorer.advanceShallow(target);
  }

  @Override
  public float getMaxScore(int upTo) throws IOException {
    return reqScorer.getMaxScore(upTo);
  }

  @Override
  public void setMinCompetitiveScore(float score) throws IOException {
    // The score of this scorer is the same as the score of 'reqScorer'.
    reqScorer.setMinCompetitiveScore(score);
  }

  @Override
  public Collection<ChildScorable> getChildren() {
    return Collections.singleton(new ChildScorable(reqScorer, "MUST"));
  }

  /**
   * Estimation of the number of operations required to call DISI.advance.
   * This is likely completely wrong, especially given that the cost of
   * this method usually depends on how far you want to advance, but it's
   * probably better than nothing.
   */
  private static final int ADVANCE_COST = 10;

  private static float matchCost(
      DocIdSetIterator reqApproximation,
      TwoPhaseIterator reqTwoPhaseIterator,
      DocIdSetIterator exclApproximation,
      TwoPhaseIterator exclTwoPhaseIterator) {
    float matchCost = 2; // we perform 2 comparisons to advance exclApproximation
    if (reqTwoPhaseIterator != null) {
      // this two-phase iterator must always be matched
      matchCost += reqTwoPhaseIterator.matchCost();
    }

    // match cost of the prohibited clause: we need to advance the approximation
    // and match the two-phased iterator
    final float exclMatchCost = ADVANCE_COST
        + (exclTwoPhaseIterator == null ? 0 : exclTwoPhaseIterator.matchCost());

    // upper value for the ratio of documents that reqApproximation matches that
    // exclApproximation also matches
    float ratio;
    if (reqApproximation.cost() <= 0) {
      ratio = 1f;
    } else if (exclApproximation.cost() <= 0) {
      ratio = 0f;
    } else {
      ratio = (float) Math.min(reqApproximation.cost(), exclApproximation.cost()) / reqApproximation.cost();
    }
    matchCost += ratio * exclMatchCost;

    return matchCost;
  }

  @Override
  public TwoPhaseIterator twoPhaseIterator() {
    final float matchCost = matchCost(reqApproximation, reqTwoPhaseIterator, exclApproximation, exclTwoPhaseIterator);

    if (reqTwoPhaseIterator == null
        || (exclTwoPhaseIterator != null && reqTwoPhaseIterator.matchCost() <= exclTwoPhaseIterator.matchCost())) {
      // reqTwoPhaseIterator is LESS costly than exclTwoPhaseIterator, check it first
      return new TwoPhaseIterator(reqApproximation) {

        @Override
        public boolean matches() throws IOException {
          final int doc = reqApproximation.docID();
          // check if the doc is not excluded
          int exclDoc = exclApproximation.docID();
          if (exclDoc < doc) {
            exclDoc = exclApproximation.advance(doc);
          }
          if (exclDoc != doc) {
            return matchesOrNull(reqTwoPhaseIterator);
          }
          return matchesOrNull(reqTwoPhaseIterator) && !matchesOrNull(exclTwoPhaseIterator);
        }

        @Override
        public float matchCost() {
          return matchCost;
        }
      };
    } else {
      // reqTwoPhaseIterator is MORE costly than exclTwoPhaseIterator, check it last
      return new TwoPhaseIterator(reqApproximation) {

        @Override
        public boolean matches() throws IOException {
          final int doc = reqApproximation.docID();
          // check if the doc is not excluded
          int exclDoc = exclApproximation.docID();
          if (exclDoc < doc) {
            exclDoc = exclApproximation.advance(doc);
          }
          if (exclDoc != doc) {
            return matchesOrNull(reqTwoPhaseIterator);
          }
          return !matchesOrNull(exclTwoPhaseIterator) && matchesOrNull(reqTwoPhaseIterator);
        }

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