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

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.lucene.analysis.Analyzer; // javadocs
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexableFieldType;
import org.apache.lucene.index.PointValues;
import org.apache.lucene.index.VectorEncoding;
import org.apache.lucene.index.VectorSimilarityFunction;

/** Describes the properties of a field. */
public class FieldType implements IndexableFieldType {

  private boolean stored;
  private boolean tokenized = true;
  private boolean storeTermVectors;
  private boolean storeTermVectorOffsets;
  private boolean storeTermVectorPositions;
  private boolean storeTermVectorPayloads;
  private boolean omitNorms;
  private IndexOptions indexOptions = IndexOptions.NONE;
  private boolean frozen;
  private DocValuesType docValuesType = DocValuesType.NONE;
  private int dimensionCount;
  private int indexDimensionCount;
  private int dimensionNumBytes;
  private int vectorDimension;
  private VectorEncoding vectorEncoding = VectorEncoding.FLOAT32;
  private VectorSimilarityFunction vectorSimilarityFunction = VectorSimilarityFunction.EUCLIDEAN;
  private Map<String, String> attributes;

  /** Create a new mutable FieldType with all of the properties from <code>ref</code> */
  public FieldType(IndexableFieldType ref) {
    this.stored = ref.stored();
    this.tokenized = ref.tokenized();
    this.storeTermVectors = ref.storeTermVectors();
    this.storeTermVectorOffsets = ref.storeTermVectorOffsets();
    this.storeTermVectorPositions = ref.storeTermVectorPositions();
    this.storeTermVectorPayloads = ref.storeTermVectorPayloads();
    this.omitNorms = ref.omitNorms();
    this.indexOptions = ref.indexOptions();
    this.docValuesType = ref.docValuesType();
    this.dimensionCount = ref.pointDimensionCount();
    this.indexDimensionCount = ref.pointIndexDimensionCount();
    this.dimensionNumBytes = ref.pointNumBytes();
    this.vectorDimension = ref.vectorDimension();
    this.vectorEncoding = ref.vectorEncoding();
    this.vectorSimilarityFunction = ref.vectorSimilarityFunction();
    if (ref.getAttributes() != null) {
      this.attributes = new HashMap<>(ref.getAttributes());
    }
    // Do not copy frozen!
  }

  /** Create a new FieldType with default properties. */
  public FieldType() {}

  /**
   * Throws an exception if this FieldType is frozen. Subclasses should call this within setters for
   * additional state.
   */
  protected void checkIfFrozen() {
    if (frozen) {
      throw new IllegalStateException("this FieldType is already frozen and cannot be changed");
    }
  }

  /**
   * Prevents future changes. Note, it is recommended that this is called once the FieldTypes's
   * properties have been set, to prevent unintentional state changes.
   */
  public void freeze() {
    this.frozen = true;
  }

  /**
   * {@inheritDoc}
   *
   * <p>The default is <code>false</code>.
   *
   * @see #setStored(boolean)
   */
  @Override
  public boolean stored() {
    return this.stored;
  }

  /**
   * Set to <code>true</code> to store this field.
   *
   * @param value true if this field should be stored.
   * @throws IllegalStateException if this FieldType is frozen against future modifications.
   * @see #stored()
   */
  public void setStored(boolean value) {
    checkIfFrozen();
    this.stored = value;
  }

  /**
   * {@inheritDoc}
   *
   * <p>The default is <code>true</code>.
   *
   * @see #setTokenized(boolean)
   */
  @Override
  public boolean tokenized() {
    return this.tokenized;
  }

  /**
   * Set to <code>true</code> to tokenize this field's contents via the configured {@link Analyzer}.
   *
   * @param value true if this field should be tokenized.
   * @throws IllegalStateException if this FieldType is frozen against future modifications.
   * @see #tokenized()
   */
  public void setTokenized(boolean value) {
    checkIfFrozen();
    this.tokenized = value;
  }

  /**
   * {@inheritDoc}
   *
   * <p>The default is <code>false</code>.
   *
   * @see #setStoreTermVectors(boolean)
   */
  @Override
  public boolean storeTermVectors() {
    return this.storeTermVectors;
  }

  /**
   * Set to <code>true</code> if this field's indexed form should be also stored into term vectors.
   *
   * @param value true if this field should store term vectors.
   * @throws IllegalStateException if this FieldType is frozen against future modifications.
   * @see #storeTermVectors()
   */
  public void setStoreTermVectors(boolean value) {
    checkIfFrozen();
    this.storeTermVectors = value;
  }

  /**
   * {@inheritDoc}
   *
   * <p>The default is <code>false</code>.
   *
   * @see #setStoreTermVectorOffsets(boolean)
   */
  @Override
  public boolean storeTermVectorOffsets() {
    return this.storeTermVectorOffsets;
  }

  /**
   * Set to <code>true</code> to also store token character offsets into the term vector for this
   * field.
   *
   * @param value true if this field should store term vector offsets.
   * @throws IllegalStateException if this FieldType is frozen against future modifications.
   * @see #storeTermVectorOffsets()
   */
  public void setStoreTermVectorOffsets(boolean value) {
    checkIfFrozen();
    this.storeTermVectorOffsets = value;
  }

  /**
   * {@inheritDoc}
   *
   * <p>The default is <code>false</code>.
   *
   * @see #setStoreTermVectorPositions(boolean)
   */
  @Override
  public boolean storeTermVectorPositions() {
    return this.storeTermVectorPositions;
  }

  /**
   * Set to <code>true</code> to also store token positions into the term vector for this field.
   *
   * @param value true if this field should store term vector positions.
   * @throws IllegalStateException if this FieldType is frozen against future modifications.
   * @see #storeTermVectorPositions()
   */
  public void setStoreTermVectorPositions(boolean value) {
    checkIfFrozen();
    this.storeTermVectorPositions = value;
  }

  /**
   * {@inheritDoc}
   *
   * <p>The default is <code>false</code>.
   *
   * @see #setStoreTermVectorPayloads(boolean)
   */
  @Override
  public boolean storeTermVectorPayloads() {
    return this.storeTermVectorPayloads;
  }

  /**
   * Set to <code>true</code> to also store token payloads into the term vector for this field.
   *
   * @param value true if this field should store term vector payloads.
   * @throws IllegalStateException if this FieldType is frozen against future modifications.
   * @see #storeTermVectorPayloads()
   */
  public void setStoreTermVectorPayloads(boolean value) {
    checkIfFrozen();
    this.storeTermVectorPayloads = value;
  }

  /**
   * {@inheritDoc}
   *
   * <p>The default is <code>false</code>.
   *
   * @see #setOmitNorms(boolean)
   */
  @Override
  public boolean omitNorms() {
    return this.omitNorms;
  }

  /**
   * Set to <code>true</code> to omit normalization values for the field.
   *
   * @param value true if this field should omit norms.
   * @throws IllegalStateException if this FieldType is frozen against future modifications.
   * @see #omitNorms()
   */
  public void setOmitNorms(boolean value) {
    checkIfFrozen();
    this.omitNorms = value;
  }

  /**
   * {@inheritDoc}
   *
   * <p>The default is {@link IndexOptions#DOCS_AND_FREQS_AND_POSITIONS}.
   *
   * @see #setIndexOptions(IndexOptions)
   */
  @Override
  public IndexOptions indexOptions() {
    return this.indexOptions;
  }

  /**
   * Sets the indexing options for the field:
   *
   * @param value indexing options
   * @throws IllegalStateException if this FieldType is frozen against future modifications.
   * @see #indexOptions()
   */
  public void setIndexOptions(IndexOptions value) {
    checkIfFrozen();
    if (value == null) {
      throw new NullPointerException("IndexOptions must not be null");
    }
    this.indexOptions = value;
  }

  /** Enables points indexing. */
  public void setDimensions(int dimensionCount, int dimensionNumBytes) {
    this.setDimensions(dimensionCount, dimensionCount, dimensionNumBytes);
  }

  /** Enables points indexing with selectable dimension indexing. */
  public void setDimensions(int dimensionCount, int indexDimensionCount, int dimensionNumBytes) {
    checkIfFrozen();
    if (dimensionCount < 0) {
      throw new IllegalArgumentException("dimensionCount must be >= 0; got " + dimensionCount);
    }
    if (dimensionCount > PointValues.MAX_DIMENSIONS) {
      throw new IllegalArgumentException(
          "dimensionCount must be <= " + PointValues.MAX_DIMENSIONS + "; got " + dimensionCount);
    }
    if (indexDimensionCount < 0) {
      throw new IllegalArgumentException(
          "indexDimensionCount must be >= 0; got " + indexDimensionCount);
    }
    if (indexDimensionCount > dimensionCount) {
      throw new IllegalArgumentException(
          "indexDimensionCount must be <= dimensionCount: "
              + dimensionCount
              + "; got "
              + indexDimensionCount);
    }
    if (indexDimensionCount > PointValues.MAX_INDEX_DIMENSIONS) {
      throw new IllegalArgumentException(
          "indexDimensionCount must be <= "
              + PointValues.MAX_INDEX_DIMENSIONS
              + "; got "
              + indexDimensionCount);
    }
    if (dimensionNumBytes < 0) {
      throw new IllegalArgumentException(
          "dimensionNumBytes must be >= 0; got " + dimensionNumBytes);
    }
    if (dimensionNumBytes > PointValues.MAX_NUM_BYTES) {
      throw new IllegalArgumentException(
          "dimensionNumBytes must be <= "
              + PointValues.MAX_NUM_BYTES
              + "; got "
              + dimensionNumBytes);
    }
    if (dimensionCount == 0) {
      if (indexDimensionCount != 0) {
        throw new IllegalArgumentException(
            "when dimensionCount is 0, indexDimensionCount must be 0; got " + indexDimensionCount);
      }
      if (dimensionNumBytes != 0) {
        throw new IllegalArgumentException(
            "when dimensionCount is 0, dimensionNumBytes must be 0; got " + dimensionNumBytes);
      }
    } else if (indexDimensionCount == 0) {
      throw new IllegalArgumentException(
          "when dimensionCount is > 0, indexDimensionCount must be > 0; got "
              + indexDimensionCount);
    } else if (dimensionNumBytes == 0) {
      throw new IllegalArgumentException(
          "when dimensionNumBytes is 0, dimensionCount must be 0; got " + dimensionCount);
    }

    this.dimensionCount = dimensionCount;
    this.indexDimensionCount = indexDimensionCount;
    this.dimensionNumBytes = dimensionNumBytes;
  }

  @Override
  public int pointDimensionCount() {
    return dimensionCount;
  }

  @Override
  public int pointIndexDimensionCount() {
    return indexDimensionCount;
  }

  @Override
  public int pointNumBytes() {
    return dimensionNumBytes;
  }

  /** Enable vector indexing, with the specified number of dimensions and distance function. */
  public void setVectorAttributes(
      int numDimensions, VectorEncoding encoding, VectorSimilarityFunction similarity) {
    checkIfFrozen();
    if (numDimensions <= 0) {
      throw new IllegalArgumentException("vector numDimensions must be > 0; got " + numDimensions);
    }
    this.vectorDimension = numDimensions;
    this.vectorSimilarityFunction = Objects.requireNonNull(similarity);
    this.vectorEncoding = Objects.requireNonNull(encoding);
  }

  @Override
  public int vectorDimension() {
    return vectorDimension;
  }

  @Override
  public VectorEncoding vectorEncoding() {
    return vectorEncoding;
  }

  @Override
  public VectorSimilarityFunction vectorSimilarityFunction() {
    return vectorSimilarityFunction;
  }

  /**
   * Puts an attribute value.
   *
   * <p>This is a key-value mapping for the field that the codec can use to store additional
   * metadata.
   *
   * <p>If a value already exists for the field, it will be replaced with the new value. This method
   * is not thread-safe, user must not add attributes while other threads are indexing documents
   * with this field type.
   *
   * @lucene.experimental
   */
  public String putAttribute(String key, String value) {
    checkIfFrozen();
    if (attributes == null) {
      attributes = new HashMap<>();
    }
    return attributes.put(key, value);
  }

  @Override
  public Map<String, String> getAttributes() {
    return attributes;
  }

  /** Prints a Field for human consumption. */
  @Override
  public String toString() {
    StringBuilder result = new StringBuilder();
    if (stored()) {
      result.append("stored");
    }
    if (indexOptions != IndexOptions.NONE) {
      if (result.length() > 0) result.append(",");
      result.append("indexed");
      if (tokenized()) {
        result.append(",tokenized");
      }
      if (storeTermVectors()) {
        result.append(",termVector");
      }
      if (storeTermVectorOffsets()) {
        result.append(",termVectorOffsets");
      }
      if (storeTermVectorPositions()) {
        result.append(",termVectorPosition");
      }
      if (storeTermVectorPayloads()) {
        result.append(",termVectorPayloads");
      }
      if (omitNorms()) {
        result.append(",omitNorms");
      }
      if (indexOptions != IndexOptions.DOCS_AND_FREQS_AND_POSITIONS) {
        result.append(",indexOptions=");
        result.append(indexOptions);
      }
    }
    if (dimensionCount != 0) {
      if (result.length() > 0) {
        result.append(",");
      }
      result.append("pointDimensionCount=");
      result.append(dimensionCount);
      result.append(",pointIndexDimensionCount=");
      result.append(indexDimensionCount);
      result.append(",pointNumBytes=");
      result.append(dimensionNumBytes);
    }
    if (docValuesType != DocValuesType.NONE) {
      if (result.length() > 0) {
        result.append(",");
      }
      result.append("docValuesType=");
      result.append(docValuesType);
    }

    return result.toString();
  }

  /**
   * {@inheritDoc}
   *
   * <p>The default is <code>null</code> (no docValues)
   *
   * @see #setDocValuesType(DocValuesType)
   */
  @Override
  public DocValuesType docValuesType() {
    return docValuesType;
  }

  /**
   * Sets the field's DocValuesType
   *
   * @param type DocValues type, or null if no DocValues should be stored.
   * @throws IllegalStateException if this FieldType is frozen against future modifications.
   * @see #docValuesType()
   */
  public void setDocValuesType(DocValuesType type) {
    checkIfFrozen();
    if (type == null) {
      throw new NullPointerException("DocValuesType must not be null");
    }
    docValuesType = type;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + dimensionCount;
    result = prime * result + indexDimensionCount;
    result = prime * result + dimensionNumBytes;
    result = prime * result + ((docValuesType == null) ? 0 : docValuesType.hashCode());
    result = prime * result + indexOptions.hashCode();
    result = prime * result + (omitNorms ? 1231 : 1237);
    result = prime * result + (storeTermVectorOffsets ? 1231 : 1237);
    result = prime * result + (storeTermVectorPayloads ? 1231 : 1237);
    result = prime * result + (storeTermVectorPositions ? 1231 : 1237);
    result = prime * result + (storeTermVectors ? 1231 : 1237);
    result = prime * result + (stored ? 1231 : 1237);
    result = prime * result + (tokenized ? 1231 : 1237);
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null) return false;
    if (getClass() != obj.getClass()) return false;
    FieldType other = (FieldType) obj;
    if (dimensionCount != other.dimensionCount) return false;
    if (indexDimensionCount != other.indexDimensionCount) return false;
    if (dimensionNumBytes != other.dimensionNumBytes) return false;
    if (docValuesType != other.docValuesType) return false;
    if (indexOptions != other.indexOptions) return false;
    if (omitNorms != other.omitNorms) return false;
    if (storeTermVectorOffsets != other.storeTermVectorOffsets) return false;
    if (storeTermVectorPayloads != other.storeTermVectorPayloads) return false;
    if (storeTermVectorPositions != other.storeTermVectorPositions) return false;
    if (storeTermVectors != other.storeTermVectors) return false;
    if (stored != other.stored) return false;
    if (tokenized != other.tokenized) return false;
    return true;
  }
}
