/*
   Copyright (c) 2004, 2012, Oracle and/or its affiliates.
   Copyright (c) 2013, MariaDB Foundation.

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; version 2 of the License.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335  USA */

#include "mariadb.h"
#include "compat56.h"
#include "myisampack.h"
#include "my_time.h"


static const int my_max_usec_value[7]
{
  0,
  900000,
  990000,
  999000,
  999900,
  999990,
  999999
};


/*** MySQL56 TIME low-level memory and disk representation routines ***/

/*
  In-memory format:

   1  bit sign          (Used for sign, when on disk)
   1  bit unused        (Reserved for wider hour range, e.g. for intervals)
   10 bit hour          (0-836)
   6  bit minute        (0-59)
   6  bit second        (0-59)
  24  bits microseconds (0-999999)

 Total: 48 bits = 6 bytes
   Suhhhhhh.hhhhmmmm.mmssssss.ffffffff.ffffffff.ffffffff
*/


/**
  Convert time value to MySQL56 numeric packed representation.
  
  @param    ltime   The value to convert.
  @return           Numeric packed representation.
*/
longlong TIME_to_longlong_time_packed(const MYSQL_TIME *ltime)
{
  DBUG_ASSERT(ltime->year == 0);
  DBUG_ASSERT(ltime->month == 0);
  // Mix days with hours: "1 00:10:10" -> "24:10:10"
  long hms= ((ltime->day * 24 + ltime->hour) << 12) |
            (ltime->minute << 6) | ltime->second;
  longlong tmp= MY_PACKED_TIME_MAKE(hms, ltime->second_part);
  return ltime->neg ? -tmp : tmp;
}



/**
  Convert MySQL56 time packed numeric representation to time.

  @param  OUT ltime  The MYSQL_TIME variable to set.
  @param      tmp    The packed numeric representation.
*/
void TIME_from_longlong_time_packed(MYSQL_TIME *ltime, longlong tmp)
{
  long hms;
  if ((ltime->neg= (tmp < 0)))
    tmp= -tmp;
  hms= (long) MY_PACKED_TIME_GET_INT_PART(tmp);
  ltime->year=   (uint) 0;
  ltime->month=  (uint) 0;
  ltime->day=    (uint) 0;
  ltime->hour=   (uint) (hms >> 12) % (1 << 10); /* 10 bits starting at 12th */
  ltime->minute= (uint) (hms >> 6)  % (1 << 6);  /* 6 bits starting at 6th   */
  ltime->second= (uint)  hms        % (1 << 6);  /* 6 bits starting at 0th   */
  ltime->second_part= MY_PACKED_TIME_GET_FRAC_PART(tmp);
  ltime->time_type= MYSQL_TIMESTAMP_TIME;
}


/**
  Calculate binary size of MySQL56 packed numeric time representation.
  
  @param   dec   Precision.
*/
uint my_time_binary_length(uint dec)
{
  DBUG_ASSERT(dec <= TIME_SECOND_PART_DIGITS);
  return 3 + (dec + 1) / 2;
}


/*
  On disk we convert from signed representation to unsigned
  representation using TIMEF_OFS, so all values become binary comparable.
*/
#define TIMEF_OFS 0x800000000000LL
#define TIMEF_INT_OFS 0x800000LL


/**
  Convert MySQL56 in-memory numeric time representation to on-disk representation
  
  @param       nr   Value in packed numeric time format.
  @param   OUT ptr  The buffer to put value at.
  @param       dec  Precision.
*/
void my_time_packed_to_binary(longlong nr, uchar *ptr, uint dec)
{
  DBUG_ASSERT(dec <= TIME_SECOND_PART_DIGITS);
  /* Make sure the stored value was previously properly rounded or truncated */
  DBUG_ASSERT((MY_PACKED_TIME_GET_FRAC_PART(nr) % 
              (int) log_10_int[TIME_SECOND_PART_DIGITS - dec]) == 0);

  switch (dec)
  {
  case 0:
  default:
    mi_int3store(ptr, TIMEF_INT_OFS + MY_PACKED_TIME_GET_INT_PART(nr));
    break;

  case 1:
  case 2:
    mi_int3store(ptr, TIMEF_INT_OFS + MY_PACKED_TIME_GET_INT_PART(nr));
    ptr[3]= (unsigned char) (char) (MY_PACKED_TIME_GET_FRAC_PART(nr) / 10000);
    break;

  case 4:
  case 3:
    mi_int3store(ptr, TIMEF_INT_OFS + MY_PACKED_TIME_GET_INT_PART(nr));
    mi_int2store(ptr + 3, MY_PACKED_TIME_GET_FRAC_PART(nr) / 100);
    break;

  case 5:
  case 6:
    mi_int6store(ptr, nr + TIMEF_OFS);
    break;
  }
}


/**
  Convert MySQL56 on-disk time representation to in-memory packed numeric 
  representation.
  
  @param   ptr  The pointer to read the value at.
  @param   dec  Precision.
  @return       Packed numeric time representation.
*/
longlong my_time_packed_from_binary(const uchar *ptr, uint dec)
{
  DBUG_ASSERT(dec <= TIME_SECOND_PART_DIGITS);

  switch (dec)
  {
  case 0:
  default:
    {
      longlong intpart= mi_uint3korr(ptr) - TIMEF_INT_OFS;
      return MY_PACKED_TIME_MAKE_INT(intpart);
    }
  case 1:
  case 2:
    {
      longlong intpart= mi_uint3korr(ptr) - TIMEF_INT_OFS;
      int frac= (uint) ptr[3];
      if (intpart < 0 && frac)
      {
        /*
          Negative values are stored with reverse fractional part order,
          for binary sort compatibility.

            Disk value  intpart frac   Time value   Memory value
            800000.00    0      0      00:00:00.00  0000000000.000000
            7FFFFF.FF   -1      255   -00:00:00.01  FFFFFFFFFF.FFD8F0
            7FFFFF.9D   -1      99    -00:00:00.99  FFFFFFFFFF.F0E4D0
            7FFFFF.00   -1      0     -00:00:01.00  FFFFFFFFFF.000000
            7FFFFE.FF   -1      255   -00:00:01.01  FFFFFFFFFE.FFD8F0
            7FFFFE.F6   -2      246   -00:00:01.10  FFFFFFFFFE.FE7960

            Formula to convert fractional part from disk format
            (now stored in "frac" variable) to absolute value: "0x100 - frac".
            To reconstruct in-memory value, we shift
            to the next integer value and then substruct fractional part.
        */
        intpart++;    /* Shift to the next integer value */
        frac-= 0x100; /* -(0x100 - frac) */
      }
      return MY_PACKED_TIME_MAKE(intpart, frac * 10000);
    }

  case 3:
  case 4:
    {
      longlong intpart= mi_uint3korr(ptr) - TIMEF_INT_OFS;
      int frac= mi_uint2korr(ptr + 3);
      if (intpart < 0 && frac)
      {
        /*
          Fix reverse fractional part order: "0x10000 - frac".
          See comments for FSP=1 and FSP=2 above.
        */
        intpart++;      /* Shift to the next integer value */
        frac-= 0x10000; /* -(0x10000-frac) */
      }
      return MY_PACKED_TIME_MAKE(intpart, frac * 100);
    }

  case 5:
  case 6:
    return ((longlong) mi_uint6korr(ptr)) - TIMEF_OFS;
  }
}


/*** MySQL56 DATETIME low-level memory and disk representation routines ***/

/*
    1 bit  sign            (used when on disk)
   17 bits year*13+month   (year 0-9999, month 0-12)
    5 bits day             (0-31)
    5 bits hour            (0-23)
    6 bits minute          (0-59)
    6 bits second          (0-59)
   24 bits microseconds    (0-999999)

   Total: 64 bits = 8 bytes

   SYYYYYYY.YYYYYYYY.YYdddddh.hhhhmmmm.mmssssss.ffffffff.ffffffff.ffffffff
*/

/**
  Convert datetime to MySQL56 packed numeric datetime representation.
  @param ltime  The value to convert.
  @return       Packed numeric representation of ltime.
*/
longlong TIME_to_longlong_datetime_packed(const MYSQL_TIME *ltime)
{
  longlong ymd= ((ltime->year * 13 + ltime->month) << 5) | ltime->day;
  longlong hms= (ltime->hour << 12) | (ltime->minute << 6) | ltime->second;
  longlong tmp= MY_PACKED_TIME_MAKE(((ymd << 17) | hms), ltime->second_part);
  DBUG_ASSERT(!check_datetime_range(ltime)); /* Make sure no overflow */
  return ltime->neg ? -tmp : tmp;
}


/**
  Convert MySQL56 packed numeric datetime representation to MYSQL_TIME.
  @param OUT  ltime The datetime variable to convert to.
  @param      tmp   The packed numeric datetime value.
*/
void TIME_from_longlong_datetime_packed(MYSQL_TIME *ltime, longlong tmp)
{
  longlong ymd, hms;
  longlong ymdhms, ym;

  DBUG_ASSERT(tmp != LONGLONG_MIN);

  if ((ltime->neg= (tmp < 0)))
    tmp= -tmp;

  ltime->second_part= MY_PACKED_TIME_GET_FRAC_PART(tmp);
  ymdhms= MY_PACKED_TIME_GET_INT_PART(tmp);

  ymd= ymdhms >> 17;
  ym= ymd >> 5;
  hms= ymdhms % (1 << 17);

  ltime->day= ymd % (1 << 5);
  ltime->month= ym % 13;
  ltime->year= (uint) (ym / 13);

  ltime->second= hms % (1 << 6);
  ltime->minute= (hms >> 6) % (1 << 6);
  ltime->hour= (uint) (hms >> 12);
  
  ltime->time_type= MYSQL_TIMESTAMP_DATETIME;
}


/**
  Calculate binary size of MySQL56 packed datetime representation.
  @param dec  Precision.
*/
uint my_datetime_binary_length(uint dec)
{
  DBUG_ASSERT(dec <= TIME_SECOND_PART_DIGITS);
  return 5 + (dec + 1) / 2;
}


/*
  On disk we store as unsigned number with DATETIMEF_INT_OFS offset,
  for HA_KETYPE_BINARY compatibility purposes.
*/
#define DATETIMEF_INT_OFS 0x8000000000LL


/**
  Convert MySQL56 on-disk datetime representation
  to in-memory packed numeric representation.

  @param ptr   The pointer to read value at.
  @param dec   Precision.
  @return      In-memory packed numeric datetime representation.
*/
longlong my_datetime_packed_from_binary(const uchar *ptr, uint dec)
{
  longlong intpart= mi_uint5korr(ptr) - DATETIMEF_INT_OFS;
  int frac;
  DBUG_ASSERT(dec <= TIME_SECOND_PART_DIGITS);
  switch (dec)
  {
  case 0:
  default:
    return MY_PACKED_TIME_MAKE_INT(intpart);
  case 1:
  case 2:
    frac= ((int) (signed char) ptr[5]) * 10000;
    break;
  case 3:
  case 4:
    frac= mi_sint2korr(ptr + 5) * 100;
    break;
  case 5:
  case 6:
    frac= mi_sint3korr(ptr + 5);
    break;
  }
  return MY_PACKED_TIME_MAKE(intpart, frac);
}


/**
  Store MySQL56 in-memory numeric packed datetime representation to disk.

  @param      nr  In-memory numeric packed datetime representation.
  @param OUT  ptr The pointer to store at.
  @param      dec Precision, 1-6.
*/
void my_datetime_packed_to_binary(longlong nr, uchar *ptr, uint dec)
{
  DBUG_ASSERT(dec <= TIME_SECOND_PART_DIGITS);
  /* The value being stored must have been properly rounded or truncated */
  DBUG_ASSERT((MY_PACKED_TIME_GET_FRAC_PART(nr) %
              (int) log_10_int[TIME_SECOND_PART_DIGITS - dec]) == 0);

  mi_int5store(ptr, MY_PACKED_TIME_GET_INT_PART(nr) + DATETIMEF_INT_OFS);
  switch (dec)
  {
  case 0:
  default:
    break;
  case 1:
  case 2:
    ptr[5]= (unsigned char) (char) (MY_PACKED_TIME_GET_FRAC_PART(nr) / 10000);
    break;
  case 3:
  case 4:
    mi_int2store(ptr + 5, MY_PACKED_TIME_GET_FRAC_PART(nr) / 100);
    break;
  case 5:
  case 6:
    mi_int3store(ptr + 5, MY_PACKED_TIME_GET_FRAC_PART(nr));
  }
}


/*** MySQL56 TIMESTAMP low-level memory and disk representation routines ***/

/**
  Calculate on-disk size of a timestamp value.

  @param  dec  Precision.
*/
uint my_timestamp_binary_length(uint dec)
{
  DBUG_ASSERT(dec <= TIME_SECOND_PART_DIGITS);
  return 4 + (dec + 1) / 2;
}


/**
  Convert MySQL56 binary timestamp representation to in-memory representation.

  @param  OUT tm  The variable to convert to.
  @param      ptr The pointer to read the value from.
  @param      dec Precision.
*/
void my_timestamp_from_binary(struct timeval *tm, const uchar *ptr, uint dec)
{
  DBUG_ASSERT(dec <= TIME_SECOND_PART_DIGITS);
  tm->tv_sec= mi_uint4korr(ptr);
  switch (dec)
  {
    case 0:
    default:
      tm->tv_usec= 0;
      return;
    case 1:
    case 2:
      tm->tv_usec= ((int) ptr[4]) * 10000;
      break;
    case 3:
    case 4:
      tm->tv_usec= (uint) mi_uint2korr(ptr + 4) * 100;
      break;
    case 5:
    case 6:
      tm->tv_usec= (uint) mi_uint3korr(ptr + 4);
  }
  // The binary data my be corrupt. Cut fractional seconds to the valid range.
  set_if_smaller(tm->tv_usec, my_max_usec_value[dec]);
}


/**
  Convert MySQL56 in-memory timestamp representation to on-disk representation.

  @param        tm   The value to convert.
  @param  OUT   ptr  The pointer to store the value to.
  @param        dec  Precision.
*/
void my_timestamp_to_binary(const struct timeval *tm, uchar *ptr, uint dec)
{
  DBUG_ASSERT(dec <= TIME_SECOND_PART_DIGITS);
  /* Stored value must have been previously properly rounded or truncated */
  DBUG_ASSERT((tm->tv_usec %
               (int) log_10_int[TIME_SECOND_PART_DIGITS - dec]) == 0);
  mi_int4store(ptr, tm->tv_sec);
  switch (dec)
  {
    case 0:
    default:
      break;
    case 1:
    case 2:
      ptr[4]= (unsigned char) (char) (tm->tv_usec / 10000);
      break;
    case 3:
    case 4:
      mi_int2store(ptr + 4, tm->tv_usec / 100);
      break;
      /* Impossible second precision. Fall through */
    case 5:
    case 6:
      mi_int3store(ptr + 4, tm->tv_usec);
  }
}

/****************************************/
