/*
 * 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.queries.spans;

import java.io.IOException;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.TwoPhaseIterator;

/** Wraps a Spans with additional asserts */
class AssertingSpans extends Spans {
  final Spans in;
  int doc = -1;

  /** tracks current state of this spans */
  enum State {
    /** document iteration has not yet begun ({@link #docID()} = -1) */
    DOC_START,

    /**
     * two-phase iterator has moved to a new docid, but {@link TwoPhaseIterator#matches()} has not
     * been called or it returned false (so you should not do things with the enum)
     */
    DOC_UNVERIFIED,

    /**
     * iterator set to a valid docID, but position iteration has not yet begun ({@link
     * #startPosition() == -1})
     */
    POS_START,

    /**
     * iterator set to a valid docID, and positioned (-1 < {@link #startPosition()} < {@link
     * #NO_MORE_POSITIONS})
     */
    ITERATING,

    /** positions exhausted ({@link #startPosition()} = {@link #NO_MORE_POSITIONS}) */
    POS_FINISHED,

    /** documents exhausted ({@link #docID()} = {@link #NO_MORE_DOCS}) */
    DOC_FINISHED
  };

  State state = State.DOC_START;

  AssertingSpans(Spans in) {
    this.in = in;
  }

  @Override
  public int nextStartPosition() throws IOException {
    assert state != State.DOC_START : "invalid position access, state=" + state + ": " + in;
    assert state != State.DOC_FINISHED : "invalid position access, state=" + state + ": " + in;
    assert state != State.DOC_UNVERIFIED : "invalid position access, state=" + state + ": " + in;

    checkCurrentPositions();

    // move to next position
    int prev = in.startPosition();
    int start = in.nextStartPosition();
    assert start >= prev
        : "invalid startPosition (positions went backwards, previous=" + prev + "): " + in;

    // transition state if necessary
    if (start == NO_MORE_POSITIONS) {
      state = State.POS_FINISHED;
    } else {
      state = State.ITERATING;
    }

    // check new positions
    checkCurrentPositions();
    return start;
  }

  private void checkCurrentPositions() {
    int start = in.startPosition();
    int end = in.endPosition();

    if (state == State.DOC_START || state == State.DOC_UNVERIFIED || state == State.POS_START) {
      assert start == -1 : "invalid startPosition (should be -1): " + in;
      assert end == -1 : "invalid endPosition (should be -1): " + in;
    } else if (state == State.POS_FINISHED) {
      assert start == NO_MORE_POSITIONS
          : "invalid startPosition (should be NO_MORE_POSITIONS): " + in;
      assert end == NO_MORE_POSITIONS : "invalid endPosition (should be NO_MORE_POSITIONS): " + in;
    } else {
      assert start >= 0 : "invalid startPosition (negative): " + in;
      assert start <= end : "invalid startPosition (> endPosition): " + in;
    }
  }

  @Override
  public int startPosition() {
    checkCurrentPositions();
    return in.startPosition();
  }

  @Override
  public int endPosition() {
    checkCurrentPositions();
    return in.endPosition();
  }

  @Override
  public int width() {
    assert state == State.ITERATING;
    final int distance = in.width();
    assert distance >= 0;
    return distance;
  }

  @Override
  public void collect(SpanCollector collector) throws IOException {
    assert state == State.ITERATING : "collect() called in illegal state: " + state + ": " + in;
    in.collect(collector);
  }

  @Override
  public int docID() {
    int doc = in.docID();
    assert doc == this.doc
        : "broken docID() impl: docID() = "
            + doc
            + ", but next/advance last returned: "
            + this.doc
            + ": "
            + in;
    return doc;
  }

  @Override
  public int nextDoc() throws IOException {
    assert state != State.DOC_FINISHED : "nextDoc() called after NO_MORE_DOCS: " + in;
    int nextDoc = in.nextDoc();
    assert nextDoc > doc : "backwards nextDoc from " + doc + " to " + nextDoc + ": " + in;
    if (nextDoc == NO_MORE_DOCS) {
      state = State.DOC_FINISHED;
    } else {
      assert in.startPosition() == -1 : "invalid initial startPosition() [should be -1]: " + in;
      assert in.endPosition() == -1 : "invalid initial endPosition() [should be -1]: " + in;
      state = State.POS_START;
    }
    doc = nextDoc;
    return docID();
  }

  @Override
  public int advance(int target) throws IOException {
    assert state != State.DOC_FINISHED : "advance() called after NO_MORE_DOCS: " + in;
    assert target > doc : "target must be > docID(), got " + target + " <= " + doc + ": " + in;
    int advanced = in.advance(target);
    assert advanced >= target
        : "backwards advance from: " + target + " to: " + advanced + ": " + in;
    if (advanced == NO_MORE_DOCS) {
      state = State.DOC_FINISHED;
    } else {
      assert in.startPosition() == -1 : "invalid initial startPosition() [should be -1]: " + in;
      assert in.endPosition() == -1 : "invalid initial endPosition() [should be -1]: " + in;
      state = State.POS_START;
    }
    doc = advanced;
    return docID();
  }

  @Override
  public String toString() {
    return "Asserting(" + in + ")";
  }

  @Override
  public long cost() {
    return in.cost();
  }

  @Override
  public float positionsCost() {
    float cost = in.positionsCost();
    assert !Float.isNaN(cost) : "positionsCost() should not be NaN";
    assert cost > 0 : "positionsCost() must be positive";
    return cost;
  }

  @Override
  public TwoPhaseIterator asTwoPhaseIterator() {
    final TwoPhaseIterator iterator = in.asTwoPhaseIterator();
    if (iterator == null) {
      return null;
    }
    return new AssertingTwoPhaseView(iterator);
  }

  class AssertingTwoPhaseView extends TwoPhaseIterator {
    final TwoPhaseIterator in;
    int lastDoc = -1;

    AssertingTwoPhaseView(TwoPhaseIterator iterator) {
      super(new AssertingDISI(iterator.approximation()));
      this.in = iterator;
    }

    @Override
    public boolean matches() throws IOException {
      if (approximation.docID() == -1 || approximation.docID() == NO_MORE_DOCS) {
        throw new AssertionError(
            "matches() should not be called on doc ID " + approximation.docID());
      }
      if (lastDoc == approximation.docID()) {
        throw new AssertionError(
            "matches() has been called twice on doc ID " + approximation.docID());
      }
      lastDoc = approximation.docID();
      boolean v = in.matches();
      if (v) {
        state = State.POS_START;
      }
      return v;
    }

    @Override
    public float matchCost() {
      float cost = in.matchCost();
      if (Float.isNaN(cost)) {
        throw new AssertionError(
            "matchCost()=" + cost + " should not be NaN on doc ID " + approximation.docID());
      }
      if (cost < 0) {
        throw new AssertionError(
            "matchCost()=" + cost + " should be non negative on doc ID " + approximation.docID());
      }
      return cost;
    }
  }

  class AssertingDISI extends DocIdSetIterator {
    final DocIdSetIterator in;

    AssertingDISI(DocIdSetIterator in) {
      this.in = in;
    }

    @Override
    public int docID() {
      assert in.docID() == AssertingSpans.this.docID();
      return in.docID();
    }

    @Override
    public int nextDoc() throws IOException {
      assert state != State.DOC_FINISHED : "nextDoc() called after NO_MORE_DOCS: " + in;
      int nextDoc = in.nextDoc();
      assert nextDoc > doc : "backwards nextDoc from " + doc + " to " + nextDoc + ": " + in;
      if (nextDoc == DocIdSetIterator.NO_MORE_DOCS) {
        state = State.DOC_FINISHED;
      } else {
        state = State.DOC_UNVERIFIED;
      }
      doc = nextDoc;
      return docID();
    }

    @Override
    public int advance(int target) throws IOException {
      assert state != State.DOC_FINISHED : "advance() called after NO_MORE_DOCS: " + in;
      assert target > doc : "target must be > docID(), got " + target + " <= " + doc + ": " + in;
      int advanced = in.advance(target);
      assert advanced >= target
          : "backwards advance from: " + target + " to: " + advanced + ": " + in;
      if (advanced == DocIdSetIterator.NO_MORE_DOCS) {
        state = State.DOC_FINISHED;
      } else {
        state = State.DOC_UNVERIFIED;
      }
      doc = advanced;
      return docID();
    }

    @Override
    public long cost() {
      return in.cost();
    }
  }
}
