/*
 * 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.uima.cas.impl;

import java.util.List;

import org.apache.uima.cas.CAS;
import org.apache.uima.cas.Feature;
import org.apache.uima.cas.Type;
import org.apache.uima.cas.TypeSystem;
import org.apache.uima.cas.impl.SlotKinds.SlotKind;
import org.apache.uima.internal.util.Misc;

/**
 * The implementation of features in the type system. A featureImpl instance is shared by the top
 * defining type and all of its subtypes
 * 
 */
public class FeatureImpl implements Feature {

  private final int featureCode; // unique id for this feature, in this type system
  /**
   * the 0 based offset for this feature ignoring ref/int distinction, in feature order, without
   * regard to JCas implemented features; set at commit time used by v2 style de/serializers
   */
  private short featureOffset = -1;
  private short adjustedFeatureOffset = -1; // the offset in the storage array for this feature,
                                            // adjusted to exclude JCas implemented features; set at
                                            // commit time

  // not used 2/29/16 to be removed
  // int registryIndex = -1; // set from JCas classes feature registry
  // used to setup index corruption bitset
  public final boolean isInInt; // specifies which array the data is in
  private final boolean isMultipleRefsAllowed;
  /**
   * true if the range is a long or double
   */
  public final boolean isLongOrDouble;
  private final TypeImpl highestDefiningType; // if changed, this feature is thrown away and a
                                              // replacement is made

  private final TypeImpl rangeType;

  // private final TypeSystemImpl ts;

  /**
   * true for the feature which is the AnnotationBase sofa reference.
   */
  public final boolean isAnnotBaseSofaRef;
  private final String shortName; // feat

  // protected Object jcasGetter; // null or the functional interface to call to get this feature
  // protected Object jcasSetter; // null or the functional interface to call to set this feature
  // protected Object jcasSetterNcNj; // null or the functional interface to call to set this
  // feature, no check for corruption, no journaling

  private final SlotKind slotKind;
  /** type class of the range, including CasSerializer List constants */
  public final int rangeTypeClass; // set from CasSerializerSupport.classifyType

  private final long hashCodeLong;

  /**
   * used to make singleton which is used for "missing feature"
   */
  private FeatureImpl() {
    this.featureCode = 0;
    this.isInInt = false;
    this.rangeType = null;
    this.isMultipleRefsAllowed = false;
    this.isAnnotBaseSofaRef = false;
    this.shortName = null;
    this.slotKind = null;
    this.rangeTypeClass = 0;
    this.isLongOrDouble = false;
    this.highestDefiningType = null;
    this.hashCodeLong = computeHashCodeLong();
  }

  FeatureImpl(TypeImpl typeImpl, String shortName, TypeImpl rangeType, TypeSystemImpl tsi,
          boolean isMultipleRefsAllowed, SlotKind slotKind) {
    // this.code = code;
    this.highestDefiningType = typeImpl;
    List<FeatureImpl> feats = (tsi == null) ? null : tsi.features;
    this.featureCode = (feats == null) ? -1 : feats.size();

    this.rangeType = rangeType;
    this.isLongOrDouble = (rangeType == null) ? false : rangeType.isLongOrDouble;
    this.slotKind = slotKind;
    this.shortName = shortName;
    this.isMultipleRefsAllowed = isMultipleRefsAllowed;
    this.isAnnotBaseSofaRef = (highestDefiningType == null) ? false
            : ((highestDefiningType.getCode() == TypeSystemConstants.annotBaseTypeCode)
                    && shortName.equals(CAS.FEATURE_BASE_NAME_SOFA));
    this.isInInt = (rangeType == null) ? false : (rangeType.getTypeSystem().isInInt(rangeType));
    this.rangeTypeClass = (rangeType == null) ? CASImpl.TYPE_CLASS_FS
            : CasSerializerSupport.classifyType(rangeType);
    this.hashCodeLong = computeHashCodeLong();
    if (typeImpl != null) {
      // if typeImpl is null, this is a "jcas only" defined feature, not a real feature
      typeImpl.addFeature(this); // might throw if existing feature with different range
      feats.add(this);
    }
  }

  /**
   * @return the internal code of this feature. Necessary when using low-level APIs.
   */
  public int getCode() {
    return this.featureCode;
  }

  /**
   * Get the domain type for this feature.
   * 
   * @return The domain type. This can not be <code>null</code>.
   */
  @Override
  public Type getDomain() {
    return this.highestDefiningType;
  }

  /**
   * Get the range type for this feature. * @return The range type. This can not be
   * <code>null</code>.
   */
  @Override
  public Type getRange() {
    return this.rangeType;
  }

  public TypeImpl getRangeImpl() {
    return this.rangeType;
  }

  public SlotKind getSlotKind() {
    return this.slotKind;
  }

  /**
   * Get the fully qualified name for this feature. The Feature qualifier is that of the highest
   * defining type.
   * 
   * @return The name. This can not be <code>null</code>.
   */
  @Override
  public String getName() {
    return highestDefiningType.getName() + TypeSystem.FEATURE_SEPARATOR + shortName;
  }

  @Override
  public String getShortName() {
    return this.shortName;
  }

  @Override
  public String toString() {
    return String.format("%s [%s: rangeType=%s, isMultipleRefsAllowed=%s, slotKind=%s]",
            this.getClass().getSimpleName(), getName(), rangeType, isMultipleRefsAllowed, slotKind);
  }

  // public String getGetterSetterName(boolean isGet) {
  // String shortName1stLetterUpperCase = Character.toUpperCase(this.shortName.charAt(0)) +
  // ((shortName.length() == 1) ? "" : this.shortName.substring(1));
  //
  // return (isGet ? "get" : "set") + shortName1stLetterUpperCase;
  // }

  @Override
  public boolean isMultipleReferencesAllowed() {
    return this.isMultipleRefsAllowed;
  }

  /**
   * @return the 0-based offset for this feature
   */
  public int getOffset() {
    return featureOffset;
  }

  void setOffset(int offset) {
    if (offset > Short.MAX_VALUE) {
      throw new RuntimeException("Feature Offset exceeds maximum of 32767");
    }
    featureOffset = (short) offset;
  }

  public int getAdjustedOffset() {
    return adjustedFeatureOffset;
  }

  void setAdjustedOffset(int offset) {
    if (offset > Short.MAX_VALUE) {
      throw new RuntimeException("Feature Offset exceeds maximum of 32767");
    }
    adjustedFeatureOffset = (short) offset;
  }

  // /**
  // * @return the functionalGetter
  // */
  // Object getJCasGetter() {
  // return jcasGetter;
  // }
  //
  // /**
  // * @param functionalGetter the functionalGetter to set
  // */
  // void setJCasGetter(Object functionalGetter) {
  // this.jcasGetter = functionalGetter;
  // }
  //
  // /**
  // * @return the functionalSetter
  // */
  // Object getJCasSetter() {
  // return jcasSetter;
  // }
  //
  // /**
  // * @return the functional setter with no index corruption checking, no journalling
  // */
  // Object getJCasSetterNcNj() {
  // return jcasSetterNcNj;
  // }

  /* Set for these values done directly in TypeSystemImpl, computeAdjustedFeatureOffsets */

  TypeImpl getHighestDefiningType() {
    return highestDefiningType;
  }

  // void setHighestDefiningType(Type type) {
  // highestDefiningType = (TypeImpl) type;
  // }

  /**
   * throw if v is not in the allowed value set of the range type
   * 
   * @param v
   *          the value to check
   */
  public void validateIsInAllowedValue(String v) {
    TypeImpl_stringSubtype ti = (TypeImpl_stringSubtype) getRangeImpl();
    ti.validateIsInAllowedValues(v);
  }

  @Override
  public int hashCode() {
    return (int) hashCodeLong;
  }

  public long hashCodeLong() {
    return hashCodeLong;
  }

  /**
   * Hashcode and equals are used, possibly for features in different type systems, where the
   * features should be "equal". Example: fitering during serialization.
   * 
   * @return long version of hashcode
   */

  public long computeHashCodeLong() {
    final long prime = 31;
    long result;
    // return this.featureCode; // can't use this across different type systems
    result = prime + Misc.hashStringLong(shortName);
    result = prime * result
            + ((highestDefiningType == null) ? 0 : highestDefiningType.hashCodeNameLong());
    result = prime * result + (isMultipleRefsAllowed ? 1231 : 1237);
    result = prime * result + ((rangeType == null) ? 0 : rangeType.hashCodeNameLong());
    return result;
  }

  /**
   * This should work across different type systems, for instance when using filtered serialization
   */
  @Override
  public int compareTo(Feature o) {
    if (this == o) {
      return 0;
    }

    FeatureImpl other = (FeatureImpl) o;
    if (hashCodeLong == other.hashCodeLong)
      return 0;

    // to preserve the compare contract, can't use hash for miscompare

    int c;
    c = this.shortName.compareTo(other.shortName);
    if (c != 0)
      return c;
    c = highestDefiningType.getName().compareTo(other.highestDefiningType.getName());
    if (c != 0)
      return c;
    c = Boolean.compare(this.isMultipleRefsAllowed, other.isMultipleRefsAllowed);
    if (c != 0)
      return c;
    c = rangeType.getName().compareTo(other.rangeType.getName());
    if (c != 0)
      return c;

    throw Misc.internalError();
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (!(obj instanceof FeatureImpl))
      return false;

    FeatureImpl other = (FeatureImpl) obj;
    // return this.featureCode == other.featureCode; // can't use this across different type systems
    return hashCodeLong == other.hashCodeLong;
    // if (!highestDefiningType.getName().equals(other.highestDefiningType.getName())) return false;
    // if (isMultipleRefsAllowed != other.isMultipleRefsAllowed) return false;
    // if (!rangeType.getName().equals(other.rangeType.getName())) return false;
    // if (!shortName.equals(other.shortName)) return false;
    // return true;
  }

  /**
   * Used by CAS Copier to denote missing feature
   */
  public final static FeatureImpl singleton = new FeatureImpl();

}
