# 
#  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.
# 

'''
The `qpython.qtype` module defines number of utility function which help to work
with types mapping between q and Python.

This module declares supported q types as constants, which can be used along
with conversion functions e.g.: :func:`.qcollection.qlist` or 
:func:`.qtemporal.qtemporal`. 

List of q type codes:

==================     =============
q type name             q type code
==================     =============
QNULL                  0x65
QGENERAL_LIST          0x00
QBOOL                  -0x01
QBOOL_LIST             0x01
QGUID                  -0x02
QGUID_LIST             0x02
QBYTE                  -0x04
QBYTE_LIST             0x04
QSHORT                 -0x05
QSHORT_LIST            0x05
QINT                   -0x06
QINT_LIST              0x06
QLONG                  -0x07
QLONG_LIST             0x07
QFLOAT                 -0x08
QFLOAT_LIST            0x08
QDOUBLE                -0x09
QDOUBLE_LIST           0x09
QCHAR                  -0x0a
QSTRING                0x0a
QSTRING_LIST           0x00
QSYMBOL                -0x0b
QSYMBOL_LIST           0x0b
QTIMESTAMP             -0x0c
QTIMESTAMP_LIST        0x0c
QMONTH                 -0x0d
QMONTH_LIST            0x0d
QDATE                  -0x0e
QDATE_LIST             0x0e
QDATETIME              -0x0f
QDATETIME_LIST         0x0f
QTIMESPAN              -0x10
QTIMESPAN_LIST         0x10
QMINUTE                -0x11
QMINUTE_LIST           0x11
QSECOND                -0x12
QSECOND_LIST           0x12
QTIME                  -0x13
QTIME_LIST             0x13
QDICTIONARY            0x63
QKEYED_TABLE           0x63
QTABLE                 0x62
QLAMBDA                0x64
QUNARY_FUNC            0x65
QBINARY_FUNC           0x66
QTERNARY_FUNC          0x67
QCOMPOSITION_FUNC      0x69
QADVERB_FUNC_106       0x6a
QADVERB_FUNC_107       0x6b
QADVERB_FUNC_108       0x6c
QADVERB_FUNC_109       0x6d
QADVERB_FUNC_110       0x6e
QADVERB_FUNC_111       0x6f
QPROJECTION            0x68
QERROR                 -0x80
==================     =============
'''

import numpy
import re
import uuid
import sys
from functools import reduce
from .qtypes import QInt8, QInt16, QInt32, QInt64, QDateDay, QDateMonth, QTimeMinute, QTimeSecond, QTimeMs, QTimeNs, QDatetimeMs, QDatetimeNs
if sys.version > '3':
    long = int

# qtype constants:
QNULL               =  0x65
QGENERAL_LIST       =  0x00
QBOOL               =  -0x01
QBOOL_LIST          =  0x01
QGUID               =  -0x02
QGUID_LIST          =  0x02
QBYTE               =  -0x04
QBYTE_LIST          =  0x04
QSHORT              =  -0x05
QSHORT_LIST         =  0x05
QINT                =  -0x06
QINT_LIST           =  0x06
QLONG               =  -0x07
QLONG_LIST          =  0x07
QFLOAT              =  -0x08
QFLOAT_LIST         =  0x08
QDOUBLE             =  -0x09
QDOUBLE_LIST        =  0x09
QCHAR               =  -0x0a
QSTRING             =  0x0a
QSTRING_LIST        =  0x00
QSYMBOL             =  -0x0b
QSYMBOL_LIST        =  0x0b

QTIMESTAMP          =  -0x0c
QTIMESTAMP_LIST     =  0x0c
QMONTH              =  -0x0d
QMONTH_LIST         =  0x0d
QDATE               =  -0x0e
QDATE_LIST          =  0x0e
QDATETIME           =  -0x0f
QDATETIME_LIST      =  0x0f
QTIMESPAN           =  -0x10
QTIMESPAN_LIST      =  0x10
QMINUTE             =  -0x11
QMINUTE_LIST        =  0x11
QSECOND             =  -0x12
QSECOND_LIST        =  0x12
QTIME               =  -0x13
QTIME_LIST          =  0x13

QDICTIONARY         =  0x63
QKEYED_TABLE        =  0x63
QTABLE              =  0x62
QLAMBDA             =  0x64
QUNARY_FUNC         =  0x65
QBINARY_FUNC        =  0x66
QTERNARY_FUNC       =  0x67
QCOMPOSITION_FUNC   =  0x69
QADVERB_FUNC_106    =  0x6a
QADVERB_FUNC_107    =  0x6b
QADVERB_FUNC_108    =  0x6c
QADVERB_FUNC_109    =  0x6d
QADVERB_FUNC_110    =  0x6e
QADVERB_FUNC_111    =  0x6f
QPROJECTION         =  0x68

QERROR              = -0x80



ATOM_SIZE = ( 0, 1, 16, 0, 1, 2, 4, 8, 4, 8, 1, 0, 8, 4, 4, 8, 8, 4, 4, 4 )



# mapping of q atoms to corresponding Python types
PY_TYPE = {
    QBOOL:          numpy.bool_,
    QBYTE:          QInt8,
    QGUID:          numpy.object_,
    QSHORT:         QInt16,
    QINT:           QInt32,
    QLONG:          QInt64,
    QFLOAT:         numpy.float32,
    QDOUBLE:        numpy.float64,
    QCHAR:          numpy.byte,
    QSYMBOL:        numpy.bytes_,
    QMONTH:         QDateMonth,
    QDATE:          QDateDay,
    QDATETIME:      QDatetimeMs,
    QMINUTE:        QTimeMinute,
    QSECOND:        QTimeSecond,
    QTIME:          QTimeMs,
    QTIMESTAMP:     QDatetimeNs,
    QTIMESPAN:      QTimeNs,
    # artificial qtype for convenient conversion of string lists
    QSTRING_LIST:   numpy.object_,
    }

class Char(str):
    """Wrapper for q char type to preserve type information"""
    
    def __new__(cls, value):
        if isinstance(value, bytes):
            decoded = value.decode('utf-8')
        else:
            decoded = str(value)
        return str.__new__(cls, decoded)
    
    def __str__(self):
        return f"Char('{super().__str__()}')"
    
    def __repr__(self):
        return f"Char('{super().__str__()}')"
    
    @property
    def str(self):
        return str.__str__(self)


class String(str):
    """Wrapper for q string type to preserve type information"""
    
    def __new__(cls, value):
        if isinstance(value, bytes):
            decoded = value.decode('utf-8')
        else:
            decoded = str(value)
        return str.__new__(cls, decoded)
    
    def __str__(self):
        return f'String("{super().__str__()}")'
    
    def __repr__(self):
        return f'String("{super().__str__()}")'
    
    @property
    def str(self):
        return str.__str__(self)
    
    def decode(self, encoding='utf-8', errors='strict'):
        """Add decode method for compatibility with qPython"""
        return str.__str__(self)

# mapping of Python types to corresponding q atoms
Q_TYPE = {
    bool             : QBOOL,
    numpy.bool_      : QBOOL,
    numpy.byte       : QBYTE,
    numpy.int16      : QSHORT,
    int              : QINT,
    numpy.int32      : QINT,
    long             : QLONG,
    numpy.int64      : QLONG,
    numpy.float32    : QFLOAT,
    float            : QDOUBLE,
    numpy.float64    : QDOUBLE,
    String           : QSTRING,
    Char             : QCHAR,
    str              : QSYMBOL,
    uuid.UUID        : QGUID,
    }

# mapping of numpy datetime64/timedelta64 to q temporal types
TEMPORAL_PY_TYPE = {
    'datetime64[M]'    : QMONTH,
    'datetime64[D]'    : QDATE,
    'datetime64[ms]'   : QDATETIME,
    'timedelta64[m]'   : QMINUTE,
    'timedelta64[s]'   : QSECOND,
    'timedelta64[ms]'  : QTIME,
    'datetime64[ns]'   : QTIMESTAMP,
    'timedelta64[ns]'  : QTIMESPAN
    }


TEMPORAL_Q_TYPE = {
    QMONTH:      'datetime64[M]',
    QDATE:       'datetime64[D]',
    QDATETIME:   'datetime64[ms]',
    QMINUTE:     'timedelta64[m]',
    QSECOND:     'timedelta64[s]',
    QTIME:       'timedelta64[ms]',
    QTIMESTAMP:  'datetime64[ns]',
    QTIMESPAN:   'timedelta64[ns]'
    }


# mapping of q types for Python binary translation
STRUCT_MAP = {
    QBOOL:      'b',
    QBYTE:      'b',
    QSHORT:     'h',
    QINT:       'i',
    QLONG:      'q',
    QFLOAT:     'f',
    QDOUBLE:    'd',
    QSTRING:    's',
    QSYMBOL:    'S',
    QCHAR:      'b',

    QMONTH:     'i',
    QDATE:      'i',
    QDATETIME:  'd',
    QMINUTE:    'i',
    QSECOND:    'i',
    QTIME:      'i',
    QTIMESPAN:  'q',
    QTIMESTAMP: 'q',
    }


# null definitions
_QNULL1 = numpy.int8(-2**7)
_QNULL2 = numpy.int16(-2**15)
_QNULL4 = numpy.int32(-2**31)
_QNULL8 = numpy.int64(-2**63)
_QNAN32 = numpy.frombuffer(b'\x00\x00\xc0\x7f', dtype=numpy.float32)[0]
_QNAN64 = numpy.frombuffer(b'\x00\x00\x00\x00\x00\x00\xf8\x7f', dtype=numpy.float64)[0]
_QNULL_BOOL = numpy.bool_(False)
_QNULL_SYM = numpy.bytes_('')
_QNULL_GUID = uuid.UUID('00000000-0000-0000-0000-000000000000')


QNULLMAP = {
    QGUID:       ('0Ng',    _QNULL_GUID,         lambda v: v == _QNULL_GUID),
    QBOOL:       ('0b',     _QNULL_BOOL,         lambda v: v == numpy.bool_(False)),
    QBYTE:       ('0x00',   _QNULL1,             lambda v: v == _QNULL1),
    QSHORT:      ('0Nh',    _QNULL2,             lambda v: v == _QNULL2),
    QINT:        ('0N',     _QNULL4,             lambda v: v == _QNULL4),
    QLONG:       ('0Nj',    _QNULL8,             lambda v: v == _QNULL8),
    QFLOAT:      ('0Ne',    _QNAN32,             lambda v: numpy.isnan(v)),
    QDOUBLE:     ('0n',     _QNAN64,             lambda v: numpy.isnan(v)),
    QSTRING:     ('" "',    b' ',                 lambda v: v == b' '),
    QSYMBOL:     ('`',      _QNULL_SYM,          lambda v: v == _QNULL_SYM),
    QMONTH:      ('0Nm',    _QNULL4,             lambda v: v == _QNULL4),
    QDATE:       ('0Nd',    _QNULL4,             lambda v: v == _QNULL4),
    QDATETIME:   ('0Nz',    _QNAN64,             lambda v: numpy.isnan(v)),
    QMINUTE:     ('0Nu',    _QNULL4,             lambda v: v == _QNULL4),
    QSECOND:     ('0Nv',    _QNULL4,             lambda v: v == _QNULL4),
    QTIME:       ('0Nt',    _QNULL4,             lambda v: v == _QNULL4),
    QTIMESPAN:   ('0Nn',    _QNULL8,             lambda v: v == _QNULL8),
    QTIMESTAMP:  ('0Np',    _QNULL8,             lambda v: v == _QNULL8),
    }



def qnull(qtype):
    '''Retrieve null value for requested q type.

    :Parameters:
     - `qtype` (`integer`) - qtype indicator

    :returns: null value for specified q type
    '''
    return QNULLMAP[qtype][1]



def is_null(value, qtype):
    '''Checks whether given value matches null value for a particular q type.

    :Parameters:
     - `qtype` (`integer`) - qtype indicator

    :returns: `boolean` - ``True`` if value is considered null for given type
              ``False`` otherwise
    '''
    return QNULLMAP[qtype][2](value)



class QException(Exception):
    '''Represents a q error.'''
    pass



class QFunction(object):
    '''Represents a q function.'''

    def __init__(self, qtype):
        self.qtype = qtype


    def __str__(self):
        return '%s#%s' % (self.__class__.__name__, self.qtype)



class QLambda(QFunction):

    '''Represents a q lambda expression.

    .. note:: `expression` is trimmed and required to be valid q function 
              (``{..}``) or k function (``k){..}``).

    :Parameters:
     - `expression` (`string`) - lambda expression

    :raises: `ValueError`
    '''
    def __init__(self, expression):
        QFunction.__init__(self, QLAMBDA)

        if not expression:
            raise ValueError('Lambda expression cannot be None or empty')

        expression = expression.strip()

        if not QLambda._EXPRESSION_REGEX.match(expression):
            raise ValueError('Invalid lambda expression: %s' % expression)

        self.expression = expression


    _EXPRESSION_REGEX = re.compile(r'\s*(k\))?\s*\{.*\}')


    def __str__(self):
        return '%s(\'%s\')' % (self.__class__.__name__, self.expression)


    def __eq__(self, other):
        return (isinstance(other, self.__class__) and self.expression == other.expression)



class QProjection(QFunction):

    '''Represents a q projection.

    :Parameters:
     - `parameters` (`list`) - list of parameters for lambda expression
    '''
    def __init__(self, parameters):
        QFunction.__init__(self, QPROJECTION)

        self.parameters = parameters


    def __str__(self):
        parameters_str = []
        for arg in self.parameters:
            from qmcp.qpython.display import format_qython_value
            
            # FIX: Use modern display system first (primary approach)
            try:
                formatted_arg = format_qython_value(arg)
                parameters_str.append(formatted_arg)
                continue  # Skip fallbacks if modern system works
            except Exception:
                pass  # Continue to fallbacks
            
            # FALLBACK 1: Try original legacy approach (may crash)
            try:
                original_format = '%s' % arg
                parameters_str.append(original_format)
                continue  # Skip final fallback if legacy works
            except Exception:
                pass  # Continue to final fallback
            
            # FALLBACK 2: Safe generic representation (never crashes)
            safe_fallback = f"<{type(arg).__name__} object>"
            parameters_str.append(safe_fallback)

        return '%s(%s)' % (self.__class__.__name__, ', '.join(parameters_str))


    def __eq__(self, other):
        return (not self.parameters and not other.parameters) or \
                reduce(lambda v1,v2: v1 or v2, map(lambda v: v in self.parameters, other.parameters))


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



class Mapper(object):
    '''Utility class for creating function execution map via decorators.

    :Parameters:
     - `call_map` (`dictionary`) -  target execution map
    '''
    def __init__(self, call_map):
        self.call_map = call_map


    def __call__(self, *args):
        def wrap(func):
            for arg in args:
                self.call_map[arg] = func
            return func
        return wrap
