/*
 * 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.uima.cas.impl;

import java.util.Comparator;
import java.util.NoSuchElementException;

import org.apache.uima.cas.FSIterator;
import org.apache.uima.cas.FeatureStructure;
import org.apache.uima.jcas.cas.TOP;
import org.apache.uima.jcas.tcas.Annotation;

/**
 * Performs an ordered iteration among a set of iterators, each one corresponding to the type or
 * subtype of the uppermost type.
 * 
 * The set of iterators is maintained in an array, with the 0th element being the current valid
 * iterator.
 *
 * @param <T>
 *          result type
 */
public class FsIterator_subtypes_ordered<T extends FeatureStructure>
        extends FsIterator_multiple_indexes<T> {

  private static final boolean DEBUG = false;

  /**
   * The number of elements to keep in order before the binary heap starts. This section helps the
   * performance in cases where a couple of types dominate the index.
   * 
   * The sorted section is searched sequentially. Above the sorted section, the search is done using
   * binary search
   */
  private static final int SORTED_SECTION = 3;

  /** index into nonEmptyIterators, shows last valid one */
  protected int lastValidIteratorIndex = -1;

  private boolean wentForward = true;

  // The IICP
  final private FsIndex_iicp<T> iicp;

  // /** true if sorted index, with typepriority as a key, but ignoring it because
  // * either there are no type priorities defined, or
  // * using a select-API-created iterator configured without typePriority
  // *
  // * Not final for the use case where there's a type-order key, type priorities are specified,
  // * but a select-API-created iterator wants to ignore type priorities.
  // */
  // final private boolean isSortedAndIgnoringTypeOrderKey;

  // call used by select framework to specify ignoring type priority
  public FsIterator_subtypes_ordered(FsIndex_iicp<T> iicp,
          Comparator<TOP> comparatorMaybeNoTypeWithoutId) {
    super(iicp, iicp.getIterators(), comparatorMaybeNoTypeWithoutId);
    this.iicp = iicp;
    FsIndex_set_sorted<T> idx2 = (FsIndex_set_sorted<T>) iicp.fsIndex_singletype;
    moveToFirstNoReinit();
  }

  /**
   * Move operators have to move a group of iterators for this type and all its subtypes
   */

  @Override
  public void moveToFirstNoReinit() {
    if (DEBUG) {
      dumpIteratorInfo("moveToFirstNoReinit - start");
    }

    int lvi = this.nonEmptyIterators.length - 1;
    // Need to consider all iterators.
    // Set all iterators to insertion point.
    int i = 0;
    while (i <= lvi) {
      final LowLevelIterator<T> it = this.nonEmptyIterators[i];
      it.moveToFirstNoReinit();
      if (it.isValid()) {
        heapify_up(it, i, 1);
        ++i;
      } else {
        // swap this iterator with the last possibly valid one
        // lvi might be equal to i, this will not be a problem
        this.nonEmptyIterators[i] = this.nonEmptyIterators[lvi];
        this.nonEmptyIterators[lvi] = it;
        --lvi;
      }
    }
    // configured to continue with forward iterations
    this.wentForward = true;
    this.lastValidIteratorIndex = lvi;

    if (DEBUG) {
      dumpIteratorInfo("moveToFirstNoReinit - end");
    }
  }

  @Override
  public void moveToLastNoReinit() {
    if (DEBUG) {
      dumpIteratorInfo("moveToLastNoReinit - start");
    }

    // no need to call isIndexesHaveBeenUpdated because
    // there's no state in this iterator that needs updating.

    int lvi = this.nonEmptyIterators.length - 1;
    // Need to consider all iterators.
    // Set all iterators to insertion point.
    int i = 0;
    while (i <= lvi) {
      final LowLevelIterator<T> it = this.nonEmptyIterators[i];
      // it.resetConcurrentModification();
      it.moveToLastNoReinit();
      if (it.isValid()) {
        heapify_up(it, i, -1);
        ++i;
      } else {
        // swap this iterator with the last possibly valid one
        // lvi might be equal to i, this will not be a problem
        this.nonEmptyIterators[i] = this.nonEmptyIterators[lvi];
        this.nonEmptyIterators[lvi] = it;
        --lvi;
      }
    }
    // configured to continue with backward iterations
    this.wentForward = false;
    this.lastValidIteratorIndex = lvi;

    if (DEBUG) {
      dumpIteratorInfo("moveToLastNoReinit - end");
    }
  }

  @Override
  public void moveToNextNvc() {
    if (DEBUG) {
      dumpIteratorInfo("moveToNextNvc - start");
    }

    final LowLevelIterator<T> it0 = nonEmptyIterators[0]/* .checkConcurrentModification() */;

    if (this.wentForward) {
      it0.moveToNextNvc();
      heapify_down(it0, 1);
    } else {
      moveToNextCmn(it0);
    }

    if (DEBUG) {
      dumpIteratorInfo("moveToNextNvc - end");
    }
  }

  /**
   * 
   * @param it0
   *          guaranteed to be a valid iterator by callers
   */
  private void moveToNextCmn(final LowLevelIterator<T> it0) {
    // We need to increment everything.
    int lvi = this.nonEmptyIterators.length - 1;
    int i = 1;
    while (i <= lvi) {
      // Any iterator other than the current one needs to be
      // incremented until it's pointing at something that's
      // greater than the current element.
      final LowLevelIterator<T> it = nonEmptyIterators[i]/* .checkConcurrentModification() */;
      // If the iterator we're considering is not valid, we
      // set it to the first element. This should be it for this iterator...
      if (!it.isValid()) {
        it.moveToFirstNoReinit();
      }
      // Increment the iterator while it is valid and pointing
      // at something smaller than the current element.
      while (it.isValid() && is_before(it, it0, 1)) {
        it.moveToNextNvc();
      }

      // find placement
      if (it.isValid()) {
        heapify_up(it, i, 1);
        ++i;
      } else {
        // swap this iterator with the last possibly valid one
        // lvi might be equal to i, this will not be a problem
        this.nonEmptyIterators[i] = this.nonEmptyIterators[lvi];
        this.nonEmptyIterators[lvi] = it;
        --lvi;
      }
    }

    this.lastValidIteratorIndex = lvi;
    this.wentForward = true;

    it0.moveToNext();
    heapify_down(it0, 1);

  }

  @Override
  public void moveToPreviousNvc() {
    if (DEBUG) {
      dumpIteratorInfo("moveToPreviousNvc - start");
    }

    final LowLevelIterator<T> it0 = nonEmptyIterators[0]/* .checkConcurrentModification() */;
    if (!this.wentForward) {
      it0.moveToPreviousNvc();
      // this also takes care of invalid iterators
      heapify_down(it0, -1);
    } else {
      // We need to decrement everything.
      int lvi = this.nonEmptyIterators.length - 1;
      int i = 1;
      while (i <= lvi) {
        // Any iterator other than the current one needs to be
        // decremented until it's pointing at something that's
        // smaller than the current element.
        final LowLevelIterator<T> it = nonEmptyIterators[i]/* .checkConcurrentModification() */;
        // If the iterator we're considering is not valid, we
        // set it to the last element. This should be it for this iterator...
        if (!it.isValid()) {
          it.moveToLastNoReinit();
        }
        // Decrement the iterator while it is valid and pointing
        // at something greater than the current element.
        while (it.isValid() && is_before(it, it0, -1)) {
          it.moveToPrevious();
        }

        // find placement
        if (it.isValid()) {
          heapify_up(it, i, -1);
          ++i;
        } else {
          // swap this iterator with the last possibly valid one
          // lvi might be equal to i, this will not be a problem
          this.nonEmptyIterators[i] = this.nonEmptyIterators[lvi];
          this.nonEmptyIterators[lvi] = it;
          --lvi;
        }
      }

      this.lastValidIteratorIndex = lvi;
      this.wentForward = false;

      it0.moveToPrevious();
      heapify_down(it0, -1);

    }

    if (DEBUG) {
      dumpIteratorInfo("moveToPreviousNvc - end");
    }
  }

  /**
   * Test the order with which the two iterators should be used. Introduces arbitrary ordering for
   * equivalent FSs. Only called with valid iterators.
   * 
   * @param l
   *          - guaranteed to ba a valid iterator by callers
   * @param r
   *          - guaranteed to be a valid iterator by callers
   * @param dir
   *          Direction of movement, 1 for forward, -1 for backward
   * @return true if the left iterator needs to be used before the right one.
   */
  private boolean is_before(LowLevelIterator<T> l, LowLevelIterator<T> r, int dir) {

    // // debug
    // if (!l.isValid()) {
    // throw new RuntimeException("1st arg invalid");
    // }
    //
    // if (!r.isValid()) {
    // throw new RuntimeException("2nd arg invalid");
    // }

    int d = compare(l.getNvc(), r.getNvc());
    return d * dir < 0;
  }

  // @formatter:off
  /**
   * Only used to compare two iterator's with different types position
   * @param fsLeft the left iterator's element
   * @param fsRight the right iterator's element
   * @return  1 if left > right,   (compare maybe ignores type)
   *         -1 if left < right,   (compare maybe ignores type)
   *          1 if left == right and left.id > right.id
   *         -1 if left == right and left.id < right.id
   */
  // @formatter:on
  private int compare(FeatureStructure fsLeft, FeatureStructure fsRight) {
    int d = comparatorMaybeNoTypeWithoutId.compare((TOP) fsLeft, (TOP) fsRight);

    // If two FSs are identical wrt the comparator of the index,
    // we still need to be able to distinguish them to be able to have a
    // well-defined sequence. In that case, we arbitrarily order FSs by
    // their ids. We need to do this in order to be able to ensure that a
    // reverse iterator produces the reverse order of the forward iterator.
    if (d == 0) {
      d = fsLeft._id() - fsRight._id();
    }
    return d;
  }

  /**
   * Move the idx'th iterator element up in the heap until it finds its proper position. Up means
   * previous iterators are before it
   * 
   * @param it
   *          indexes[idx], guaranteed to be "valid"
   * @param idx
   *          Element to move, nonEmptyIterators[i] == it
   * @param dir
   *          Direction of iterator movement, 1 for forward, -1 for backward
   */
  private void heapify_up(LowLevelIterator<T> it, int idx, int dir) {
    // FSIndexFlat<? extends FeatureStructure> flatIndexInfo = iicp.flatIndex;
    // if (null != flatIndexInfo) {
    // flatIndexInfo.incrementReorderingCount();
    // }
    int nidx;

    while (idx > SORTED_SECTION) {
      nidx = (idx + SORTED_SECTION - 1) >> 1;
      if (!is_before(it, this.nonEmptyIterators[nidx], dir)) {
        this.nonEmptyIterators[idx] = it;
        return;
      }
      this.nonEmptyIterators[idx] = this.nonEmptyIterators[nidx];
      idx = nidx;
    }

    while (idx > 0) {
      nidx = idx - 1;
      if (!is_before(it, this.nonEmptyIterators[nidx], dir)) {
        this.nonEmptyIterators[idx] = it;
        return;
      }
      this.nonEmptyIterators[idx] = this.nonEmptyIterators[nidx];
      idx = nidx;
    }

    this.nonEmptyIterators[idx] = it;
  }

  /**
   * Move the top element down in the heap until it finds its proper position.
   * 
   * @param it
   *          indexes[0], may be invalid
   * @param dir
   *          Direction of iterator movement, 1 for forward, -1 for backward
   */
  private void heapify_down(LowLevelIterator<T> it, int dir) {
    // FSIndexFlat<? extends FeatureStructure> flatIndexInfo = iicp.flatIndex;
    // if (null != flatIndexInfo) {
    // flatIndexInfo.incrementReorderingCount();
    // }

    if (!it.isValid()) {
      // if at the end of the iteration, the lastValidIteratorIndex is this one (e.g., is 0)
      // and this operation is a noop, but sets the lastValidIteratorIndex to -1, indicating the
      // iterator is invalid
      final LowLevelIterator<T> itl = this.nonEmptyIterators[this.lastValidIteratorIndex]/*
                                                                                          * .checkConcurrentModification
                                                                                          * ()
                                                                                          */;
      this.nonEmptyIterators[this.lastValidIteratorIndex] = it;
      this.nonEmptyIterators[0] = itl;
      --this.lastValidIteratorIndex;
      it = itl;
    }

    final int num = this.lastValidIteratorIndex;
    if ((num < 1)
            || !is_before(this.nonEmptyIterators[1]/* .checkConcurrentModification() */, it, dir)) {
      return;
    }

    int idx = 1;
    this.nonEmptyIterators[0] = this.nonEmptyIterators[1];
    final int end = Math.min(num, SORTED_SECTION);
    int nidx = idx + 1;

    // make sure we don't leave the iterator in a completely invalid state
    // (i.e. one it can't recover from using moveTo/moveToFirst/moveToLast)
    // in case of a concurrent modification
    try {
      while (nidx <= end) {
        if (!is_before(this.nonEmptyIterators[nidx]/* .checkConcurrentModification() */, it, dir)) {
          return; // passes through finally
        }

        this.nonEmptyIterators[idx] = this.nonEmptyIterators[nidx];
        idx = nidx;
        nidx = idx + 1;
      }

      nidx = SORTED_SECTION + 1;
      while (nidx <= num) {
        if ((nidx < num)
                && is_before(this.nonEmptyIterators[nidx + 1]/* .checkConcurrentModification() */,
                        this.nonEmptyIterators[nidx]/* .checkConcurrentModification() */, dir)) {
          ++nidx;
        }

        if (!is_before(this.nonEmptyIterators[nidx], it, dir)) {
          return;
        }

        this.nonEmptyIterators[idx] = this.nonEmptyIterators[nidx];
        idx = nidx;
        nidx = (nidx << 1) - (SORTED_SECTION - 1);
      }
    } finally {
      this.nonEmptyIterators[idx] = it;
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.cas.FSIterator#isValid()
   */
  @Override
  public boolean isValid() {
    return lastValidIteratorIndex >= 0;
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.cas.FSIterator#getNvc()
   */
  @Override
  public T getNvc() throws NoSuchElementException {
    return nonEmptyIterators[0].get();
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.cas.FSIterator#moveTo(org.apache.uima.cas.FeatureStructure)
   * 
   * set all iterators to insertion point
   * 
   * While rattling the iterators for sort order: if ignoreType_moveTo use compare without type
   */
  @Override
  public void moveToNoReinit(FeatureStructure fs) {
    if (DEBUG) {
      dumpIteratorInfo("moveToNoReinit - start");
    }

    // no need to call isIndexesHaveBeenUpdated because
    // there's no state in this iterator that needs updating.

    // set null unless need special extra type compare
    // Type typeCompare = (isSortedAndIgnoringTypeOrderKey) ? fs.getType() : null;

    int lastValidIterator_local = this.nonEmptyIterators.length - 1;
    // Need to consider all iterators.
    // Set all iterators to insertion point.
    int i = 0;
    while (i <= lastValidIterator_local) {
      final LowLevelIterator<T> it = this.nonEmptyIterators[i];
      it.moveToNoReinit(fs);

      // If the moveTo operation moved the iterator beyond the end (made it invalid), we need to
      // check if this was really justified according to our comparator. If our comparator says that
      // there are elements in the iterator which are equivalent according to the comparator
      // (can happen in particular if the comparator ignores type priorities), then we need to
      // back up the iterator and restore its validity.
      if (!it.isValid()) {
        it.moveToLastNoReinit();

        boolean wentBack = false;
        while (it.isValid()
                && comparatorMaybeNoTypeWithoutId.compare((TOP) it.get(), (TOP) fs) == 0) {
          it.moveToPreviousNvc();
          wentBack = true;
        }
        if (!it.isValid()) {
          if (wentBack) {
            // Go back to the last one matching according to the comparator
            it.moveToFirst();
          }
        } else {
          // Go back to the last one matching according to the comparator (or simply to where we
          // came from)
          it.moveToNext();
        }
      }
      // The moveTo operation may have moved the iterator beyond the last potentially matching FS
      // because the target FS is of a different type then the index. We need to check that and it
      // necessary back up
      else {
        it.moveToPrevious();
        while (it.isValid()
                && comparatorMaybeNoTypeWithoutId.compare((TOP) it.get(), (TOP) fs) == 0) {
          it.moveToPreviousNvc();
        }

        if (!it.isValid()) {
          // Go back to the last one matching according to the comparator
          it.moveToFirst();
        } else {
          // Go back to the last one matching according to the comparator (or simply to where we
          // came from)
          it.moveToNext();
        }
      }

      if (it.isValid()) {
        heapify_up(it, i, 1);
        ++i;
      } else {
        // swap this iterator with the last possibly valid one
        // lvi might be equal to i, this will not be a problem
        this.nonEmptyIterators[i] = this.nonEmptyIterators[lastValidIterator_local];
        this.nonEmptyIterators[lastValidIterator_local] = it;
        --lastValidIterator_local;
      }
    }

    // configured to continue with forward iterations
    this.wentForward = true;
    this.lastValidIteratorIndex = lastValidIterator_local;

    if (DEBUG) {
      dumpIteratorInfo("moveToNoReinit - end");
    }
  }

  // /* (non-Javadoc)
  // * @see org.apache.uima.cas.FSIterator#moveTo(org.apache.uima.cas.FeatureStructure)
  // */
  // @Override
  // public void moveToExactNoReinit(FeatureStructure fs) {
  //
  // int lvi = this.nonEmptyIterators.length - 1;
  // // Need to consider all iterators.
  // // Set all iterators to insertion point.
  // int i = 0;
  // while (i <= lvi) {
  // final LowLevelIterator<T> it = this.nonEmptyIterators[i];
  // it.moveToExactNoReinit(fs);
  // if (it.isValid()) {
  // heapify_up(it, i, 1);
  // ++i;
  // } else {
  // // swap this iterator with the last possibly valid one
  // // lvi might be equal to i, this will not be a problem
  // this.nonEmptyIterators[i] = this.nonEmptyIterators[lvi];
  // this.nonEmptyIterators[lvi] = it;
  // --lvi;
  // }
  // }
  // // configured to continue with forward iterations
  // this.wentForward = true;
  // this.lastValidIteratorIndex = lvi;
  // }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.cas.FSIterator#copy()
   */
  @Override
  public FSIterator<T> copy() {
    FsIterator_subtypes_ordered<T> it = new FsIterator_subtypes_ordered<>(iicp,
            comparatorMaybeNoTypeWithoutId);
    if (!isValid()) {
      it.moveToPrevious(); // mark new one also invalid
    } else {
      T posFs = getNvc();
      it.moveToNoReinit(posFs); // moves to left-most position
      while (it.get() != posFs) {
        it.moveToNext();
      }
    }
    return it;
  }

  @Override
  public Comparator<TOP> getComparator() {
    return comparatorMaybeNoTypeWithoutId;
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.cas.impl.LowLevelIterator#moveToSupported()
   */
  @Override
  public boolean isMoveToSupported() {
    return true;
  }

  private void dumpIteratorInfo(String context) {
    StringBuilder sb = new StringBuilder();
    for (LowLevelIterator it : nonEmptyIterators) {
      FsIterator_set_sorted2 it2 = (FsIterator_set_sorted2) it;
      sb.append(String.format("  [%s@%d] %s %b pos:%d ", it.getClass().getSimpleName(),
              System.identityHashCode(it), it.getType(), it.isValid(), it2.pos));
      if (it.isValid()) {
        Annotation a = (Annotation) it.get();
        sb.append(
                String.format("%s@[%d-%d]", a.getType().getShortName(), a.getBegin(), a.getEnd()));
      } else {
        sb.append("<invalid>");
      }
      sb.append("\n");
    }
    System.out.printf("[%s@%d] %s (lvi: %d  wentForward:%b)%n%s", getClass().getSimpleName(),
            System.identityHashCode(this), context, lastValidIteratorIndex, wentForward, sb);
  }
}
