"""Glue for the Nani library."""

__all__ = ['Bool', 'Object', 'Number', 'String', 'Unicode', 'Array',
           'Structure', 'Bytes', 'Str', 'Field', 'READ_ONLY']

import collections
import sys

import nani

import hienoi._numeric


_PY2 = sys.version_info[0] == 2


if _PY2:
    _BUILTIN_MODULE = '__builtin__'
    _BuiltinString = str
    _BuiltinUnicode = unicode
    _range = xrange
else:
    _BUILTIN_MODULE = 'builtins'
    _BuiltinString = bytes
    _BuiltinUnicode = str
    _range = range


assert nani.Bool._fields == ('default', 'view')
assert nani.Object._fields == ('default', 'view')
assert nani.Number._fields == ('type', 'default', 'view')
assert nani.String._fields == ('length', 'default', 'view')
assert nani.Unicode._fields == ('length', 'default', 'view')
assert nani.Array._fields == ('element_type', 'shape', 'name', 'view')
assert nani.Structure._fields == ('fields', 'name', 'view')
assert nani.Field._fields == ('name', 'type', 'read_only')


class Bool(nani.Bool):
    """Type corresponding to a boolean.

    Attributes
    ----------
    default : bool
        Default value.
    view : type or None
        If ``None``, the owning array returns a direct reference to this
        boolean value, otherwise it is expected to be a class object wrapping
        it and accepting 2 parameters: ``data``, the NumPy array owning the
        boolean value, and ``index``, its position in the array.
    """

    __slots__ = ()


Bool.__new__.__defaults__ = (False, None)


class Object(nani.Object):
    """Type corresponding to a Python object.

    Attributes
    ----------
    default : object
        Default value.
    view : type or None
        If ``None``, the owning array returns a direct reference to this Python
        object, otherwise it is expected to be a class object wrapping it and
        accepting 2 parameters: ``data``, the NumPy array owning the Python
        object, and ``index``, its position in the array.
    """

    __slots__ = ()


Object.__new__.__defaults__ = (None, None)


class Number(nani.Number):
    """Type corresponding to a number.

    Attributes
    ----------
    type : hienoi numerical type
        Type of the number. Either one from :class:`Int8`, :class:`UInt8`,
        :class:`Int16`, :class:`UInt16`, :class:`Int32`, :class:`UInt32`,
        :class:`Float32`, or :class:`Float64`.
    default : numbers.Number or numpy.number
        Default value.
    view : type or None
        If ``None``, the owning array returns a direct reference to this
        numeric value, otherwise it is expected to be a class object wrapping
        it and accepting 2 parameters: ``data``, the NumPy array owning the
        numeric value, and ``index``, its position in the array.
    """

    __slots__ = ()


Number.__new__.__defaults__ = (hienoi._numeric.Float32, 0, None)


class String(nani.String):
    """Type corresponding to a string.

    Attributes
    ----------
    length : int
        Number of characters.
    default : str on PY2 or bytes on PY3
        Default value.
    view : type or None
        If ``None``, the owning array returns a direct reference to this string
        value, otherwise it is expected to be a class object wrapping it and
        accepting 2 parameters: ``data``, the NumPy array owning the string
        value, and ``index``, its position in the array.
    """

    __slots__ = ()


String.__new__.__defaults__ = (_BuiltinString(), None)


class Unicode(nani.Unicode):
    """Type corresponding to a unicode string.

    Attributes
    ----------
    length : int
        Number of characters.
    default : unicode on PY2 or str on PY3
        Default value.
    view : type or None
        If ``None``, the owning array returns a direct reference to this
        unicode value, otherwise it is expected to be a class object wrapping
        it and accepting 2 parameters: ``data``, the NumPy array owning the
        unicode value, and ``index``, its position in the array.
    """

    __slots__ = ()


Unicode.__new__.__defaults__ = (_BuiltinUnicode(), None)


class Array(nani.Array):
    """Type corresponding to an array.

    Attributes
    ----------
    element_type : hienoi type
        Type of each element.
    shape : int or tuple of int
        Shape of the array. Passing an int defines a 1D array.
    name : str or None
        Name for the view type if `view` is ``None``.
    view : type or None
        If ``None``, a view for this array is dynamically generated by Nani,
        otherwise it is expected to be a class object wrapping it and accepting
        1 parameter: ``data``, the corresponding NumPy array.
    """

    __slots__ = ()


Array.__new__.__defaults__ = (None, None)


class Structure(nani.Structure):
    """Type corresponding to a structure.

    Attributes
    ----------
    fields : tuple of hienoi.Field or compatible tuple
        Fields defining the structure.
    name : str or None
        Name for the view type if `view` is ``None``.
    view : type or None
        If ``None``, a view for this structured array is dynamically generated
        by Nani, otherwise it is expected to be a class object wrapping it and
        accepting 1 parameter: ``data``, the corresponding NumPy structured
        array.
    """

    __slots__ = ()


Structure.__new__.__defaults__ = (None, None)


# Aliases.
Bytes = String
if _PY2:
    Str = String
else:
    Str = Unicode


_ALL_TYPES = (Bool, Object, Number, String, Unicode, Array, Structure)


class Field(nani.Field):
    """Describe a field of a structured array.

    Attributes
    ----------
    name : str
        Name of the field.
    type : nani data type
        Type of the field.
    read_only : bool
        ``True`` to not define a setter property in the structured array view
        if it is set to be dynamically generated by Nani.
    """

    __slots__ = ()


Field.__new__.__defaults__ = (False,)


READ_ONLY = nani.READ_ONLY


_FIELD_TYPE = Field._fields.index('type')
_FIELD_ATTR_COUNT = len(Field._fields)


Nani = collections.namedtuple(
    'Nani', (
        'dtype',
        'default',
        'view',
        'element_view'
    ))


class PickableNaniStructure(object):
    """Nani structure that can be pickled."""

    def __init__(self, fields, name):
        self._fields = fields
        self._name = name
        self._build()

    def __getstate__(self):
        return {'_fields': self._fields, '_name': self._name}

    def __setstate__(self, state):
        self.__dict__.update(state)
        self._build()

    def _build(self):
        self._obj = resolve(Structure(fields=self._fields, name=self._name))
        setattr(self._obj.view, 'data', property(fget=lambda self: self._data))

    @property
    def dtype(self):
        return self._obj.dtype

    @property
    def default(self):
        return self._obj.default

    @property
    def view(self):
        return self._obj.view

    @property
    def element_view(self):
        return self._obj.element_view


def resolve(data_type, name=None):
    """Retrieve the properties of a given data type."""
    _validate(data_type)
    data_type = _consolidate(data_type)
    nani.validate(data_type)
    properties = nani.resolve(data_type, name=name)
    return Nani(
        dtype=properties.dtype,
        default=properties.default,
        view=properties.view,
        element_view=nani.get_element_view(properties.view))


def _validate(data_type):
    """Check if the data type is well-formed."""
    if not isinstance(data_type, _ALL_TYPES):
        raise TypeError(
            "The data type '%s' isn't supported. It is expected to be an "
            "instance object of type %s."
            % (type(data_type).__name__, _join_types(_ALL_TYPES, "or ")))

    if isinstance(data_type, Array):
        _validate(data_type.element_type)
    elif isinstance(data_type, Structure):
        for field in data_type.fields:
            _validate(field[_FIELD_TYPE])


def _consolidate(data_type):
    """Enforce the structure of the data type.

    Specifically, convert any hienoi type into its NumPy equivalent.
    """
    def get_field_item(field, i):
        if i < len(field):
            return field[i]
        else:
            i -= _FIELD_ATTR_COUNT
            return Field.__new__.__defaults__[i]

    if isinstance(data_type, Number):
        out = data_type._replace(type=hienoi._numeric.to_numpy(data_type.type))
    elif isinstance(data_type, Array):
        out = data_type._replace(
            element_type=_consolidate(data_type.element_type))
    elif isinstance(data_type, Structure):
        fields = tuple(
            Field(*(_consolidate(get_field_item(field, i))
                    if i == _FIELD_TYPE else get_field_item(field, i)
                    for i in _range(_FIELD_ATTR_COUNT)))
            for field in data_type.fields)
        out = data_type._replace(fields=fields)
    else:
        out = data_type

    return out


def _format_type(cls):
    """Format a type name for printing."""
    if cls.__module__ == _BUILTIN_MODULE:
        return cls.__name__
    else:
        return '%s.%s' % (cls.__module__, cls.__name__)


def _format_element(element, count, idx, last_separator):
    """Format an element from a sequence.

    This only prepends a separator for the last element and wraps each element
    with single quotes.
    """
    return ("%s'%s'" % (last_separator, element)
            if count > 1 and idx == count - 1
            else "'%s'" % (element,))


def _join_sequence(seq, last_separator=''):
    """Join a sequence into a string."""
    count = len(seq)
    return ', '.join(_format_element(element, count, i, last_separator)
                     for i, element in enumerate(seq))


def _join_types(seq, last_separator=''):
    """Join class object names into a string."""
    class_names = [_format_type(cls) for cls in seq]
    return _join_sequence(class_names, last_separator)
