/*
 * 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 org.apache.lucene.index.DocValues;
import org.apache.lucene.index.FilterNumericDocValues;
import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.index.SortedNumericDocValues;
import org.apache.lucene.util.NumericUtils;

/**
 * Selects a value from the document's list to use as the representative value
 *
 * <p>This provides a NumericDocValues view over the SortedNumeric, for use with sorting,
 * expressions, function queries, etc.
 */
public class SortedNumericSelector {

  /** Type of selection to perform. */
  public enum Type {
    /** Selects the minimum value in the set */
    MIN,
    /** Selects the maximum value in the set */
    MAX,
    // TODO: we could do MEDIAN in constant time (at most 2 lookups)
  }

  /**
   * Wraps a multi-valued SortedNumericDocValues as a single-valued view, using the specified
   * selector and numericType.
   */
  public static NumericDocValues wrap(
      SortedNumericDocValues sortedNumeric, Type selector, SortField.Type numericType) {
    if (numericType != SortField.Type.INT
        && numericType != SortField.Type.LONG
        && numericType != SortField.Type.FLOAT
        && numericType != SortField.Type.DOUBLE) {
      throw new IllegalArgumentException("numericType must be a numeric type");
    }
    final NumericDocValues view;
    NumericDocValues singleton = DocValues.unwrapSingleton(sortedNumeric);
    if (singleton != null) {
      // it's actually single-valued in practice, but indexed as multi-valued,
      // so just sort on the underlying single-valued dv directly.
      // regardless of selector type, this optimization is safe!
      view = singleton;
    } else {
      switch (selector) {
        case MIN:
          view = new MinValue(sortedNumeric);
          break;
        case MAX:
          view = new MaxValue(sortedNumeric);
          break;
        default:
          throw new AssertionError();
      }
    }
    // undo the numericutils sortability
    switch (numericType) {
      case FLOAT:
        return new FilterNumericDocValues(view) {
          @Override
          public long longValue() throws IOException {
            return NumericUtils.sortableFloatBits((int) in.longValue());
          }
        };
      case DOUBLE:
        return new FilterNumericDocValues(view) {
          @Override
          public long longValue() throws IOException {
            return NumericUtils.sortableDoubleBits(in.longValue());
          }
        };
      case INT:
      case LONG:
      case CUSTOM:
      case DOC:
      case REWRITEABLE:
      case STRING:
      case STRING_VAL:
      case SCORE:
      default:
        return view;
    }
  }

  /** Wraps a SortedNumericDocValues and returns the first value (min) */
  static class MinValue extends NumericDocValues {
    final SortedNumericDocValues in;
    private long value;

    MinValue(SortedNumericDocValues in) {
      this.in = in;
    }

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

    @Override
    public int nextDoc() throws IOException {
      int docID = in.nextDoc();
      if (docID != NO_MORE_DOCS) {
        value = in.nextValue();
      }
      return docID;
    }

    @Override
    public int advance(int target) throws IOException {
      int docID = in.advance(target);
      if (docID != NO_MORE_DOCS) {
        value = in.nextValue();
      }
      return docID;
    }

    @Override
    public boolean advanceExact(int target) throws IOException {
      if (in.advanceExact(target)) {
        value = in.nextValue();
        return true;
      }
      return false;
    }

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

    @Override
    public long longValue() {
      return value;
    }
  }

  /** Wraps a SortedNumericDocValues and returns the last value (max) */
  static class MaxValue extends NumericDocValues {
    final SortedNumericDocValues in;
    private long value;

    MaxValue(SortedNumericDocValues in) {
      this.in = in;
    }

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

    private void setValue() throws IOException {
      int count = in.docValueCount();
      for (int i = 0; i < count; i++) {
        value = in.nextValue();
      }
    }

    @Override
    public int nextDoc() throws IOException {
      int docID = in.nextDoc();
      if (docID != NO_MORE_DOCS) {
        setValue();
      }
      return docID;
    }

    @Override
    public int advance(int target) throws IOException {
      int docID = in.advance(target);
      if (docID != NO_MORE_DOCS) {
        setValue();
      }
      return docID;
    }

    @Override
    public boolean advanceExact(int target) throws IOException {
      if (in.advanceExact(target)) {
        setValue();
        return true;
      }
      return false;
    }

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

    @Override
    public long longValue() {
      return value;
    }
  }
}
