/*
 * 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.geode.cache.query.internal.types;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.apache.geode.InternalGemFireError;
import org.apache.geode.annotations.Immutable;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.TypeMismatchException;
import org.apache.geode.cache.query.internal.Support;
import org.apache.geode.cache.query.internal.Undefined;
import org.apache.geode.cache.query.internal.parse.OQLLexerTokenTypes;
import org.apache.geode.cache.query.types.ObjectType;
import org.apache.geode.pdx.internal.EnumInfo.PdxInstanceEnumInfo;
import org.apache.geode.pdx.internal.PdxInstanceEnum;
import org.apache.geode.pdx.internal.PdxString;


/**
 * Utilities for casting and comparing values of possibly differing types, testing and cloning query
 * literals.
 *
 * @version $Revision: 1.1 $
 */

public class TypeUtils implements OQLLexerTokenTypes {
  @Immutable
  protected static final List<Class> _numericPrimitiveClasses =
      Collections.unmodifiableList(Arrays.asList(
          new Class[] {Byte.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE}));

  @Immutable
  protected static final List<Class> _numericWrapperClasses =
      Collections.unmodifiableList(Arrays.asList(
          new Class[] {Byte.class, Short.class, Integer.class, Long.class, Float.class,
              Double.class}));

  /**
   * Enum to execute comparisons based on types.
   */
  private enum ComparisonStrategy {
    TEMPORAL {
      @Override
      public Boolean execute(Object object1, Object object2, int comparator)
          throws ClassCastException {
        return applyComparator(getTemporalComparator().compare(object1, object2), comparator);
      }
    },

    NUMERIC {
      @Override
      public Boolean execute(Object object1, Object object2, int comparator)
          throws ClassCastException {
        return applyComparator(getNumericComparator().compare(object1, object2), comparator);
      }
    },

    BOOLEAN {
      @Override
      public Boolean execute(Object object1, Object object2, int comparator)
          throws TypeMismatchException {
        return booleanCompare(object1, object2, comparator);
      }
    },

    COMPARABLE {
      @Override
      public Boolean execute(Object object1, Object object2, int comparator)
          throws ClassCastException {
        return applyComparator(((Comparable) object1).compareTo(object2), comparator);
      }
    },

    ARBITRARY {
      @Override
      public Boolean execute(Object object1, Object object2, int comparator) {
        if (comparator == TOK_EQ) {
          return object1.equals(object2);
        } else {
          return !object1.equals(object2);
        }
      }
    };

    protected Boolean applyComparator(int temporalResult, int comparator)
        throws IllegalArgumentException {
      switch (comparator) {
        case TOK_EQ:
          return temporalResult == 0;
        case TOK_LT:
          return temporalResult < 0;
        case TOK_LE:
          return temporalResult <= 0;
        case TOK_GT:
          return temporalResult > 0;
        case TOK_GE:
          return temporalResult >= 0;
        case TOK_NE:
          return temporalResult != 0;
        default:
          throw new IllegalArgumentException(String.format("Unknown operator: %s",
              comparator));
      }
    }

    public static ComparisonStrategy get(Class object1Class, Class object2Class, int comparator)
        throws TypeMismatchException {
      if (isAssignableFrom(object1Class, Date.class)
          && isAssignableFrom(object1Class, Date.class)) {
        return TEMPORAL;
      } else if (object1Class != object2Class && (isAssignableFrom(object1Class, Number.class)
          && isAssignableFrom(object2Class, Number.class))) {
        return NUMERIC;
      } else if (isAssignableFrom(object1Class, Boolean.class)
          || isAssignableFrom(object2Class, Boolean.class)) {
        return BOOLEAN;
      } else if (isAssignableFrom(object1Class, Comparable.class)
          && isAssignableFrom(object2Class, Comparable.class)) {
        return COMPARABLE;
      } else if ((comparator == TOK_EQ) || (comparator == TOK_NE)) {
        return ARBITRARY;
      } else {
        throw new TypeMismatchException(
            String.format(
                "Unable to use a relational comparison operator to compare an instance of class ' %s ' with an instance of ' %s '",
                object1Class.getName(), object2Class.getName()));
      }
    }

    /**
     * Executes the comparison strategy.
     *
     */
    public abstract Boolean execute(Object object1, Object object2, int comparator)
        throws TypeMismatchException, ClassCastException;
  }

  /* Common Types */
  /** ObjectType for Object.class */
  @Immutable
  public static final ObjectType OBJECT_TYPE = new ObjectTypeImpl(Object.class);

  /** prevent instantiation */
  private TypeUtils() {}

  /**
   * Creates an instance of {@link org.apache.geode.cache.query.internal.types.TemporalComparator}.
   *
   * @return Comparator for mixed comparisons between instances of java.util.Date, java.sql.Date,
   *         java.sql.Time, and java.sql.Timestamp.
   * @see org.apache.geode.cache.query.internal.types.TemporalComparator
   */
  public static Comparator getTemporalComparator() {
    return new TemporalComparator();
  }

  /**
   * Creates an instance of {@link org.apache.geode.cache.query.internal.types.NumericComparator}.
   *
   * @return Comparator for mixed comparisons between numbers.
   * @see org.apache.geode.cache.query.internal.types.NumericComparator
   */
  public static Comparator getNumericComparator() {
    return new NumericComparator();
  }

  /**
   * Creates an instance of
   * {@link org.apache.geode.cache.query.internal.types.ExtendedNumericComparator}.
   *
   * @return A general comparator that compares different numeric types for equality.
   * @see org.apache.geode.cache.query.internal.types.ExtendedNumericComparator
   */
  public static Comparator getExtendedNumericComparator() {
    return new ExtendedNumericComparator();
  }

  /**
   * Verify that type-cast will work, or else throw an informative exception.
   *
   * @return the castTarget
   * @throws InternalGemFireError if cast will fail
   */
  @SuppressWarnings("unchecked")
  public static <T> T checkCast(Object castTarget, Class<T> castClass) {
    if (castTarget == null) {
      return null; // null can be cast to anything
    }

    if (!castClass.isInstance(castTarget)) {
      throw new InternalGemFireError(String.format("expected instance of %s but was %s",
          castClass.getName(), castTarget.getClass().getName()));
    }

    return (T) castTarget;
  }

  /**
   * Given an arbitrary object, return the value that should be used as the key for a particular
   * index.
   *
   * @param obj the object to evaluate.
   * @return the value that should be used as the key for an index.
   * @throws TypeMismatchException if the object type is not supported to be used as an index.
   */
  public static Object indexKeyFor(Object obj) throws TypeMismatchException {
    if (obj == null) {
      return null;
    }

    if (obj instanceof Byte) {
      return ((Byte) obj).intValue();
    }

    if (obj instanceof Short) {
      return ((Short) obj).intValue();
    }

    // Ketan : Added later. Indexes should be created
    // if the IndexedExpr implements Comparable interface.
    if (obj instanceof Comparable) {
      if (obj instanceof Enum) {
        obj = new PdxInstanceEnum((Enum<?>) obj);
      }

      return obj;
    }

    throw new TypeMismatchException(String.format("Indexes are not supported for type ' %s '",
        obj.getClass().getName()));
  }

  /**
   * Determines if the Class represented by the first parameter is either the same as, or is a
   * superclass or superinterface of, the class or interface represented by the second parameter.
   *
   * @param srcType the class to be checked.
   * @param destType the class to be checked against.
   * @return the {@code boolean} value indicating whether objects of the type {@code srcType} can be
   *         assigned to objects of type {@code destType} .
   */
  protected static boolean isAssignableFrom(Class srcType, Class destType) {
    return destType.isAssignableFrom(srcType);
  }

  /**
   * Determines if the Class represented by the first parameter can be safely converted to the Class
   * represented by the second parameter.
   *
   * @param srcType the source class to be converted.
   * @param destType the target class to be converted to.
   * @return the {@code boolean} value indicating whether objects of the type {@code srcType} can be
   *         converted to objects of type {@code destType} .
   */
  public static boolean isTypeConvertible(Class srcType, Class destType) {
    // handle null: if srcType is null, then it represents
    // a null runtime value, and for our purposes it is type
    // convertible to any type that is assignable from Object.class
    if (srcType == null) {
      // TODO: This should always be true as Object is assignable from everything, is the root type
      // of all Java classes. Object.class is always assignable from every other Java class.
      return isAssignableFrom(destType, Object.class);
    }

    // check to see if the classes are assignable
    if (isAssignableFrom(srcType, destType)) {
      return true;
    }

    // handle booleans: are we going from a wrapper Boolean
    // to a primitive boolean or vice-versa?
    if ((srcType == Boolean.TYPE || srcType == Boolean.class)
        && (destType == Boolean.TYPE || destType == Boolean.class)) {
      return true;
    }

    // a numeric primitive or wrapper can be converted to
    // the same wrapper or a same or wider primitive.
    // handle chars specially
    int i = _numericPrimitiveClasses.indexOf(srcType);
    if (i < 0) {
      i = _numericWrapperClasses.indexOf(srcType);
    }

    int destP = _numericPrimitiveClasses.indexOf(destType);
    int destW = -1;
    if (destP < 0) {
      destW = _numericWrapperClasses.indexOf(destType);
    }

    // same size wrapper
    if (i >= 0 && destW == i) {
      return true;
    }

    // same or wider primitive
    if (i >= 0 && destP >= i) {
      return true;
    }

    // chars
    if (srcType == Character.class || srcType == Character.TYPE) {
      // chars: same size wrapper/primitive
      return destType == Character.class || destType == Character.TYPE;
    }

    // no other possibilities
    return false;
  }

  /**
   * Determines if the all classes represented by the first parameter can be safely converted to the
   * classes represented by the second parameter.
   *
   * @param srcTypes the source classes to be converted.
   * @param destTypes the target classes to be converted to.
   * @return the {@code boolean} value indicating whether all objects of the first array can be
   *         converted to objects of the second array.
   */
  public static boolean areTypesConvertible(Class[] srcTypes, Class[] destTypes) {
    Support.assertArg(srcTypes.length == destTypes.length,
        "Arguments 'srcTypes' and 'destTypes' must be of same length");

    for (int i = 0; i < srcTypes.length; i++) {
      if (!isTypeConvertible(srcTypes[i], destTypes[i])) {
        return false;
      }
    }

    return true;
  }

  /**
   * Return the {@link org.apache.geode.cache.query.types.ObjectType} corresponding to the specified
   * Class.
   *
   * @return the internal ObjectType implementation that represents the specified class.
   */
  public static ObjectType getObjectType(Class cls) {
    if (cls == Object.class) {
      return OBJECT_TYPE;
    }

    if (Collection.class.isAssignableFrom(cls)) {
      // we don't have element type info here
      return new CollectionTypeImpl(cls, OBJECT_TYPE);
    }

    if (cls.isArray()) {
      return new CollectionTypeImpl(cls, getObjectType(cls.getComponentType()));
    }

    if (Region.class.isAssignableFrom(cls)) {
      // we don't have access to the region itself for element type
      return new CollectionTypeImpl(cls, OBJECT_TYPE);
    }

    if (Map.class.isAssignableFrom(cls)) {
      return new MapTypeImpl(cls, OBJECT_TYPE, OBJECT_TYPE);
    }

    // if it's a struct we have no field info, so just return an ObjectTypeImpl
    return new ObjectTypeImpl(cls);
  }

  /**
   * Return a fixed {@link org.apache.geode.cache.query.types.ObjectType} representing a Region
   * Entry.
   *
   * @return the internal ObjectType implementation that represents a the region entry class.
   */
  public static ObjectType getRegionEntryType(Region rgn) {
    // just use an ObjectType for now
    return new ObjectTypeImpl(Region.Entry.class);
  }

  /**
   * Compare two booleans.
   *
   * @return a boolean indicating the result of applying the comparison operator on the arguments.
   * @throws TypeMismatchException When either one of the arguments is not a Boolean, or the
   *         comparison operator is not supported.
   */
  protected static boolean booleanCompare(Object obj1, Object obj2, int compOp)
      throws TypeMismatchException {
    if (!(obj1 instanceof Boolean) || !(obj2 instanceof Boolean)) {
      throw new TypeMismatchException(
          "Booleans can only be compared with booleans");
    }

    if (compOp == TOK_EQ) {
      return obj1.equals(obj2);
    } else if (compOp == TOK_NE) {
      return !obj1.equals(obj2);
    } else {
      throw new TypeMismatchException(
          "Boolean values can only be compared with = or <>");
    }
  }

  /**
   *
   * @return a Boolean, or null if UNDEFINED.
   */
  private static Boolean nullCompare(Object obj1, Object obj2, int compOp) {
    Boolean result = null;

    switch (compOp) {
      case TOK_EQ: {
        if (obj1 == null) {
          result = obj2 == null;
        } else { // obj1 is not null obj2 must be
          result = Boolean.FALSE;
        }

        break;
      }

      case TOK_NE: {
        if (obj1 == null) {
          result = obj2 != null;
        } else { // obj1 is not null so obj2 must be
          result = Boolean.TRUE;
        }

        break;
      }
    }

    return result;
  }

  /**
   * Compares two objects using the operator
   *
   * @return boolean;<br>
   *         {@link Undefined} if either of the operands is {@link Undefined} or if either of the
   *         operands is Null and operator is other than == or !=
   */
  public static Object compare(Object obj1, Object obj2, int compOp) throws TypeMismatchException {
    // Check for nulls first.
    if (obj1 == null || obj2 == null) {
      Boolean result = nullCompare(obj1, obj2, compOp);

      if (result == null) {
        return QueryService.UNDEFINED;
      }

      return result;
    }

    // if either object is UNDEFINED, result is UNDEFINED
    if (obj1 == QueryService.UNDEFINED || obj2 == QueryService.UNDEFINED) {
      if (compOp == TOK_NE && !(obj1 == QueryService.UNDEFINED && obj2 == QueryService.UNDEFINED)) {
        return true;
      } else if (compOp == TOK_EQ && obj1.equals(obj2)) {
        return true;
      } else {
        return QueryService.UNDEFINED;
      }
    }

    // Prepare pdx instances if needed.
    if (obj1 instanceof PdxInstanceEnumInfo && obj2 instanceof Enum) {
      obj2 = new PdxInstanceEnum((Enum<?>) obj2);
    } else if (obj1 instanceof Enum && obj2 instanceof PdxInstanceEnumInfo) {
      obj1 = new PdxInstanceEnum((Enum<?>) obj1);
    }

    if (obj1 instanceof PdxString && obj2 instanceof String) {
      obj2 = new PdxString((String) obj2);
    } else if (obj1 instanceof String && obj2 instanceof PdxString) {
      obj1 = new PdxString((String) obj1);
    }

    try {
      ComparisonStrategy strategy =
          ComparisonStrategy.get(obj1.getClass(), obj2.getClass(), compOp);
      return strategy.execute(obj1, obj2, compOp);
    } catch (ClassCastException | TypeMismatchException e) {
      // if a ClassCastException or TypeMismatchException was thrown and the operator is equals
      // or not equals, then override and return true or false.
      if (compOp == TOK_EQ) {
        return Boolean.FALSE;
      }

      if (compOp == TOK_NE) {
        return Boolean.TRUE;
      }

      if (isAssignableFrom(e.getClass(), ClassCastException.class)) {
        throw new TypeMismatchException(
            String.format("Unable to compare object of type ' %s ' with object of type ' %s '",

                obj1.getClass().getName(), obj2.getClass().getName()),
            e);
      } else {
        throw e;
      }
    }
  }
}
