/*
 * 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.IndexSorter;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.index.SortFieldProvider;
import org.apache.lucene.index.SortedNumericDocValues;
import org.apache.lucene.search.comparators.DoubleComparator;
import org.apache.lucene.search.comparators.FloatComparator;
import org.apache.lucene.search.comparators.IntComparator;
import org.apache.lucene.search.comparators.LongComparator;
import org.apache.lucene.store.DataInput;
import org.apache.lucene.store.DataOutput;
import org.apache.lucene.util.NumericUtils;

/**
 * SortField for {@link SortedNumericDocValues}.
 *
 * <p>A SortedNumericDocValues contains multiple values for a field, so sorting with this technique
 * "selects" a value as the representative sort value for the document.
 *
 * <p>By default, the minimum value in the list is selected as the sort value, but this can be
 * customized.
 *
 * <p>Like sorting by string, this also supports sorting missing values as first or last, via {@link
 * #setMissingValue(Object)}.
 *
 * @see SortedNumericSelector
 */
public class SortedNumericSortField extends SortField {

  private final SortedNumericSelector.Type selector;
  private final SortField.Type type;

  /**
   * Creates a sort, by the minimum value in the set for the document.
   *
   * @param field Name of field to sort by. Must not be null.
   * @param type Type of values
   */
  public SortedNumericSortField(String field, SortField.Type type) {
    this(field, type, false);
  }

  /**
   * Creates a sort, possibly in reverse, by the minimum value in the set for the document.
   *
   * @param field Name of field to sort by. Must not be null.
   * @param type Type of values
   * @param reverse True if natural order should be reversed.
   */
  public SortedNumericSortField(String field, SortField.Type type, boolean reverse) {
    this(field, type, reverse, SortedNumericSelector.Type.MIN, null);
  }

  /**
   * Creates a sort, possibly in reverse, specifying how the sort value from the document's set is
   * selected.
   *
   * @param field Name of field to sort by. Must not be null.
   * @param type Type of values
   * @param reverse True if natural order should be reversed.
   * @param selector custom selector type for choosing the sort value from the set.
   */
  public SortedNumericSortField(
      String field, SortField.Type type, boolean reverse, SortedNumericSelector.Type selector) {
    this(field, type, reverse, selector, null);
  }

  /**
   * Creates a sort, possibly in reverse, specifying how the sort value from the document's set is
   * selected.
   *
   * @param field Name of field to sort by. Must not be null.
   * @param type Type of values
   * @param reverse True if natural order should be reversed.
   * @param selector custom selector type for choosing the sort value from the set.
   * @param missingValue the sort value to use for documents with no value in the field
   */
  public SortedNumericSortField(
      String field,
      SortField.Type type,
      boolean reverse,
      SortedNumericSelector.Type selector,
      Object missingValue) {
    super(field, SortField.Type.CUSTOM, reverse, missingValue);
    if (selector == null) {
      throw new NullPointerException();
    }
    if (type == null) {
      throw new NullPointerException();
    }
    this.selector = selector;
    this.type = type;
  }

  /** A SortFieldProvider for this sort field */
  public static final class Provider extends SortFieldProvider {

    /** The name this provider is registered under */
    public static final String NAME = "SortedNumericSortField";

    /** Creates a new Provider */
    public Provider() {
      super(NAME);
    }

    @Override
    public SortField readSortField(DataInput in) throws IOException {
      String field = in.readString();
      Type type = readType(in);
      boolean reverse = in.readInt() == 1;
      SortedNumericSelector.Type selectorType = readSelectorType(in);
      if (in.readInt() == 1) {
        return switch (type) {
          case INT -> new SortedNumericSortField(field, type, reverse, selectorType, in.readInt());
          case LONG ->
              new SortedNumericSortField(field, type, reverse, selectorType, in.readLong());
          case FLOAT ->
              new SortedNumericSortField(
                  field,
                  type,
                  reverse,
                  selectorType,
                  NumericUtils.sortableIntToFloat(in.readInt()));
          case DOUBLE ->
              new SortedNumericSortField(
                  field,
                  type,
                  reverse,
                  selectorType,
                  NumericUtils.sortableLongToDouble(in.readLong()));
          // $CASES-OMITTED$
          default -> throw new AssertionError();
        };
      }
      return new SortedNumericSortField(field, type, reverse, selectorType, null);
    }

    @Override
    public void writeSortField(SortField sf, DataOutput out) throws IOException {
      assert sf instanceof SortedNumericSortField;
      ((SortedNumericSortField) sf).serialize(out);
    }
  }

  private static SortedNumericSelector.Type readSelectorType(DataInput in) throws IOException {
    int selectorType = in.readInt();
    if (selectorType >= SortedNumericSelector.Type.values().length) {
      throw new IllegalArgumentException(
          "Can't deserialize SortedNumericSortField - unknown selector type " + selectorType);
    }
    return SortedNumericSelector.Type.values()[selectorType];
  }

  private void serialize(DataOutput out) throws IOException {
    out.writeString(getField());
    out.writeString(type.toString());
    out.writeInt(reverse ? 1 : 0);
    out.writeInt(selector.ordinal());
    if (missingValue == null) {
      out.writeInt(0);
    } else {
      out.writeInt(1);
      // oh for switch expressions...
      switch (type) {
        case INT:
          out.writeInt((int) missingValue);
          break;
        case LONG:
          out.writeLong((long) missingValue);
          break;
        case FLOAT:
          out.writeInt(NumericUtils.floatToSortableInt((float) missingValue));
          break;
        case DOUBLE:
          out.writeLong(NumericUtils.doubleToSortableLong((double) missingValue));
          break;
        case CUSTOM:
        case DOC:
        case REWRITEABLE:
        case STRING_VAL:
        case SCORE:
        case STRING:
        default:
          throw new AssertionError();
      }
    }
  }

  /** Returns the numeric type in use for this sort */
  public SortField.Type getNumericType() {
    return type;
  }

  /** Returns the selector in use for this sort */
  public SortedNumericSelector.Type getSelector() {
    return selector;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + selector.hashCode();
    result = prime * result + type.hashCode();
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!super.equals(obj)) return false;
    if (getClass() != obj.getClass()) return false;
    SortedNumericSortField other = (SortedNumericSortField) obj;
    if (selector != other.selector) return false;
    if (type != other.type) return false;
    return true;
  }

  @Override
  public String toString() {
    StringBuilder buffer = new StringBuilder();
    buffer.append("<sortednumeric" + ": \"").append(getField()).append("\">");
    if (getReverse()) buffer.append('!');
    if (missingValue != null) {
      buffer.append(" missingValue=");
      buffer.append(missingValue);
    }
    buffer.append(" selector=");
    buffer.append(selector);
    buffer.append(" type=");
    buffer.append(type);

    return buffer.toString();
  }

  @Override
  public void setMissingValue(Object missingValue) {
    this.missingValue = missingValue;
  }

  @Override
  public FieldComparator<?> getComparator(int numHits, Pruning pruning) {
    final FieldComparator<?> fieldComparator;
    // we can use sort optimization with points if selector is MIN or MAX,
    // because we can still build successful iterator over points in this case.
    boolean isMinOrMax =
        selector == SortedNumericSelector.Type.MAX || selector == SortedNumericSelector.Type.MIN;
    switch (type) {
      case INT:
        fieldComparator =
            new IntComparator(
                numHits,
                getField(),
                (Integer) missingValue,
                reverse,
                isMinOrMax ? pruning : Pruning.NONE) {
              @Override
              public LeafFieldComparator getLeafComparator(LeafReaderContext context)
                  throws IOException {
                return new IntLeafComparator(context) {
                  @Override
                  protected NumericDocValues getNumericDocValues(
                      LeafReaderContext context, String field) throws IOException {
                    return SortedNumericSelector.wrap(
                        DocValues.getSortedNumeric(context.reader(), field), selector, type);
                  }
                };
              }
            };
        break;
      case FLOAT:
        fieldComparator =
            new FloatComparator(
                numHits,
                getField(),
                (Float) missingValue,
                reverse,
                isMinOrMax ? pruning : Pruning.NONE) {
              @Override
              public LeafFieldComparator getLeafComparator(LeafReaderContext context)
                  throws IOException {
                return new FloatLeafComparator(context) {
                  @Override
                  protected NumericDocValues getNumericDocValues(
                      LeafReaderContext context, String field) throws IOException {
                    return SortedNumericSelector.wrap(
                        DocValues.getSortedNumeric(context.reader(), field), selector, type);
                  }
                };
              }
            };
        break;
      case LONG:
        fieldComparator =
            new LongComparator(
                numHits,
                getField(),
                (Long) missingValue,
                reverse,
                isMinOrMax ? pruning : Pruning.NONE) {
              @Override
              public LeafFieldComparator getLeafComparator(LeafReaderContext context)
                  throws IOException {
                return new LongLeafComparator(context) {
                  @Override
                  protected NumericDocValues getNumericDocValues(
                      LeafReaderContext context, String field) throws IOException {
                    return SortedNumericSelector.wrap(
                        DocValues.getSortedNumeric(context.reader(), field), selector, type);
                  }
                };
              }
            };
        break;
      case DOUBLE:
        fieldComparator =
            new DoubleComparator(
                numHits,
                getField(),
                (Double) missingValue,
                reverse,
                isMinOrMax ? pruning : Pruning.NONE) {
              @Override
              public LeafFieldComparator getLeafComparator(LeafReaderContext context)
                  throws IOException {
                return new DoubleLeafComparator(context) {
                  @Override
                  protected NumericDocValues getNumericDocValues(
                      LeafReaderContext context, String field) throws IOException {
                    return SortedNumericSelector.wrap(
                        DocValues.getSortedNumeric(context.reader(), field), selector, type);
                  }
                };
              }
            };
        break;
      case CUSTOM:
      case DOC:
      case REWRITEABLE:
      case STRING_VAL:
      case SCORE:
      case STRING:
      default:
        throw new AssertionError();
    }
    if (getOptimizeSortWithIndexedData() == false) {
      fieldComparator.disableSkipping();
    }
    return fieldComparator;
  }

  private NumericDocValues getValue(LeafReader reader) throws IOException {
    return SortedNumericSelector.wrap(
        DocValues.getSortedNumeric(reader, getField()), selector, type);
  }

  @Override
  public IndexSorter getIndexSorter() {
    switch (type) {
      case INT:
        return new IndexSorter.IntSorter(
            Provider.NAME, (Integer) missingValue, reverse, this::getValue);
      case LONG:
        return new IndexSorter.LongSorter(
            Provider.NAME, (Long) missingValue, reverse, this::getValue);
      case DOUBLE:
        return new IndexSorter.DoubleSorter(
            Provider.NAME, (Double) missingValue, reverse, this::getValue);
      case FLOAT:
        return new IndexSorter.FloatSorter(
            Provider.NAME, (Float) missingValue, reverse, this::getValue);
      case CUSTOM:
      case DOC:
      case REWRITEABLE:
      case STRING_VAL:
      case STRING:
      case SCORE:
      default:
        throw new AssertionError();
    }
  }
}
