/*
 * 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.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.document.StoredValue;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.valuesource.MultiValuedIntFieldSource;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.SortedNumericSelector;
import org.apache.lucene.search.TermRangeQuery;
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.EnumFieldValue;
import org.apache.solr.common.SolrException;
import org.apache.solr.search.QParser;
import org.apache.solr.uninverting.UninvertingReader.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Field type for support of string values with custom sort order. */
public class EnumFieldType extends AbstractEnumField {
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  @Override
  public Type getUninversionType(SchemaField sf) {
    return null;
  }

  @Override
  protected Query getSpecializedRangeQuery(
      QParser parser,
      SchemaField field,
      String min,
      String max,
      boolean minInclusive,
      boolean maxInclusive) {
    Integer minValue = enumMapping.stringValueToIntValue(min);
    Integer maxValue = enumMapping.stringValueToIntValue(max);

    if (field.indexed()) {
      BytesRef minBytes = null;
      if (min != null) {
        byte[] bytes = new byte[Integer.BYTES];
        NumericUtils.intToSortableBytes(minValue, bytes, 0);
        minBytes = new BytesRef(bytes);
      }
      BytesRef maxBytes = null;
      if (max != null) {
        byte[] bytes = new byte[Integer.BYTES];
        NumericUtils.intToSortableBytes(maxValue, bytes, 0);
        maxBytes = new BytesRef(bytes);
      }
      return new TermRangeQuery(field.getName(), minBytes, maxBytes, minInclusive, maxInclusive);

    } else {
      long lowerValue = Long.MIN_VALUE;
      long upperValue = Long.MAX_VALUE;
      if (minValue != null) {
        lowerValue = minValue.longValue();
        if (minInclusive == false) {
          ++lowerValue;
        }
      }
      if (maxValue != null) {
        upperValue = maxValue.longValue();
        if (maxInclusive == false) {
          --upperValue;
        }
      }
      if (field.multiValued()) {
        return new ConstantScoreQuery(
            SortedNumericDocValuesField.newSlowRangeQuery(field.getName(), lowerValue, upperValue));
      } else {
        return new ConstantScoreQuery(
            NumericDocValuesField.newSlowRangeQuery(field.getName(), lowerValue, upperValue));
      }
    }
  }

  @Override
  public void readableToIndexed(CharSequence val, BytesRefBuilder result) {
    final String s = val.toString();
    if (s == null) return;

    result.grow(Integer.BYTES);
    result.setLength(Integer.BYTES);
    final Integer intValue = enumMapping.stringValueToIntValue(s);
    NumericUtils.intToSortableBytes(intValue, result.bytes(), 0);
  }

  @Override
  public String indexedToReadable(String indexedForm) {
    if (indexedForm == null) return null;
    final BytesRef bytesRef = new BytesRef(indexedForm);
    final Integer intValue = NumericUtils.sortableBytesToInt(bytesRef.bytes, 0);
    return enumMapping.intValueToStringValue(intValue);
  }

  @Override
  public CharsRef indexedToReadable(BytesRef input, CharsRefBuilder output) {
    final Integer intValue = NumericUtils.sortableBytesToInt(input.bytes, 0);
    final String stringValue = enumMapping.intValueToStringValue(intValue);
    output.grow(stringValue.length());
    output.setLength(stringValue.length());
    stringValue.getChars(0, output.length(), output.chars(), 0);
    return output.get();
  }

  @Override
  public EnumFieldValue toObject(SchemaField sf, BytesRef term) {
    final Integer intValue = NumericUtils.sortableBytesToInt(term.bytes, 0);
    final String stringValue = enumMapping.intValueToStringValue(intValue);
    return new EnumFieldValue(intValue, stringValue);
  }

  @Override
  public String storedToIndexed(IndexableField f) {
    final Number val = f.numericValue();
    if (val == null) return null;
    final BytesRefBuilder bytes = new BytesRefBuilder();
    bytes.grow(Integer.BYTES);
    bytes.setLength(Integer.BYTES);
    NumericUtils.intToSortableBytes(val.intValue(), bytes.bytes(), 0);
    return bytes.get().utf8ToString();
  }

  @Override
  public IndexableField createField(SchemaField field, Object value) {
    final Integer intValue = enumMapping.stringValueToIntValue(value.toString());
    if (intValue == null || intValue.equals(EnumMapping.DEFAULT_VALUE)) {
      String exceptionMessage =
          String.format(
              Locale.ENGLISH,
              "Unknown value for enum field: %s, value: %s",
              field.getName(),
              value.toString());
      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, exceptionMessage);
    }

    org.apache.lucene.document.FieldType newType = new org.apache.lucene.document.FieldType();
    newType.setTokenized(false);
    newType.setStored(field.stored());
    newType.setOmitNorms(field.omitNorms());
    newType.setIndexOptions(field.indexOptions());
    newType.setStoreTermVectors(field.storeTermVector());
    newType.setStoreTermVectorOffsets(field.storeTermOffsets());
    newType.setStoreTermVectorPositions(field.storeTermPositions());
    newType.setStoreTermVectorPayloads(field.storeTermPayloads());

    byte[] bytes = new byte[Integer.BYTES];
    NumericUtils.intToSortableBytes(intValue, bytes, 0);
    return new Field(field.getName(), bytes, newType) {
      @Override
      public Number numericValue() {
        return NumericUtils.sortableBytesToInt(((BytesRef) fieldsData).bytes, 0);
      }

      @Override
      public StoredValue storedValue() {
        return new StoredValue(NumericUtils.sortableBytesToInt(((BytesRef) fieldsData).bytes, 0));
      }
    };
  }

  @Override
  public List<IndexableField> createFields(SchemaField sf, Object value) {
    if (!sf.hasDocValues()) {
      throw new SolrException(
          SolrException.ErrorCode.SERVER_ERROR,
          getClass().getSimpleName() + " requires docValues=\"true\".");
    }
    final IndexableField field = createField(sf, value);
    final List<IndexableField> fields = new ArrayList<>();
    fields.add(field);
    final long longValue = field.numericValue().longValue();
    if (sf.multiValued()) {
      fields.add(new SortedNumericDocValuesField(sf.getName(), longValue));
    } else {
      fields.add(new NumericDocValuesField(sf.getName(), longValue));
    }
    return fields;
  }

  @Override
  public final ValueSource getSingleValueSource(
      MultiValueSelector choice, SchemaField field, QParser parser) {
    if (!field.multiValued()) { // trivial base case
      return getValueSource(field, parser); // single value matches any selector
    }
    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 new MultiValuedIntFieldSource(field.getName(), selectorType);
  }

  @Override
  public SortField getSortField(SchemaField field, boolean top) {
    final SortField result = getNumericSort(field, NumberType.INTEGER, top);
    if (null == result.getMissingValue()) {
      // special case 'enum' default behavior: assume missing values are "below" all enum values
      result.setMissingValue(Integer.MIN_VALUE);
    }
    return result;
  }
}
