package com.avaje.ebeaninternal.server.core;

import com.avaje.ebeaninternal.server.type.ScalarTypeUUIDBinary;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Calendar;
import java.util.UUID;

/**
 * Default implementation of TypeConverter.
 * <p>
 * Converts objects to the required type if required.
 * </p>
 */
public final class BasicTypeConverter implements Serializable {

  private static final long serialVersionUID = 7691463236204070311L;

  /**
   * Type code for java.util.Calendar.
   */
  public static final int UTIL_CALENDAR = -999998986;

  /**
   * Type code for java.util.Date.
   */
  public static final int UTIL_DATE = -999998988;

  private BasicTypeConverter() {
  }

  /**
   * Convert the Object to the required data type.
   *
   * @param value      the Object value
   * @param toDataType the dataType as per java.sql.Types.
   */
  public static Object convert(Object value, int toDataType) {

    try {
      switch (toDataType) {
        case UTIL_DATE: {
          return toUtilDate(value);
        }
        case UTIL_CALENDAR: {
          return toCalendar(value);
        }
        case Types.BIGINT: {
          return toLong(value);
        }
        case Types.INTEGER: {
          return toInteger(value);
        }
        case Types.BIT: {
          return toBoolean(value);
        }
        case Types.TINYINT: {
          return toByte(value);
        }
        case Types.SMALLINT: {
          return toShort(value);
        }
        case Types.NUMERIC: {
          return toBigDecimal(value);
        }
        case Types.DECIMAL: {
          return toBigDecimal(value);
        }
        case Types.REAL: {
          return toFloat(value);
        }
        case Types.DOUBLE: {
          return toDouble(value);
        }
        case Types.FLOAT: {
          return toDouble(value);
        }
        case Types.BOOLEAN: {
          return toBoolean(value);
        }
        case Types.TIMESTAMP: {
          return toTimestamp(value);
        }
        case Types.DATE: {
          return toDate(value);
        }
        case Types.VARCHAR: {
          return toString(value);
        }
        case Types.CHAR: {
          return toString(value);
        }
        case Types.OTHER: {
          return value;
        }
        case Types.JAVA_OBJECT: {
          return value;
        }
        case Types.BINARY:
        case Types.LONGVARBINARY:
        case Types.BLOB: {
          return value;
        }
        case Types.LONGVARCHAR:
        case Types.CLOB: {
          return value;
        }
        default: {
          String msg = "Unhandled data type [" + toDataType + "] converting [" + value + "]";
          throw new RuntimeException(msg);
        }
      }
    } catch (ClassCastException e) {
      String m = "ClassCastException converting to data type [" + toDataType + "] value [" + value + "]";
      throw new RuntimeException(m);
    }
  }

  /**
   * Convert the value to a String.
   */
  public static String toString(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof String) {
      return (String) value;
    }
    if (value instanceof char[]) {
      return String.valueOf((char[]) value);
    }

    return value.toString();
  }


  /**
   * Convert the value to a Boolean with an explicit String true value.
   */
  public static Boolean toBoolean(Object value, String dbTrueValue) {

    if (value == null) {
      return null;
    }
    if (value instanceof Boolean) {
      return (Boolean) value;
    }
    if (value instanceof Number) {
      return ((Number)value).intValue() == 1;
    }
    String s = value.toString();
    return s.equalsIgnoreCase(dbTrueValue);
  }

  /**
   * Convert the value to a Boolean. Can be a Boolean or the string values
   * "true" or "false".
   */
  public static Boolean toBoolean(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof Boolean) {
      return (Boolean) value;
    }

    return Boolean.valueOf(value.toString());
  }

  /**
   * Convert the value to a UUID.
   */
  public static UUID toUUID(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof UUID) {
      return (UUID) value;
    }
    if (value instanceof String) {
      return UUID.fromString((String) value);
    }
    if (value instanceof byte[]) {
      return ScalarTypeUUIDBinary.convertFromBytes((byte[]) value);
    }
    return UUID.fromString(value.toString());
  }

  /**
   * convert the passed in object to a BigDecimal. It should be another
   * numeric type.
   */
  public static BigDecimal toBigDecimal(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof BigDecimal) {
      return (BigDecimal) value;
    }
    return new BigDecimal(value.toString());
  }

  public static Float toFloat(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof Float) {
      return (Float) value;
    }
    if (value instanceof Number) {
      return ((Number) value).floatValue();
    }
    return Float.valueOf(value.toString());
  }

  public static Short toShort(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof Short) {
      return (Short) value;
    }
    if (value instanceof Number) {
      return ((Number) value).shortValue();
    }
    return Short.valueOf(value.toString());
  }

  public static Byte toByte(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof Byte) {
      return (Byte) value;
    }
    return Byte.valueOf(value.toString());
  }

  /**
   * convert the passed in object to a Integer. It should be another numeric
   * type.
   */
  public static Integer toInteger(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof Integer) {
      return (Integer) value;
    }
    if (value instanceof Number) {
      return ((Number) value).intValue();
    }
    return Integer.valueOf(value.toString());
  }

  /**
   * Convert the object to a Long. It should be another numeric type.
   */
  public static Long toLong(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof Long) {
      return (Long) value;
    }
    if (value instanceof String) {
      return Long.valueOf((String) value);
    }
    if (value instanceof Number) {
      return ((Number) value).longValue();
    }
    if (value instanceof java.util.Date) {
      return ((java.util.Date) value).getTime();
    }
    if (value instanceof Calendar) {
      return ((Calendar) value).getTime().getTime();
    }
    return Long.valueOf(value.toString());
  }

  public static BigInteger toMathBigInteger(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof BigInteger) {
      return (BigInteger) value;
    }
    return new BigInteger(value.toString());
  }

  /**
   * Convert the object to a Double. It should be another numberic type.
   */
  public static Double toDouble(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof Double) {
      return (Double) value;
    }
    if (value instanceof Number) {
      return ((Number) value).doubleValue();
    }
    return Double.valueOf(value.toString());
  }

  /**
   * convert the passed in object to a Timestamp. It is expected to be a
   * java.sql.Date really.
   */
  public static Timestamp toTimestamp(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof Timestamp) {
      return (Timestamp) value;

    } else if (value instanceof java.util.Date) {
      // no nanos here... so hopefully ok
      return new Timestamp(((java.util.Date) value).getTime());

    } else if (value instanceof Calendar) {
      return new Timestamp(((Calendar) value).getTime().getTime());

    } else if (value instanceof String) {
      return Timestamp.valueOf((String) value);

    } else if (value instanceof Number) {
      return new Timestamp(((Number) value).longValue());

    } else {
      String msg = "Unable to convert [" + value.getClass().getName() + "] into a Timestamp.";
      throw new RuntimeException(msg);
    }
  }

  public static java.sql.Time toTime(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof java.sql.Time) {
      return (java.sql.Time) value;

    } else if (value instanceof String) {
      return java.sql.Time.valueOf((String) value);

    } else {
      String m = "Unable to convert [" + value.getClass().getName() + "] into a java.sql.Date.";
      throw new RuntimeException(m);
    }
  }

  /**
   * convert the passed in object to a java sql Date.
   */
  public static java.sql.Date toDate(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof java.sql.Date) {
      return (java.sql.Date) value;

    } else if (value instanceof java.util.Date) {
      return new java.sql.Date(((java.util.Date) value).getTime());

    } else if (value instanceof Calendar) {
      return new java.sql.Date(((Calendar) value).getTime().getTime());

    } else if (value instanceof String) {
      return java.sql.Date.valueOf((String) value);

    } else if (value instanceof Number) {
      return new java.sql.Date(((Number) value).longValue());

    } else {
      String m = "Unable to convert [" + value.getClass().getName() + "] into a java.sql.Date.";
      throw new RuntimeException(m);
    }
  }

  /**
   * convert the passed in object to a java sql Date.
   */
  public static java.util.Date toUtilDate(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof java.sql.Timestamp) {
      // loss of nanos precision
      return new java.util.Date(((java.sql.Timestamp) value).getTime());
    }
    // DEVNOTE: strictly speaking do I need to convert a java.sql.Date to
    // java.util.Date? equals() is symmetrical so perhaps this is not
    // really required?
    if (value instanceof java.sql.Date) {
      return new java.util.Date(((java.sql.Date) value).getTime());
    }
    if (value instanceof java.util.Date) {
      return (java.util.Date) value;

    } else if (value instanceof Calendar) {
      return ((Calendar) value).getTime();

    } else if (value instanceof String) {
      return new java.util.Date(Timestamp.valueOf((String) value).getTime());

    } else if (value instanceof Number) {
      return new java.util.Date(((Number) value).longValue());

    } else {
      throw new RuntimeException("Unable to convert [" + value.getClass().getName() + "] into a java.util.Date");
    }
  }

  /**
   * convert the passed in object to a java sql Date.
   */
  public static Calendar toCalendar(Object value) {

    if (value == null) {
      return null;
    }
    if (value instanceof Calendar) {
      return (Calendar) value;

    } else if (value instanceof java.util.Date) {
      java.util.Date date = ((java.util.Date) value);
      return toCalendarFromDate(date);

    } else if (value instanceof String) {
      java.util.Date date = toUtilDate(value);
      return toCalendarFromDate(date);

    } else if (value instanceof Number) {
      long timeMillis = ((Number) value).longValue();
      java.util.Date date = new java.util.Date(timeMillis);
      return toCalendarFromDate(date);

    } else {
      String m = "Unable to convert [" + value.getClass().getName() + "] into a java.util.Date";
      throw new RuntimeException(m);
    }
  }

  private static Calendar toCalendarFromDate(java.util.Date date) {

    Calendar cal = Calendar.getInstance();
    cal.setTime(date);

    return cal;
  }

}
