/*
 * 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.tinkerpop.gremlin.object.reflect;

import org.apache.tinkerpop.gremlin.object.model.DefaultValue;
import org.apache.tinkerpop.gremlin.object.structure.Element;
import org.javatuples.Pair;

import java.lang.reflect.Field;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import lombok.SneakyThrows;

/**
 * {@link Primitives} helps us work with properties whose type is a primitive, or a wrapper thereof.
 * It also handles conversions between various formats of time-based properties, and default values
 * for various primitive types.
 *
 * <p>
 * It is important to know which properties are treated as primitive by the graph system, so that
 * the object graph can pass them as-is to the traversal. If a graph system defines custom primitive
 * types, they may register them here using the {@link #registerPrimitiveClass} method.
 *
 * @author Karthick Sankarachary (http://github.com/karthicks)
 */
@SuppressWarnings({"rawtypes", "PMD.AvoidUsingShortType"})
public final class Primitives {

  /**
   * If false, then primitive fields that are {@code PrimaryKey} or {@code OrderingKey} cannot be
   * stored using the primitive default values.
   */
  public static boolean allowDefaultKeys = true;

  /**
   * What is the primitive value for the primitive classes that we know of?
   */
  private static final Map<Class, Object> PRIMITIVE_DEFAULTS = new HashMap<>();

  /**
   * How can one parse a primitive value from the string specified in {@link DefaultValue#value()}?
   */
  private static final Map<Class, Function<String, ?>> STRING_CONVERTERS = new HashMap<>();

  /**
   * How do we go from one type of time class to another?
   */
  private static final Map<Pair<Class, Class>, Function<Object, Object>> TIME_CONVERTERS =
      new HashMap<>();

  static {
    // Register the known primitive types and default values thereof.
    registerPrimitiveClass(boolean.class, false);
    registerPrimitiveClass(byte.class, 0xB);
    registerPrimitiveClass(char.class, '\u0000');
    registerPrimitiveClass(double.class, 0D);
    registerPrimitiveClass(float.class, 0F);
    registerPrimitiveClass(int.class, 0);
    registerPrimitiveClass(long.class, 0L);
    registerPrimitiveClass(short.class, 0);
    registerPrimitiveClass(Boolean.class, false);
    registerPrimitiveClass(Byte.class, 0xB);
    registerPrimitiveClass(Character.class, '\u0000');
    registerPrimitiveClass(Double.class, 0D);
    registerPrimitiveClass(Float.class, 0F);
    registerPrimitiveClass(Integer.class, 0);
    registerPrimitiveClass(Long.class, 0L);
    registerPrimitiveClass(Short.class, 0);
    registerPrimitiveClass(Void.class);
    registerPrimitiveClass(String.class, "");
    registerPrimitiveClass(Date.class, new Date(0));
    registerPrimitiveClass(Instant.class, Instant.ofEpochMilli(0));

    // Register the string parsers for the known primitive types.
    registerStringConverters(byte.class, Byte::parseByte);
    registerStringConverters(char.class, string -> string.charAt(0));
    registerStringConverters(double.class, Double::parseDouble);
    registerStringConverters(float.class, Float::parseFloat);
    registerStringConverters(int.class, Integer::parseInt);
    registerStringConverters(long.class, Long::parseLong);
    registerStringConverters(short.class, Short::parseShort);
    registerStringConverters(Byte.class, Byte::parseByte);
    registerStringConverters(Character.class, string -> string.charAt(0));
    registerStringConverters(Double.class, Double::parseDouble);
    registerStringConverters(Float.class, Float::parseFloat);
    registerStringConverters(Integer.class, Integer::parseInt);
    registerStringConverters(Long.class, Long::parseLong);
    registerStringConverters(Short.class, Short::parseShort);
    registerStringConverters(String.class, Function.identity());

    // Register the time converters for the know time types.
    registerTimeConverters(Date.class, Date.class, Function.identity());
    registerTimeConverters(Date.class, Instant.class, value -> toInstant((Date) value));
    registerTimeConverters(Instant.class, Date.class, value -> toDate((Instant) value));
    registerTimeConverters(Instant.class, Instant.class, Function.identity());
    registerTimeConverters(Long.class, Date.class, value -> toDate((Long) value));
    registerTimeConverters(Long.class, Instant.class, value -> toInstant((Long) value));
    registerTimeConverters(Date.class, Long.class, value -> toEpoch((Date) value));
    registerTimeConverters(Instant.class, Long.class, value -> toEpoch((Instant) value));
  }

  private Primitives() {}

  public static void registerPrimitiveClass(Class primitiveClass) {
    registerPrimitiveClass(primitiveClass, null);
  }

  @SneakyThrows
  public static void registerPrimitiveClass(Class primitiveClass, Object primitiveDefault) {
    PRIMITIVE_DEFAULTS.put(primitiveClass, primitiveDefault);
  }

  @SuppressWarnings("unchecked")
  public static <P> void registerStringConverters(Class<P> primitiveClass,
      Function<String, P> stringConverter) {
    STRING_CONVERTERS.put(primitiveClass, stringConverter);
  }

  @SuppressWarnings("unchecked")
  public static void registerTimeConverters(Class sourceTimeClass, Class targetTimeClass,
      Function<Object, Object> timeConverter) {
    TIME_CONVERTERS.put(new Pair(sourceTimeClass, targetTimeClass), timeConverter);
  }

  public static boolean isPrimitive(Field field) {
    return isPrimitive(field.getType());
  }


  public static boolean isPrimitive(Class clazz) {
    return clazz.isEnum() || clazz.isPrimitive() || PRIMITIVE_DEFAULTS.containsKey(clazz);
  }

  public static boolean isPrimitiveDefault(Field field, Object value) {
    return value == null || (isPrimitive(field) && PRIMITIVE_DEFAULTS.get(field.getType())
        .equals(value));
  }

  public static Object asPrimitiveType(Field field, String defaultValue) {
    Function<String, ?> stringConverter = STRING_CONVERTERS.get(field.getType());
    return (stringConverter != null) ? stringConverter.apply(defaultValue) : null;
  }

  /**
   * The given object is considered to be "missing", in the sense that it does not have a value, if
   * it is {@code null} or it's a default primitive value, and {@link #allowDefaultKeys} is false.
   */
  public static boolean isMissing(Object object) {
    if (object == null) {
      return true;
    }
    if (allowDefaultKeys) {
      return false;
    }
    Class objectClass = object.getClass();
    Object defaultValue = PRIMITIVE_DEFAULTS.get(objectClass);
    return object.equals(defaultValue);
  }

  public static boolean isTimeType(Class clazz) {
    return clazz.equals(Date.class) || clazz.equals(Instant.class) || clazz.equals(Long.class);
  }

  public static Date toDate(long epoch) {
    return toDate(epoch, TimeUnit.MILLISECONDS);
  }

  public static Date toDate(long epoch, TimeUnit timeUnit) {
    return new Date(timeUnit.toMillis(epoch));
  }

  public static long toEpoch(Date date) {
    return date.getTime();
  }

  public static Instant toInstant(long epoch) {
    return Instant.ofEpochMilli(epoch);
  }

  public static long toEpoch(Instant instant) {
    return instant.toEpochMilli();
  }

  public static Instant toInstant(Date date) {
    return date.toInstant();
  }

  public static Date toDate(Instant instant) {
    return Date.from(instant);
  }

  /**
   * Convert the given time value to an instance of the target time type.
   */
  @SuppressWarnings("unchecked")
  public static <T> T toTimeType(Object timeValue, Class<T> targetTimeType) {
    Class valueClass = timeValue.getClass();
    Function<Object, Object> timeConverter =
        TIME_CONVERTERS.get(new Pair(valueClass, targetTimeType));
    if (timeConverter == null) {
      throw Element.Exceptions.invalidTimeType(targetTimeType, timeValue);
    }
    return (T) timeConverter.apply(timeValue);
  }
}