import ctypes
import copy
import enum
import logging

from collections.abc import Iterable
from ctypes import (c_char, c_char_p, c_double, c_size_t, c_int64, pointer,
                    POINTER)


from ..tecutil import _tecutil, _tecutil_connector
from ..constant import *
from ..exception import *
from .. import tecutil, version
from ..tecutil import sv


log = logging.getLogger(__name__)


@tecutil.lock(with_recording=False)
def set_style(value, *args, **kwargs):
    if __debug__:
        assert len(args) < 7, ', '.join(args)
        if isinstance(value, int):
            if value < tecutil.minint64 or tecutil.maxuint64 < value:
                raise TecplotOverflowError('Integer outside of valid range.')

        if log.getEffectiveLevel() < logging.INFO:
            msg = 'SetStyle\n'
            msg += '  value: {}\n'.format(value)
            for a in args:
                msg += '  {} {}\n'.format(type(a), a)
            for k, v in kwargs.items():
                msg += '  {} : {} {}\n'.format(k, type(v), v)
            log.debug(msg[:-1])

            _tecutil_connector._style_call_count['SET'][' '.join(args)] += 1

    with tecutil.ArgList() as arglist:

        allocd = []
        try:
            for i, p in enumerate(args):
                arglist[getattr(sv, 'P' + str(i + 1))] = p

            if isinstance(value, enum.Enum):
                arglist[sv.IVALUE] = c_size_t(value.value)
            elif isinstance(value, tecutil.Index):
                arglist[sv.IVALUE] = c_size_t(value + 1)
            elif isinstance(value, (int, bool)):
                arglist[sv.IVALUE] = c_size_t(value)
            elif isinstance(value, float):
                arglist[sv.DVALUE] = value
            elif isinstance(value, str):
                arglist[sv.STRVALUE] = value
            elif hasattr(value, '__iter__'):
                value_list = list(value)
                if not len(value_list) or isinstance(value_list[0], int):
                    value_obj = tecutil.IndexSet(*value_list)
                    allocd.append(value_obj)
                    arglist[sv.IVALUE] = c_size_t(value_obj.value)
                else:
                    value_obj = tecutil.StringList(*value_list)
                    allocd.append(value_obj)
                    arglist[sv.IVALUE] = c_size_t(ctypes.addressof(
                        ctypes.cast(value_obj, POINTER(c_size_t)).contents))
            elif value is None:
                arglist[sv.IVALUE] = c_size_t(0)
            else:
                raise TecplotTypeError

            for k, v in kwargs.items():
                k = k.upper()
                if k == sv.UNIQUEID:
                    v = c_size_t(v)
                elif k in [sv.OBJECTSET]:
                    v = tecutil.IndexSet(*v)
                    allocd.append(v)
                elif k in [sv.OFFSET1, sv.OFFSET2]:
                    v = v + 1
                arglist[k] = v

            if __debug__:
                if log.getEffectiveLevel() < logging.INFO:
                    msg = 'SetStyle\n'
                    for k, v in arglist.items():
                        msg += '  {}: {}\n'.format(k, v)
                    log.debug(msg[:-1])

            try:
                res = _tecutil.StyleSetLowLevelX(arglist)
                if res not in [SetValueReturnCode.Ok,
                               SetValueReturnCode.DuplicateValue]:
                    if __debug__:
                        msg = 'SetStyle\n'
                        for k, v in arglist.items():
                            msg += '  {}: {}\n'.format(k, v)
                        raise TecplotSystemError(str(res) + '\n' + msg)
                    raise TecplotSystemError(res)
            except TecplotLogicError as e:
                if __debug__:
                    msg = 'SetStyle\n'
                    for k, v in arglist.items():
                        msg += '  {}: {}\n'.format(k, v)
                    raise TecplotLogicError(str(e) + '\n' + msg)
                else:
                    raise

        finally:
            for a in allocd:
                a.dealloc()


def get_style(return_type, *args, **kwargs):
    if __debug__:
        assert len(args) < 7

        if log.getEffectiveLevel() < logging.INFO:
            msg = 'GetStyle\n'
            for a in args:
                msg += '  {} {}\n'.format(type(a), a)
            for k, v in kwargs.items():
                msg += '  {} : {} {}\n'.format(k, type(v), v)
            log.debug(msg[:-1])

            _tecutil_connector._style_call_count['GET'][' '.join(args)] += 1

    with tecutil.ArgList() as arglist:

        allocd = []
        try:

            for i, p in enumerate(args):
                arglist[getattr(sv, 'P' + str(i + 1))] = p

            for k, v in kwargs.items():
                k = k.upper()
                if k == sv.UNIQUEID:
                    v = c_size_t(v)
                elif k in [sv.OBJECTSET]:
                    v = tecutil.IndexSet(*v)
                    allocd.append(v)
                elif k in [sv.OFFSET1, sv.OFFSET2]:
                    v = v + 1
                arglist[k] = v

            if (return_type in [int, bool, tecutil.Index, list, set] or
                    issubclass(return_type, enum.Enum)):
                arglist[sv.IVALUE] = pointer(c_size_t())
            elif return_type in [str]:
                arglist[sv.IVALUE] = pointer(pointer(c_char()))
            elif return_type in [float]:
                arglist[sv.DVALUE] = pointer(c_double())
            else:
                raise TecplotTypeError('unknown return_type: {}'.format(return_type))

            if __debug__:
                if log.getEffectiveLevel() < logging.INFO:
                    msg = 'GetStyle\n'
                    for k, v in arglist.items():
                        msg += '  {}: {}\n'.format(k, v)
                    log.debug(msg[:-1])

            try:
                res = _tecutil.StyleGetLowLevelX(arglist)
                if res is not GetValueReturnCode.Ok:
                    raise TecplotSystemError(res)
            except TecplotLogicError as e:
                if __debug__:
                    msg = 'GetStyle\n'
                    for k, v in arglist.items():
                        msg += '  {}: {}\n'.format(k, v)
                    raise TecplotLogicError(str(e) + '\n' + msg)
                else:
                    raise

            if return_type in [str]:
                ivalue = ctypes.cast(arglist[sv.IVALUE], POINTER(c_char_p))
                result = ivalue.contents.value
                if result not in [None, '']:
                    result = result.decode('utf-8')
            elif issubclass(return_type, enum.Enum):
                ival = arglist[sv.IVALUE]
                val = ctypes.cast(ival, POINTER(c_int64)).contents.value
                result = return_type(int(val))
            elif return_type in [tecutil.Index]:
                ival = arglist[sv.IVALUE]
                val = ctypes.cast(ival, POINTER(c_int64)).contents.value
                result = return_type(int(val) - 1)
            elif return_type in [list, set]:
                ptr = ctypes.cast(arglist[sv.IVALUE], POINTER(tecutil.IndexSet))
                iset = ptr.contents
                result = return_type(iset)
                iset.dealloc()
            elif return_type in [int, bool]:
                result = return_type(arglist[sv.IVALUE].contents.value)
            else:  # if return_type in [float]:
                result = return_type(arglist[sv.DVALUE].contents.value)

            if __debug__:
                if log.getEffectiveLevel() < logging.INFO:
                    log.debug('GetStyle result: {}'.format(result))

            return result

        finally:
            for a in allocd:
                a.dealloc()


@tecutil.lock_attributes
class Style(object):
    def __init__(self, *svargs, **kwargs):
        self._sv = list(tecutil.flatten_args(*svargs))
        uniqueid = kwargs.pop('uniqueid', None)
        offset1 = kwargs.pop('offset1', None)
        offset2 = kwargs.pop('offset2', None)
        objectset = kwargs.pop('objectset', None)

        assert not (offset1 and objectset), \
            'offset1 and objectset are mutually exclusive'
        assert not (offset2 and not (offset1 or objectset)), \
            'offset2 requires offset1 or objectset to also be specified'

        self._style_attrs = {}
        if uniqueid is not None:
            self._style_attrs[sv.UNIQUEID] = int(uniqueid)
        if offset1 is not None:
            self._style_attrs[sv.OFFSET1] = tecutil.Index(offset1)
        if offset2 is not None:
            self._style_attrs[sv.OFFSET2] = tecutil.Index(offset2)
        if objectset is not None:
            self._style_attrs[sv.OBJECTSET] = set(objectset)

    @property
    def _kw(self):
        return {k.lower(): v for k, v in self._style_attrs.items()}

    def _get_style(self, rettype, *svargs, **kwargs):
        svargs = self._sv + list(svargs)
        kw = self._style_attrs.copy()
        kw.update(**kwargs)
        if sv.OBJECTSET in kw:
            objectset = kw.pop(sv.OBJECTSET)
            result = []
            for offset1 in sorted(objectset):
                kw[sv.OFFSET1] = tecutil.Index(offset1)
                result.append(get_style(rettype, *svargs, **kw))
            return tuple(result)
        else:
            return get_style(rettype, *svargs, **kw)

    def _set_style(self, value, *svargs, **kwargs):
        svargs = self._sv + list(svargs)
        kw = self._style_attrs.copy()
        kw.update(**kwargs)
        set_style(value, *svargs, **kw)


@tecutil.lock_attributes
class SubStyle(object):
    def __init__(self, parent, *svargs):
        self.parent = parent
        self._svargs = list(tecutil.flatten_args(*svargs))

    def __eq__(self, that):
        return (isinstance(that, type(self))
                and self.parent == that.parent
                and self._svargs == that._svargs)

    def __ne__(self, that):
        return not (self == that)

    def _get_style(self, rettype, *svargs, **kwargs):
        svargs = self._svargs + list(tecutil.flatten_args(*svargs))
        return self.parent._get_style(rettype, *svargs, **kwargs)

    def _set_style(self, value, *svargs, **kwargs):
        svargs = self._svargs + list(tecutil.flatten_args(*svargs))
        self.parent._set_style(value, *svargs, **kwargs)


class StyleConfig(Style):
    """Runtime Configuration Control Base Class."""
    def __init__(self, name=None, *svargs):
        if svargs:
            Style.__init__(self, *svargs)
        self._ns = name.split('.') if name else []

    def __setattr__(self, name, value):
        if not name.startswith('_'):
            if not hasattr(self, '_keys'):
                self._keys = list(filter(lambda x: not x.startswith('_'), dir(self)))
            if name not in self._keys:
                msg = 'No configuration found: '
                raise TecplotAttributeError(msg + '.'.join(self._ns + [name]))
        super().__setattr__(name, value)

    @property
    def _opts(self):
        if not hasattr(self, '_attrs'):
            configs, props = [], []
            for key in filter(lambda x: not x.startswith('_'), dir(self)):
                value = getattr(self, key)
                if isinstance(value, StyleConfig):
                    configs.append((key, value))
                else:
                    props.append(key)
            self._attrs = (sorted(configs), sorted(props))
        return self._attrs

    def __str__(self):
        configs, props = self._opts
        lines = []
        for keyname in props:
            key = '.'.join(self._ns + [keyname])
            lines.append('{} = {}'.format(key, getattr(self, keyname)))
        for _, value in configs:
            lines.append(str(value))
        return '\n'.join(lines)


def style_property(value_type, svarg, value_cast=None):
    def getter(self):
        return self._get_style(value_type, svarg)

    if value_cast:
        def setter(self, val):
            val = value_cast(val)
            if val is not None:
                self._set_style(val, svarg)
    else:
        def setter(self, val):
            if val is not None:
                self._set_style(value_type(val), svarg)

    return property(getter, setter)


class NamedTupleStyle(SubStyle, Iterable):
    def __len__(self):
        return len(self._keys)

    def __getitem__(self, index):
        if isinstance(index, slice):
            rng = range(index.start or 0,
                        index.stop or len(self),
                        index.step or 1)
            return tuple([self[x] for x in rng])
        else:
            return getattr(self, self._keys[index])

    def __setitem__(self, index, value):
        if isinstance(index, slice):
            for i in range(index.start or 0,
                           index.stop or len(self),
                           index.step or 1):
                self[i] = value[i] if i < len(value) else None
        else:
            setattr(self, self._keys[index], value)

    def __iter__(self):
        self._current_index = -1
        return self

    def __next__(self):
        self._current_index += 1
        if self._current_index < len(self):
            return self.__getitem__(self._current_index)
        else:
            del self._current_index
            raise StopIteration()

    def __str__(self):
        return '({})'.format(', '.join(str(x) for x in self))

    def __eq__(self, other):
        return (len(self) == len(other)
                and all(a == b for a, b in zip(self, other)))


class NamedVectorStyle(NamedTupleStyle):
    def __neg__(self):
        return tuple([-v for v in self])

    def __add__(self, other):
        return tuple([a - b for a, b in zip(self, other)])

    def __sub__(self, other):
        return tuple([a - b for a, b in zip(self, other)])

    def __iadd__(self, other):
        for k, v in zip(self._keys, other):
            setattr(self, k, getattr(self, k) + v)
        return self

    def __isub__(self, other):
        for k, v in zip(self._keys, other):
            setattr(self, k, getattr(self, k) - v)
        return self


class IndependentVariableLimits(NamedTupleStyle):
    _keys = ('min', 'max')
    min = style_property(float, sv.INDVARMIN)
    max = style_property(float, sv.INDVARMAX)


class IJK(NamedVectorStyle):
    _keys = ('i', 'j', 'k')
    i = style_property(int, sv.I)
    j = style_property(int, sv.J)
    k = style_property(int, sv.K)


class IJKMaxFract(NamedTupleStyle):
    _keys = ('i', 'j', 'k')
    i = style_property(float, sv.IMAXFRACT)
    j = style_property(float, sv.JMAXFRACT)
    k = style_property(float, sv.KMAXFRACT)


class IJKMinFract(NamedTupleStyle):
    _keys = ('i', 'j', 'k')
    i = style_property(float, sv.IMINFRACT)
    j = style_property(float, sv.JMINFRACT)
    k = style_property(float, sv.KMINFRACT)


class Limits(NamedTupleStyle):
    _keys = ('min', 'max')
    min = style_property(float, sv.MIN)
    max = style_property(float, sv.MAX)


class IndexIJK(NamedVectorStyle):
    _keys = ('i', 'j', 'k')
    i = style_property(tecutil.Index, sv.I)
    j = style_property(tecutil.Index, sv.J)
    k = style_property(tecutil.Index, sv.K)


class IndexRange(NamedTupleStyle):
    """Index range specification along some axis.

    This is similar to Python's :class:`slice` object except that ``max`` is
    included in the evaluated indexes. Here are some things to note:

        * All indices start with 0 and go to some maximum index ``m``.
        * Negative values represent the indexes starting with the maximum at -1
          and continuing back to the beginning of the range.
        * A step of `None`, 0 and 1 are all equivalent and mean that no elements
          are skipped.
        * A negative step indicates a skip less than the maximum.


    .. versionchanged:: 1.1
        **(Bug fix)** ``IndexRange`` max value of zero is now interpreted as
        the first index in the range instead of the last index. Prior to
        version 1.1, the ``max`` parameter interpreted zero to be the end of
        the range instead of the first element. This meant that an
        ``IndexRange`` of ``(0, 0, 1)`` would represent the whole range instead
        of just the first item. The standard way to represent the entire index
    """
    _keys = ('min', 'max', 'step')
    min = style_property(tecutil.Index, sv.MIN,
                         lambda x: tecutil.Index(x or 0))
    max = style_property(tecutil.Index, sv.MAX,
                         lambda x: tecutil.Index(-1 if x is None else x))
    step = style_property(int, sv.SKIP, lambda x: int(x or 1))


class RectPosition(NamedTupleStyle):
    _keys = ('x1', 'y1', 'x2', 'y2')
    x1 = style_property(float, sv.X1)
    y1 = style_property(float, sv.Y1)
    x2 = style_property(float, sv.X2)
    y2 = style_property(float, sv.Y2)


class RectSize(NamedTupleStyle):
    _keys = ('width', 'height')
    width = style_property(float, sv.WIDTH)
    height = style_property(float, sv.HEIGHT)


class SplineDerivativeAtEnds(NamedTupleStyle):
    _keys = ('start', 'end')
    start = style_property(float, sv.SPLINEDERIVATIVEATSTART)
    end = style_property(float, sv.SPLINEDERIVATIVEATEND)


class XY(NamedVectorStyle):
    _keys = ('x', 'y')
    x = style_property(float, sv.X)
    y = style_property(float, sv.Y)


class XYZ(NamedVectorStyle):
    _keys = ('x', 'y', 'z')
    x = style_property(float, sv.X)
    y = style_property(float, sv.Y)
    z = style_property(float, sv.Z)
