/*
 * 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.solr.schema;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.IndexOrDocValuesQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortedNumericSelector;
import org.apache.lucene.search.SortField;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
import org.apache.lucene.util.CharsRef;
import org.apache.lucene.util.CharsRefBuilder;
import org.apache.lucene.util.NumericUtils;
import org.apache.solr.common.SolrException;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.search.QParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provides field types to support for Lucene's {@link
 * org.apache.lucene.document.IntPoint}, {@link org.apache.lucene.document.LongPoint}, {@link org.apache.lucene.document.FloatPoint} and
 * {@link org.apache.lucene.document.DoublePoint}.
 * See {@link org.apache.lucene.search.PointRangeQuery} for more details.
 * It supports integer, float, long and double types. See subclasses for details.
 * <br>
 * {@code DocValues} are supported for single-value cases ({@code NumericDocValues}).
 * {@code FieldCache} is not supported for {@code PointField}s, so sorting, faceting, etc on these fields require the use of {@code docValues="true"} in the schema.
 */
public abstract class PointField extends NumericFieldType {

  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  
  /**
   * <p>
   * The Test framework can set this global variable to instruct PointField that
   * (on init) it should be tollerant of the <code>precisionStep</code> argument used by TrieFields.
   * This allows for simple randomization of TrieFields and PointFields w/o extensive duplication
   * of <code>&lt;fieldType/&gt;</code> declarations.
   * </p>
   *
   * <p>NOTE: When {@link TrieField} is removed, this boolean must also be removed</p>
   *
   * @lucene.internal
   * @lucene.experimental
   */
  public static boolean TEST_HACK_IGNORE_USELESS_TRIEFIELD_ARGS = false;
  
  /** 
   * NOTE: This method can be removed completely when
   * {@link #TEST_HACK_IGNORE_USELESS_TRIEFIELD_ARGS} is removed 
   */
  @Override
  protected void init(IndexSchema schema, Map<String, String> args) {
    super.init(schema, args);
    if (TEST_HACK_IGNORE_USELESS_TRIEFIELD_ARGS) {
      args.remove("precisionStep");
    }
  }

  @Override
  public boolean isPointField() {
    return true;
  }
  
  @Override
  public final ValueSource getSingleValueSource(MultiValueSelector choice, SchemaField field, QParser parser) {
    // trivial base case
    if (!field.multiValued()) {
      // single value matches any selector
      return getValueSource(field, parser);
    }

    // Point fields don't support UninvertingReader. See SOLR-9202
    if (!field.hasDocValues()) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
                              "docValues='true' is required to select '" + choice.toString() +
                              "' value from multivalued field ("+ field.getName() +") at query time");
    }
    
    // multivalued Point fields all use SortedSetDocValues, so we give a clean error if that's
    // not supported by the specified choice, else we delegate to a helper
    SortedNumericSelector.Type selectorType = choice.getSortedNumericSelectorType();
    if (null == selectorType) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
                              choice.toString() + " is not a supported option for picking a single value"
                              + " from the multivalued field: " + field.getName() +
                              " (type: " + this.getTypeName() + ")");
    }
    
    return getSingleValueSource(selectorType, field);
  }

  /**
   * Helper method that will only be called for multivalued Point fields that have doc values.
   * Default impl throws an error indicating that selecting a single value from this multivalued 
   * field is not supported for this field type
   *
   * @param choice the selector Type to use, will never be null
   * @param field the field to use, guaranteed to be multivalued.
   * @see #getSingleValueSource(MultiValueSelector,SchemaField,QParser) 
   */
  protected abstract ValueSource getSingleValueSource(SortedNumericSelector.Type choice, SchemaField field);

  @Override
  public boolean isTokenized() {
    return false;
  }

  @Override
  public boolean multiValuedFieldCache() {
    return false;
  }

  @Override
  public Query getSetQuery(QParser parser, SchemaField field, Collection<String> externalVals) {
    return super.getSetQuery(parser, field, externalVals);
  }

  @Override
  public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) {
    if (!field.indexed() && field.hasDocValues()) {
      // currently implemented as singleton range
      return getRangeQuery(parser, field, externalVal, externalVal, true, true);
    } else if (field.indexed() && field.hasDocValues()) {
      Query pointsQuery = getExactQuery(field, externalVal);
      Query dvQuery = getDocValuesRangeQuery(parser, field, externalVal, externalVal, true, true);
      return new IndexOrDocValuesQuery(pointsQuery, dvQuery);
    } else {
      return getExactQuery(field, externalVal);
    } 
  }

  protected abstract Query getExactQuery(SchemaField field, String externalVal);

  public abstract Query getPointRangeQuery(QParser parser, SchemaField field, String min, String max, boolean minInclusive,
      boolean maxInclusive);

  @Override
  public Query getRangeQuery(QParser parser, SchemaField field, String min, String max, boolean minInclusive,
      boolean maxInclusive) {
    if (!field.indexed() && field.hasDocValues()) {
      return getDocValuesRangeQuery(parser, field, min, max, minInclusive, maxInclusive);
    } else if (field.indexed() && field.hasDocValues()) {
      Query pointsQuery = getPointRangeQuery(parser, field, min, max, minInclusive, maxInclusive);
      Query dvQuery = getDocValuesRangeQuery(parser, field, min, max, minInclusive, maxInclusive);
      return new IndexOrDocValuesQuery(pointsQuery, dvQuery);
    } else {
      return getPointRangeQuery(parser, field, min, max, minInclusive, maxInclusive);
    }
  }

  @Override
  public String storedToReadable(IndexableField f) {
    return toExternal(f);
  }

  @Override
  public String toInternal(String val) {
    throw new UnsupportedOperationException("Can't generate internal string in PointField. use PointField.toInternalByteRef");
  }
  
  public BytesRef toInternalByteRef(String val) {
    final BytesRefBuilder bytes = new BytesRefBuilder();
    readableToIndexed(val, bytes);
    return bytes.get();
  }
  
  @Override
  public void write(TextResponseWriter writer, String name, IndexableField f) throws IOException {
    writer.writeVal(name, toObject(f));
  }

  @Override
  public String storedToIndexed(IndexableField f) {
    throw new UnsupportedOperationException("Not supported with PointFields");
  }
  
  @Override
  public CharsRef indexedToReadable(BytesRef indexedForm, CharsRefBuilder charsRef) {
    final String value = indexedToReadable(indexedForm);
    charsRef.grow(value.length());
    charsRef.setLength(value.length());
    value.getChars(0, charsRef.length(), charsRef.chars(), 0);
    return charsRef.get();
  }
  
  @Override
  public String indexedToReadable(String indexedForm) {
    return indexedToReadable(new BytesRef(indexedForm));
  }
  
  protected abstract String indexedToReadable(BytesRef indexedForm);
  
  @Override
  public Query getPrefixQuery(QParser parser, SchemaField sf, String termStr) {
    throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Can't run prefix queries on numeric fields");
  }
  
  protected boolean isFieldUsed(SchemaField field) {
    boolean indexed = field.indexed();
    boolean stored = field.stored();
    boolean docValues = field.hasDocValues();

    if (!indexed && !stored && !docValues) {
      if (log.isTraceEnabled()) {
        log.trace("Ignoring unindexed/unstored field: " + field);
      }
      return false;
    }
    return true;
  }

  @Override
  public List<IndexableField> createFields(SchemaField sf, Object value) {
    if (!isFieldUsed(sf)) {
      return Collections.emptyList();
    }
    List<IndexableField> fields = new ArrayList<>(3);
    IndexableField field = null;
    if (sf.indexed()) {
      field = createField(sf, value);
      fields.add(field);
    }
    
    if (sf.hasDocValues()) {
      final Number numericValue;
      if (field == null) {
        final Object nativeTypeObject = toNativeType(value);
        if (getNumberType() == NumberType.DATE) {
          numericValue = ((Date)nativeTypeObject).getTime();
        } else {
          numericValue = (Number) nativeTypeObject;
        }
      } else {
        numericValue = field.numericValue();
      }
      final long bits;
      if (!sf.multiValued()) {
        if (numericValue instanceof Integer || numericValue instanceof Long) {
          bits = numericValue.longValue();
        } else if (numericValue instanceof Float) {
          bits = Float.floatToIntBits(numericValue.floatValue());
        } else {
          assert numericValue instanceof Double;
          bits = Double.doubleToLongBits(numericValue.doubleValue());
        }
        fields.add(new NumericDocValuesField(sf.getName(), bits));
      } else {
        // MultiValued
        if (numericValue instanceof Integer || numericValue instanceof Long) {
          bits = numericValue.longValue();
        } else if (numericValue instanceof Float) {
          bits = NumericUtils.floatToSortableInt(numericValue.floatValue());
        } else {
          assert numericValue instanceof Double;
          bits = NumericUtils.doubleToSortableLong(numericValue.doubleValue());
        }
        fields.add(new SortedNumericDocValuesField(sf.getName(), bits));
      }
    } 
    if (sf.stored()) {
      fields.add(getStoredField(sf, value));
    }
    return fields;
  }

  protected abstract StoredField getStoredField(SchemaField sf, Object value);

  @Override
  public SortField getSortField(SchemaField field, boolean top) {
    return getNumericSort(field, getNumberType(), top);
  }
  
}
