#
#  Copyright (c) 2011-2014 Exxeleron GmbH
#
#  Licensed 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.
#

from qpython import MetaData
from qpython.qtype import *  # @UnusedWildImport
from numpy import longlong

_MILLIS_PER_DAY = 24 * 60 * 60 * 1000
_MILLIS_PER_DAY_FLOAT = float(_MILLIS_PER_DAY)
_QEPOCH_MS = long(10957 * _MILLIS_PER_DAY)
_EPOCH_QTIMESTAMP_NS = _QEPOCH_MS * 1000000


_EPOCH_QMONTH = numpy.datetime64('2000-01', 'M')
_EPOCH_QDATE = numpy.datetime64('2000-01-01', 'D')
_EPOCH_QDATETIME = numpy.datetime64(_QEPOCH_MS, 'ms')
_EPOCH_TIMESTAMP = numpy.datetime64(_EPOCH_QTIMESTAMP_NS, 'ns')


_QMONTH_NULL = qnull(QMONTH)
_QDATE_NULL = qnull(QDATE)
_QDATETIME_NULL = qnull(QDATETIME)
_QMINUTE_NULL = qnull(QMINUTE)
_QSECOND_NULL = qnull(QSECOND)
_QTIME_NULL = qnull(QTIME)
_QTIMESTAMP_NULL = qnull(QTIMESTAMP)
_QTIMESPAN_NULL = qnull(QTIMESPAN)



class QTemporal(object):
    '''
    Represents a q temporal value.
    
    The :class:`.QTemporal` wraps `numpy.datetime64` or `numpy.timedelta64`
    along with meta-information like qtype indicator.
    
    :Parameters:
     - `dt` (`numpy.datetime64` or `numpy.timedelta64`) - datetime to be wrapped
    '''

    def __init__(self, dt):
        self._datetime = dt

    def _meta_init(self, **meta):
        self.meta = MetaData(**meta)

    @property
    def raw(self):
        '''Return wrapped datetime object.
        
        :returns: `numpy.datetime64` or `numpy.timedelta64` - wrapped datetime
        '''
        return self._datetime

    def __str__(self):
        return '%s [%s]' % (self._datetime, self.meta)

    def __eq__(self, other):
        return (isinstance(other, self.__class__)
            and self.meta.qtype == other.meta.qtype
            and self._datetime == other._datetime)

    def __ne__(self, other):
        return not self.__eq__(other)



def qtemporal(dt, **meta):
    '''Converts a `numpy.datetime64` or `numpy.timedelta64` to 
    :class:`.QTemporal` and enriches object instance with given meta data.
    
    Examples:
    
       >>> qtemporal(numpy.datetime64('2001-01-01', 'D'), qtype=QDATE)
       2001-01-01 [metadata(qtype=-14)]
       >>> qtemporal(numpy.timedelta64(43499123, 'ms'), qtype=QTIME)
       43499123 milliseconds [metadata(qtype=-19)]
       >>> qtemporal(qnull(QDATETIME), qtype=QDATETIME)
       nan [metadata(qtype=-15)]
    
    :Parameters:
     - `dt` (`numpy.datetime64` or `numpy.timedelta64`) - datetime to be wrapped
    :Kwargs:
     - `qtype` (`integer`) - qtype indicator
    
    :returns: `QTemporal` - wrapped datetime 
    '''
    result = QTemporal(dt)
    result._meta_init(**meta)
    return result



def from_raw_qtemporal(raw, qtype):
    '''
    Converts raw numeric value to `numpy.datetime64` or `numpy.timedelta64`
    instance.
    
    Actual conversion applied to raw numeric value depends on `qtype` parameter.
    
    :Parameters:
     - `raw` (`integer`, `float`) - raw representation to be converted
     - `qtype` (`integer`) - qtype indicator
     
    :returns: `numpy.datetime64` or `numpy.timedelta64` - converted datetime
    '''
    return _FROM_Q[qtype](raw)



def to_raw_qtemporal(dt, qtype):
    '''
    Converts datetime/timedelta instance to raw numeric value.
    
    Actual conversion applied to datetime/timedelta instance depends on `qtype` 
    parameter.
    
    :Parameters:
     - `dt` (`numpy.datetime64` or `numpy.timedelta64`) - datetime/timedelta
       object to be converted
     - `qtype` (`integer`) - qtype indicator
     
    :returns: `integer`, `float` - raw numeric value
    '''
    return _TO_Q[qtype](dt)



def array_from_raw_qtemporal(raw, qtype):
    '''
    Converts `numpy.array` containing raw q representation to ``datetime64``/``timedelta64``
    array.
    
    Examples:
    
      >>> raw = numpy.array([366, 121, qnull(QDATE)])
      >>> print(array_from_raw_qtemporal(raw, qtype = QDATE))
      ['2001-01-01' '2000-05-01' 'NaT']
    
    :Parameters:
     - `raw` (`numpy.array`) - numpy raw array to be converted
     - `qtype` (`integer`) - qtype indicator
    
    :returns: `numpy.array` - numpy array with ``datetime64``/``timedelta64``
    
    :raises: `ValueError`
    '''
    if not isinstance(raw, numpy.ndarray):
        raise ValueError('raw parameter is expected to be of type: numpy.ndarray. Was: %s' % type(raw))

    qtype = -abs(qtype)
    conversion = _FROM_RAW_LIST[qtype]

    mask = raw == qnull(qtype)

    dtype = PY_TYPE[qtype]
    array = raw.astype(dtype) if dtype != raw.dtype else raw

    array = conversion(array) if conversion else array
    null = _NUMPY_NULL[qtype]
    array = numpy.where(mask, null, array)
    return array



def array_to_raw_qtemporal(array, qtype):
    '''
    Converts `numpy.array` containing ``datetime64``/``timedelta64`` to raw
    q representation.
    
    Examples:
    
      >>> na_dt = numpy.arange('1999-01-01', '2005-12-31', dtype='datetime64[D]')
      >>> print(array_to_raw_qtemporal(na_dt, qtype = QDATE_LIST))
      [-365 -364 -363 ..., 2188 2189 2190]
      >>> array_to_raw_qtemporal(numpy.arange(-20, 30, dtype='int32'), qtype = QDATE_LIST)
      Traceback (most recent call last):
        ...
      ValueError: array.dtype is expected to be of type: datetime64 or timedelta64. Was: int32
    
    :Parameters:
     - `array` (`numpy.array`) - numpy datetime/timedelta array to be converted
     - `qtype` (`integer`) - qtype indicator
    
    :returns: `numpy.array` - numpy array with raw values
    
    :raises: `ValueError`
    '''
    if not isinstance(array, numpy.ndarray):
        raise ValueError('array parameter is expected to be of type: numpy.ndarray. Was: %s' % type(array))

    if not array.dtype.type in (numpy.datetime64, numpy.timedelta64):
        raise ValueError('array.dtype is expected to be of type: datetime64 or timedelta64. Was: %s' % array.dtype)

    qtype = -abs(qtype)
    conversion = _TO_RAW_LIST[qtype]
    raw = array.view(numpy.int64).view(numpy.ndarray)
    mask = raw == numpy.int64(-2 ** 63)

    raw = conversion(raw) if conversion else raw
    null = qnull(qtype)
    raw = numpy.where(mask, null, raw)
    return raw



def _from_qmonth(raw):
    if raw == _QMONTH_NULL:
        return _NUMPY_NULL[QMONTH]
    else:
        return _EPOCH_QMONTH + numpy.timedelta64(int(raw), 'M')



def _to_qmonth(dt):
    t_dt = type(dt)
    if t_dt == numpy.int32:
        return dt
    elif t_dt == numpy.datetime64:
        return (dt - _EPOCH_QMONTH).astype(int) if not dt == _NUMPY_NULL[QMONTH] else _QMONTH_NULL
    else:
        raise ValueError('Cannot convert %s of type %s to q value.' % (dt, type(dt)))



def _from_qdate(raw):
    if raw == _QDATE_NULL:
        return _NUMPY_NULL[QDATE]
    else:
        return _EPOCH_QDATE + numpy.timedelta64(int(raw), 'D')



def _to_qdate(dt):
    t_dt = type(dt)
    if t_dt == numpy.int32:
        return dt
    elif t_dt == numpy.datetime64:
        return (dt - _EPOCH_QDATE).astype(int) if not dt == _NUMPY_NULL[QDATE] else _QDATE_NULL
    else:
        raise ValueError('Cannot convert %s of type %s to q value.' % (dt, type(dt)))



def _from_qdatetime(raw):
    if numpy.isnan(raw) or raw == _QDATETIME_NULL:
        return _NUMPY_NULL[QDATETIME]
    else:
        return _EPOCH_QDATETIME + numpy.timedelta64(long(_MILLIS_PER_DAY * raw), 'ms')



def _to_qdatetime(dt):
    t_dt = type(dt)
    if t_dt == numpy.float64:
        return dt
    elif t_dt == numpy.datetime64:
        return (dt - _EPOCH_QDATETIME).astype(float) / _MILLIS_PER_DAY if not dt == _NUMPY_NULL[QDATETIME] else _QDATETIME_NULL
    else:
        raise ValueError('Cannot convert %s of type %s to q value.' % (dt, type(dt)))



def _from_qminute(raw):
    if raw == _QMINUTE_NULL:
        return _NUMPY_NULL[QMINUTE]
    else:
        return numpy.timedelta64(int(raw), 'm')



def _to_qminute(dt):
    t_dt = type(dt)
    if t_dt == numpy.int32:
        return dt
    elif t_dt == numpy.timedelta64:
        return dt.astype(int) if not dt == _NUMPY_NULL[QMINUTE] else _QMINUTE_NULL
    else:
        raise ValueError('Cannot convert %s of type %s to q value.' % (dt, type(dt)))



def _from_qsecond(raw):
    if raw == _QSECOND_NULL:
        return _NUMPY_NULL[QSECOND]
    else:
        return numpy.timedelta64(int(raw), 's')



def _to_qsecond(dt):
    t_dt = type(dt)
    if t_dt == numpy.int32:
        return dt
    elif t_dt == numpy.timedelta64:
        return dt.astype(int) if not dt == _NUMPY_NULL[QSECOND] else _QSECOND_NULL
    else:
        raise ValueError('Cannot convert %s of type %s to q value.' % (dt, type(dt)))



def _from_qtime(raw):
    if raw == _QTIME_NULL:
        return _NUMPY_NULL[QTIME]
    else:
        return numpy.timedelta64(int(raw), 'ms')



def _to_qtime(dt):
    t_dt = type(dt)
    if t_dt == numpy.int32:
        return dt
    elif t_dt == numpy.timedelta64:
        return dt.astype(int) if not dt == _NUMPY_NULL[QTIME] else _QTIME_NULL
    else:
        raise ValueError('Cannot convert %s of type %s to q value.' % (dt, type(dt)))



def _from_qtimestamp(raw):
    if raw == _QTIMESTAMP_NULL:
        return _NUMPY_NULL[QTIMESTAMP]
    else:
        return _EPOCH_TIMESTAMP + numpy.timedelta64(long(raw), 'ns')



def _to_qtimestamp(dt):
    t_dt = type(dt)
    if t_dt == numpy.int64:
        return dt
    elif t_dt == numpy.datetime64:
        return (dt - _EPOCH_TIMESTAMP).astype(longlong) if not dt == _NUMPY_NULL[QTIMESTAMP] else _QTIMESTAMP_NULL
    else:
        raise ValueError('Cannot convert %s of type %s to q value.' % (dt, type(dt)))



def _from_qtimespan(raw):
    if raw == _QTIMESPAN_NULL:
        return _NUMPY_NULL[QTIMESPAN]
    else:
        return numpy.timedelta64(long(raw), 'ns')



def _to_qtimespan(dt):
    t_dt = type(dt)
    if t_dt == numpy.int64:
        return dt
    elif t_dt == numpy.timedelta64:
        return dt.astype(longlong) if not dt == _NUMPY_NULL[QTIMESPAN] else _QTIMESTAMP_NULL
    else:
        raise ValueError('Cannot convert %s of type %s to q value.' % (dt, type(dt)))



_FROM_Q = {
           QMONTH:          _from_qmonth,
           QDATE:           _from_qdate,
           QDATETIME:       _from_qdatetime,
           QMINUTE:         _from_qminute,
           QSECOND:         _from_qsecond,
           QTIME:           _from_qtime,
           QTIMESTAMP:      _from_qtimestamp,
           QTIMESPAN:       _from_qtimespan,
           }



_TO_Q = {
         QMONTH:            _to_qmonth,
         QDATE:             _to_qdate,
         QDATETIME:         _to_qdatetime,
         QMINUTE:           _to_qminute,
         QSECOND:           _to_qsecond,
         QTIME:             _to_qtime,
         QTIMESTAMP:        _to_qtimestamp,
         QTIMESPAN:         _to_qtimespan,
         }


_TO_RAW_LIST = {
                QMONTH:      lambda a: (a - 360).astype(numpy.int32),
                QDATE:       lambda a: (a - 10957).astype(numpy.int32),
                QDATETIME:   lambda a: ((a - _QEPOCH_MS) / _MILLIS_PER_DAY_FLOAT).astype(numpy.float64),
                QMINUTE:     lambda a: a.astype(numpy.int32),
                QSECOND:     lambda a: a.astype(numpy.int32),
                QTIME:       lambda a: a.astype(numpy.int32),
                QTIMESTAMP:  lambda a: a - _EPOCH_QTIMESTAMP_NS,
                QTIMESPAN:   None,
                }



_FROM_RAW_LIST = {
                  QMONTH:      lambda a: numpy.array((a + 360), dtype = 'datetime64[M]'),
                  QDATE:       lambda a: numpy.array((a + 10957), dtype = 'datetime64[D]'),
                  QDATETIME:   lambda a: numpy.array((a * _MILLIS_PER_DAY + _QEPOCH_MS), dtype = 'datetime64[ms]'),
                  QMINUTE:     lambda a: numpy.array(a, dtype = 'timedelta64[m]'),
                  QSECOND:     lambda a: numpy.array(a, dtype = 'timedelta64[s]'),
                  QTIME:       lambda a: numpy.array(a, dtype = 'timedelta64[ms]'),
                  QTIMESTAMP:  lambda a: numpy.array((a + _EPOCH_QTIMESTAMP_NS), dtype = 'datetime64[ns]'),
                  QTIMESPAN:   lambda a: numpy.array(a, dtype = 'timedelta64[ns]'),
                  }



_NUMPY_NULL = {
               QMONTH:      numpy.datetime64('NaT', 'M'),
               QDATE:       numpy.datetime64('NaT', 'D'),
               QDATETIME:   numpy.datetime64('NaT', 'ms'),
               QMINUTE:     numpy.timedelta64('NaT', 'm'),
               QSECOND:     numpy.timedelta64('NaT', 's'),
               QTIME:       numpy.timedelta64('NaT', 'ms'),
               QTIMESTAMP:  numpy.datetime64('NaT', 'ns'),
               QTIMESPAN:   numpy.timedelta64('NaT', 'ns'),
               }