"""numerictypes: Define the numeric type objects

This module is designed so 'from numerictypes import *' is safe.
Exported symbols include:

  Dictionary with all registered number types (including aliases):
    typeDict

  Numeric type objects:
    Bool
    Int8 Int16 Int32 Int64
    UInt8 UInt16 UInt32 UInt64
    Float32 Double64
    Complex32 Complex64

  Numeric type classes:
    NumericType
      BooleanType
      SignedType
      UnsignedType
      IntegralType
        SignedIntegralType
        UnsignedIntegralType
      FloatingType
      ComplexType

$Id: numerictypes.py,v 1.35.2.1 2004/11/16 23:02:37 jaytmiller Exp $
"""

MAX_ALIGN = 8
MAX_INT_SIZE = 8

from typeconv import typeConverters as _typeConverters
import types as _types
import numinclude
import copy as _copy

#XXX RLW thoughts:
#XXX
#XXX Another useful attribute might be `pythontype', which would tell what
#XXX the corresponding native Python type is for the numeric type.

typeDict = {}      # Contains all registered numeric types with aliases

def IsType(rep):
    """Determines whether the given object or string, 'rep', represents
    a numarray type."""
    return isinstance(rep, NumericType) or typeDict.has_key(rep)

def _register(name, type, force=0):
    """Register the type object.  Raise an exception if it is already registered
    unless force is true.
    """
    if typeDict.has_key(name) and not force:
        raise ValueError("Type %s has already been registered" % name)
    typeDict[name] = type
    return type


class NumericType:

    """Numeric type class

    Used both as a type identification and the repository of
    characteristics and conversion functions.
    """

    def __init__(self, name, bytes, default):
        self.name = name
        self.bytes = bytes
        self.default = default
        # The following attribute is a hook to provide new add on types
        # a way of converting from standard types to their type. If the
        # the usual lookup for a c function that does the conversions is
        # not found, then an attempt will be made to see if this attribute
        # is set, and if it is, call it as a conversion function. Complex
        # numarray are handled this way since there are no C functions that
        # do their conversions. The attribute is set in the module that
        # defines the NumArray subclass.
        self.fromtype = None
        try:
            self._conv = _typeConverters[name]
        except KeyError:
            self._conv = None
        _register(self.name, self)

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name

    def __hash__(self):
        """Allow type & name to be used interchangeably as dict key"""
        return hash(self.name)

    def __cmp__(self, other):
        if isinstance(other, _types.StringType):
            other = typeDict.get(other)
        if other is None:
            return 1  # 1 ==> False
        elif hasattr(self, "name") and hasattr(other, "name"):
            return (genericTypeRank.index(self.name) - 
                    genericTypeRank.index(other.name))
        else:
            return 0

class BooleanType(NumericType):
    """Boolean type (distinct so we can use mask numarray as subscripts)"""
    def __init__(self):
        NumericType.__init__(self, "Bool", 1, 1)

class SignedType(NumericType):
    """Marker class used for signed type check"""
    pass

class UnsignedType(NumericType):
    """Marker class used for unsigned type check"""
    pass

class IntegralType(NumericType):
    """Integral numeric type"""
    def __init__(self, bytes, root="Int"):
        assert bytes in [1, 2, 4, 8]
        NumericType.__init__(self, "%s%d" % (root, 8*bytes), bytes, 0)

class SignedIntegralType(IntegralType, SignedType):
    """Signed integer type"""
    pass

class UnsignedIntegralType(IntegralType, UnsignedType):
    """Unsigned integer type"""
    def __init__(self, bytes, root="UInt"):
        IntegralType.__init__(self, bytes, root)

class FloatingType(NumericType):
    """Floating-point type"""
    def __init__(self, bytes, root="Float"):
        assert bytes in [4, 8]
        NumericType.__init__(self, "%s%d" % (root, 8*bytes), bytes, 0.0)

class ComplexType(NumericType):
    """Complex type (XXX may want to make complex a subclass of Numeric array)"""
    def __init__(self, bytes, root="Complex"):
        assert bytes in [8, 16]
        NumericType.__init__(self, "%s%d" % (root, 4*bytes), bytes, complex(0.0))

class AnyType(NumericType):
    def __init__(self):
        self.name = "Any"
        self.bytes = None
        self.default = None
        self._conv = None
        _register("Any", self)

class ObjectType(NumericType):
    def __init__(self):
        self.name = "Object"
        self.bytes = None
        self.default = None
        self._conv = None
        _register("Object", self)

# C-API Type Any

Any = AnyType()
Object = ObjectType()
    
# Numeric Types:
Bool  = BooleanType()
Int8  = SignedIntegralType(1)
Int16 = SignedIntegralType(2)
Int32 = SignedIntegralType(4)
Int64 = SignedIntegralType(8)
    
Float32   = FloatingType(4)
Float64  = FloatingType(8)

UInt8  = UnsignedIntegralType(1)
UInt16 = UnsignedIntegralType(2)
UInt32 = UnsignedIntegralType(4)
UInt64 = UnsignedIntegralType(8)

Complex32  = ComplexType(8)
Complex64 = ComplexType(16)

# Aliases

Byte = _register("Byte",   Int8)
Short = _register("Short",  Int16)
Int = _register("Int",    Int32)
if numinclude.LP64:
    Long = _register("Long", Int64)
    if numinclude.hasUInt64:
        _register("ULong",  UInt64)
    # MaybeLong = _register("MaybeLong", Int64)  # XXXXXX
else:
    Long = _register("Long", Int32)
    _register("ULong",  UInt32)
    # MaybeLong = _register("MaybeLong", Int32)  # XXXXXX

MaybeLong = _register("MaybeLong", Int32) # XXXXX until arraybase.h enabled.

_register("UByte",  UInt8)
_register("UShort", UInt16)
_register("UInt",   UInt32)
Float = _register("Float",  Float64)
Complex = _register("Complex",  Complex64)

# short forms

_register("b1", Bool)
_register("u1", UInt8)
_register("u2", UInt16)
_register("u4", UInt32)
_register("i1", Int8)
_register("i2", Int16)
_register("i4", Int32)

_register("i8", Int64)
if numinclude.hasUInt64:
    _register("u8", UInt64)
    
_register("f4", Float32)
_register("f8", Float64)
_register("c8", Complex32)
_register("c16", Complex64)

# NumPy forms

_register("1", Int8)
# _register("c", Int8)
_register("b", UInt8)
_register("s", Int16)
_register("w", UInt16)
_register("i", Int32)
_register("N", Int64)
_register("u", UInt32)
_register("U", UInt64)
if numinclude.LP64:
    _register("l", Int64)
else:
    _register("l", Int32)
_register("d", Float64)
_register("f", Float32)
_register("D", Complex64)
_register("F", Complex32)

# The rest is used by numeric modules to determine conversions

# Ranking of types from lowest to highest (sorta)
if not numinclude.hasUInt64:
    genericTypeRank = ['Bool','Int8','UInt8','Int16','UInt16',
                       'Int32', 'UInt32', 'Int64',
                       'Float32','Float64', 'Complex32', 'Complex64',  'Object']
else:
    genericTypeRank = ['Bool','Int8','UInt8','Int16','UInt16',
                       'Int32', 'UInt32', 'Int64', 'UInt64',
                       'Float32','Float64', 'Complex32', 'Complex64', 'Object']
    
pythonTypeRank = [_types.IntType, _types.LongType, _types.FloatType,
                  _types.ComplexType]

# The next line is not platform independent XXX Needs to be generalized
if not numinclude.LP64:
    pythonTypeMap  = {_types.IntType:("Int32","int"),
                      _types.LongType:("Int64","int"),
                      _types.FloatType:("Float64","float"),
                      _types.ComplexType:("Complex64","complex")}

    scalarTypeMap = {_types.IntType:"Int32",
                     _types.LongType:"Int64",
                     _types.FloatType:"Float64",
                     _types.ComplexType:"Complex64"}
else:
    pythonTypeMap  = {_types.IntType:("Int64","int"),
                      _types.LongType:("Int64","int"),
                      _types.FloatType:("Float64","float"),
                      _types.ComplexType:("Complex64","complex")}

    scalarTypeMap = {_types.IntType:"Int64",
                     _types.LongType:"Int64",
                     _types.FloatType:"Float64",
                     _types.ComplexType:"Complex64"}

# Generate coercion matrix

def _initGenericCoercions():
    global genericCoercions
    genericCoercions = {}
    # vector with ...
    for ntype1 in genericTypeRank:
        nt1 = typeDict[ntype1]
        rank1 = genericTypeRank.index(ntype1)
        ntypesize1, inttype1, signedtype1 = nt1.bytes, \
                    isinstance(nt1, IntegralType), isinstance(nt1, SignedIntegralType)
        for ntype2 in genericTypeRank:
            # vector
            nt2 = typeDict[ntype2]
            ntypesize2, inttype2, signedtype2 = nt2.bytes, \
                    isinstance(nt2, IntegralType), isinstance(nt2, SignedIntegralType)
            rank2 = genericTypeRank.index(ntype2)
            if (signedtype1 != signedtype2) and inttype1 and inttype2:
                # mixing of signed and unsigned ints is a special case
                # If unsigned same size or larger, final size needs to be bigger
                #   if possible
                if signedtype1:
                    if ntypesize2 >= ntypesize1:
                        size = min(2*ntypesize2, MAX_INT_SIZE)
                    else:
                        size = ntypesize1
                else:
                    if ntypesize1 >= ntypesize2:
                        size = min(2*ntypesize1, MAX_INT_SIZE)
                    else:
                        size = ntypesize2
                outtype = "Int"+str(8*size)                
            else:
                if rank1 >= rank2:
                    outtype = ntype1
                else:
                    outtype = ntype2
            genericCoercions[(ntype1, ntype2)] = outtype
        for ntype2 in pythonTypeRank:
            # scalar
            mapto, kind = pythonTypeMap[ntype2]
            if ((inttype1 and kind=="int") or (not inttype1 and kind=="float")):
                # both are of the same "kind" thus vector type dominates
                outtype = ntype1
            else:
                rank2 = genericTypeRank.index(mapto)
                if rank1 >= rank2:
                    outtype = ntype1
                else:
                    outtype = mapto
            genericCoercions[(ntype1, ntype2)] = outtype
            genericCoercions[(ntype2, ntype1)] = outtype
    # scalar-scalar
    for ntype1 in pythonTypeRank:
        maptype1 = scalarTypeMap[ntype1]
        for ntype2 in pythonTypeRank:
            maptype2 = scalarTypeMap[ntype2]
            genericCoercions[(ntype1, ntype2)] = genericCoercions[(maptype1, maptype2)]
            
    # Special cases more easily dealt with outside of the loop
    genericCoercions[("Complex32", "Float64")] = "Complex64"
    genericCoercions[("Float64", "Complex32")] = "Complex64"
    genericCoercions[("Complex32", "Int64")] = "Complex64"
    genericCoercions[("Int64", "Complex32")] = "Complex64"
    genericCoercions[("Complex32", "UInt64")] = "Complex64"
    genericCoercions[("UInt64", "Complex32")] = "Complex64"

    genericCoercions[("Int64","Float32")] = "Float64"
    genericCoercions[("Float32", "Int64")] = "Float64"
    genericCoercions[("UInt64","Float32")] = "Float64"
    genericCoercions[("Float32", "UInt64")] = "Float64"

    genericCoercions[(float, "Bool")] = "Float64"
    genericCoercions[("Bool", float)] = "Float64"

_initGenericCoercions()

# If complex is subclassed, the following may not be necessary
genericPromotionExclusions = {
    'Bool': (),
    'Int8': (),
    'Int16': (),
    'Int32': (),
    'UInt8': (),
    'UInt16': (),
    'UInt32': (),
    'Int64' : (genericTypeRank.index('Float32'),),
    'UInt64' : (genericTypeRank.index('Float32'),),
    'Float32': (),
    'Float64': (genericTypeRank.index('Complex32'),),
    'Complex32':(),
    'Complex64':()
} # e.g., don't allow promotion from Float64 to Complex32 or Int64 to Float32

# Numeric typecodes
typecodes = {'Integer': '1silN',
             'UnsignedInteger': 'bwuU',
             'Float': 'fd',
             'Character': 'c',
             'Complex': 'FD' }

typecode = {
    Bool : "1",  
    Int8 : "1",
    UInt8 : "b",
    Int16 : "s",
    UInt16 : "w",
    Int32 : "i",
    UInt32 : "u",
    Int64 : "N",
    UInt64 : "U",
    Long : "l",
    Float32: "f",
    Float64: "d",
    Complex32: "F",
    Complex64: "D",
    Object: "O"
    }


if numinclude.hasUInt64:
    _MaximumType = {
        Bool : Bool,
        
        Int8  : Int64,
        Int16 : Int64,
        Int32 : Int64,
        Int64 : Int64,
        
        UInt8  : UInt64,
        UInt16 : UInt64,
        UInt32 : UInt64,
        UInt8  : UInt64,
        
        Float32 : Float64,
        Float64 : Float64,
        
        Complex32 : Complex64,
        Complex64 : Complex64    
        }
else:
        _MaximumType = {
        Bool : Bool,
        
        Int8  : Int64,
        Int16 : Int64,
        Int32 : Int64,
        Int64 : Int64,
        
        UInt8  : Int64,
        UInt16 : Int64,
        UInt32 : Int64,
        UInt8  : Int64,
       
        Float32 : Float64,
        Float64 : Float64,
        
        Complex32 : Complex64,
        Complex64 : Complex64    
        }

def MaximumType(t):
    """returns the type of highest precision of the same general kind as 't'"""
    return _MaximumType[t]


def getType(type):
    """Return the numeric type object for type

    type may be the name of a type object or the actual object
    """
    if isinstance(type, NumericType):
        return type
    try:
        return typeDict[type]
    except KeyError:
        raise TypeError("Not a numeric type")

