/**
 * 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.apex.malhar.lib.appdata.schemas;

import java.io.Serializable;

import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;

/**
 * This enum is used to represent data types throughout AppData Framework.
 * @since 3.0.0
 */
public enum Type implements Serializable
{
  /**
   * Boolean data type.
   */
  BOOLEAN("boolean", 1, JSONType.BOOLEAN, Boolean.class,
          Collections.unmodifiableSet(new HashSet<Type>())),
  /**
   * String data type.
   */
  STRING("string", -1, JSONType.STRING, String.class,
         Collections.unmodifiableSet(new HashSet<Type>())),
  /**
   * Char data type.
   */
  CHAR("char", 2, JSONType.STRING, Character.class,
       ImmutableSet.of(STRING)),
  /**
   * Double data type.
   */
  DOUBLE("double", 8, JSONType.NUMBER, Double.class,
         Collections.unmodifiableSet(new HashSet<Type>())),
  /**
   * Float data type.
   */
  FLOAT("float", 4, JSONType.NUMBER, Float.class,
        ImmutableSet.of(DOUBLE)),
  /**
   * Long data type.
   */
  LONG("long", 8, JSONType.NUMBER, Long.class,
       Collections.unmodifiableSet(new HashSet<Type>())),
  /**
   * Integer data type.
   */
  INTEGER("integer", 4, JSONType.NUMBER, Integer.class,
          ImmutableSet.of(LONG)),
  /**
   * Short data type.
   */
  SHORT("short", 2, JSONType.NUMBER, Short.class,
        ImmutableSet.of(INTEGER, LONG)),
  /**
   * Byte data type.
   */
  BYTE("byte", 1, JSONType.NUMBER, Byte.class,
       ImmutableSet.of(SHORT, INTEGER, LONG)),
  /**
   * Object data type.
   */
  OBJECT("object", -1, JSONType.INVALID, Object.class,
          Collections.unmodifiableSet(new HashSet<Type>()));

  /**
   * A set containing all the numeric types.
   */
  public static final Set<Type> NUMERIC_TYPES = ImmutableSet.of(BYTE, SHORT, INTEGER, LONG, FLOAT, DOUBLE);
  /**
   * A set containing all the non numeric types.
   */
  public static final Set<Type> NON_NUMERIC_TYPES = ImmutableSet.of(BOOLEAN, CHAR, STRING);
  /**
   * A map from the names to the types.
   */
  public static final Map<String, Type> NAME_TO_TYPE;
  /**
   * A map from a class representing the type to the type itself.
   */
  public static final Map<Class<?>, Type> CLASS_TO_TYPE;

  static {
    Map<String, Type> nameToType = Maps.newHashMap();

    for (Type type : Type.values()) {
      nameToType.put(type.getName(), type);
    }

    NAME_TO_TYPE = Collections.unmodifiableMap(nameToType);

    Map<Class<?>, Type> clazzToType = Maps.newHashMap();

    for (Type type : Type.values()) {
      clazzToType.put(type.getClazz(), type);
    }

    CLASS_TO_TYPE = Collections.unmodifiableMap(clazzToType);
  }

  /**
   * The name of the type.
   */
  private final String name;
  /**
   * The type of the corresponding JSON element.
   */
  private final JSONType jsonType;
  /**
   * Class of the corresponding Java type.
   */
  private final Class<?> clazz;
  /**
   * In the case of numeric types, there may be a "higher" type. For example
   * ints can be promoted to longs. This set holds all of the types that this
   * type could be promoted to. If this type cannot be promoted then this will be empty.
   */
  private final Set<Type> higherTypes;
  /**
   * The number of bytes required to store a value of this type. If a value of
   * this type can be of variable length then this would be -1.
   */
  private final int byteSize;

  /**
   * Creates a type enum.
   * @param name The name of the type.
   * @param byteSize The number of bytes a value of this type would occupy. -1 if variable length.
   * @param jsonType The type of corresponding json values.
   * @param clazz The Class of the corresponding Java type.
   * @param higherTypes The set of types to which this type can be promoted.
   */
  Type(String name, int byteSize, JSONType jsonType, Class<?> clazz, Set<Type> higherTypes)
  {
    this.name = name;
    this.byteSize = byteSize;
    this.jsonType = jsonType;
    this.clazz = clazz;
    this.higherTypes = higherTypes;
  }

  /**
   * Gets the name of the type.
   * @return The name of the type.
   */
  public String getName()
  {
    return name;
  }

  /**
   * Gets the byte size of the value.
   * @return The byte size of the value.
   */
  public int getByteSize()
  {
    return byteSize;
  }

  /**
   * Gets the corresponding JSONType of this type.
   * @return The corresponding JSONType of this type.
   */
  public JSONType getJSONType()
  {
    return jsonType;
  }

  /**
   * Gets the Java class corresponding to this type.
   * @return The Java class corresponding to this type.
   */
  public Class<?> getClazz()
  {
    return clazz;
  }

  /**
   * Gets the set of types to which this type can be promoted.
   * @return The set of types to which this type can be promoted.
   */
  public Set<Type> getHigherTypes()
  {
    return higherTypes;
  }

  /**
   * Determines if the given type is a higher type of this type.
   * @param type The type which may or not be a parent of this type.
   * @return True if the given type is a higher type of this type. False otherwise.
   */
  public boolean isChildOf(Type type)
  {
    return higherTypes.contains(type);
  }

  /**
   * Determines if one of the given types is a child of the other.
   * @param a This type will be checked to see if it is a child of b.
   * @param b This type will be checked to see if it is a child of a.
   * @return True if one of the given types is a child of the other. False otherwise.
   */
  public static boolean areRelated(Type a, Type b)
  {
    return a == b || a.isChildOf(b) || b.isChildOf(a);
  }

  /**
   * Gets the type corresponding to the given name.
   * @param name The name of the type.
   * @return The type corresponding to the name or null if there is no type
   * corresponding to the given name.
   */
  public static Type getType(String name)
  {
    return NAME_TO_TYPE.get(name);
  }

  public static Type getTypeEx(String name)
  {
    Type type = getType(name);

    Preconditions.checkArgument(type != null, name + " is not a valid type.");

    return type;
  }

  /**
   * Promotes the given object from the "from" type to the "to" type.
   * @param from The current type of the given object.
   * @param to The desired type for the given object.
   * @param o The object whose type must be converted.
   * @return The result of promoting the given object o to the "to" type.
   */
  public static Object promote(Type from, Type to, Object o)
  {
    if (from == to) {
      return o;
    }

    Preconditions.checkArgument(!(from == Type.BOOLEAN || from == Type.CHAR || from == Type.LONG
        || from == Type.DOUBLE), "Cannot convert " + Type.BOOLEAN.getName() + ", " + Type.CHAR.getName()
        + ", " + Type.LONG.getName() + ", or " + Type.DOUBLE + " to a larger type.");

    Preconditions.checkArgument(from.getHigherTypes().contains(to),
        from.getName() + " cannot be promoted to " + to.getName());

    if (from == Type.FLOAT && to == Type.DOUBLE) {
      return (Double)((Float)o).doubleValue();
    }

    if (from == Type.BYTE) {
      if (to == Type.SHORT) {
        return (Short)((Byte)o).shortValue();
      } else if (to == Type.INTEGER) {
        return (Integer)((Byte)o).intValue();
      } else if (to == Type.LONG) {
        return (Long)((Byte)o).longValue();
      }
    }

    if (from == Type.SHORT) {
      if (to == Type.INTEGER) {
        return (Integer)((Short)o).intValue();
      } else if (to == Type.LONG) {
        return (Long)((Short)o).longValue();
      }
    }

    if (from == Type.INTEGER
        && to == Type.LONG) {
      return (Long)((Integer)o).longValue();
    }

    throw new UnsupportedOperationException("This should not happen.");
  }
}
