#
# uchroma - Copyright (C) 2017 Steve Kondik
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, version 3.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#

# pylint: disable=invalid-name, redefined-variable-type

import enum

from collections import OrderedDict
from typing import NamedTuple

from gi.repository.GLib import Variant
from traitlets import HasTraits, TraitType, Undefined, UseEnum

from frozendict import frozendict
from grapefruit import Color

import numpy as np

from uchroma.log import Log
from uchroma.traits import class_traits_as_dict, ColorTrait, \
        ColorSchemeTrait, trait_as_dict
from uchroma.util import camel_to_snake, snake_to_camel


ArgSpec = NamedTuple('ArgSpec', [('direction', str), ('name', str), ('type', str)])

logger = Log.get('uchroma.util')

def _check_variance(items: list):
    if len(items) == 0:
        return True

    if len(items) == 1:
        return False

    first_sig = dbus_prepare(items[0])[1]

    return not all(dbus_prepare(x)[1] == first_sig for x in items)


def dbus_prepare(obj, variant: bool=False, camel_keys: bool=False) -> tuple:
    """
    Recursively walks obj and builds a D-Bus signature
    by inspecting types. Variant types are created as
    necessary, and the returned obj may have changed.

    :param obj: An arbitrary primitive or container type
    :param variant: Force wrapping contained objects with variants
    :param camel_keys: Convert dict keys to CamelCase
    """
    sig = ''
    use_variant = variant

    try:
        if isinstance(obj, Variant):
            sig = 'v'

        elif isinstance(obj, bool):
            sig = 'b'

        elif isinstance(obj, str):
            sig = 's'

        elif isinstance(obj, int):
            if obj < pow(2, 16):
                sig = 'n'
            elif obj < pow(2, 32):
                sig = 'i'
            else:
                sig = 'x'

        elif isinstance(obj, float):
            sig = 'd'

        elif isinstance(obj, Color):
            sig = 's'
            obj = obj.html

        elif isinstance(obj, TraitType):
            obj, sig = dbus_prepare(trait_as_dict(obj), variant=True)

        elif isinstance(obj, HasTraits):
            obj, sig = dbus_prepare(class_traits_as_dict(obj), variant=True)

        elif hasattr(obj, '_asdict') and hasattr(obj, '_field_types'):
            # typing.NamedTuple
            obj, sig = dbus_prepare(obj._asdict(), variant=True)

        elif isinstance(obj, type) and issubclass(obj, enum.Enum):
            # top level enum, tuple of string keys
            obj = tuple(obj.__members__.keys())
            sig = '(%s)' % ('s' * len(obj))

        elif isinstance(obj, enum.Enum):
            obj = obj.name
            sig = 's'

        elif isinstance(obj, np.ndarray):
            dtype = obj.dtype.kind
            if dtype == 'f':
                dtype = 'd'
            sig = 'a' * obj.ndim + dtype
            obj = obj.tolist()

        elif isinstance(obj, tuple):
            tmp = []
            sig = '('

            for item in obj:
                if item is None and use_variant:
                    continue
                # struct of all items
                r_obj, r_sig = dbus_prepare(item)
                if r_obj is None:
                    continue
                sig += r_sig
                tmp.append(r_obj)
            if len(tmp) > 0:
                sig += ')'
                obj = tuple(tmp)
            else:
                sig = ''
                obj = None

        elif isinstance(obj, list):
            tmp = []
            sig = 'a'
            is_variant = use_variant or _check_variance(obj)

            for item in obj:
                if item is None and is_variant:
                    continue
                r_obj, r_sig = dbus_prepare(item, variant=is_variant)
                if r_obj is None:
                    continue

                tmp.append(r_obj)

            if is_variant:
                sig += 'v'
            else:
                sig += dbus_prepare(tmp[0])[1]

            obj = tmp

        elif isinstance(obj, (dict, frozendict)):
            if isinstance(obj, frozendict):
                tmp = {}
            else:
                tmp = obj.__class__()
            sig = 'a{s'
            vals = [x for x in obj.values() if x is not None]
            is_variant = use_variant or _check_variance(vals)

            for k, v in obj.items():
                if v is None:
                    continue
                r_obj, r_sig = dbus_prepare(v)
                if r_obj is None:
                    continue
                if camel_keys:
                    k = snake_to_camel(k)
                if is_variant:
                    tmp[k] = Variant(r_sig, r_obj)
                else:
                    tmp[k] = r_obj

            if is_variant:
                sig += 'v'
            else:
                sig += dbus_prepare(vals[0])[1]

            obj = tmp
            sig += '}'

        elif isinstance(obj, type):
            obj = obj.__name__
            sig = 's'

    except Exception as err:
        logger.exception('obj: %s  sig: %s variant: %s', obj, sig, variant, exc_info=err)
        raise

    return obj, sig


class DescriptorBuilder(object):
    """
    Helper class for creating D-BUS XML descriptors

    While pydbus allows inline specification of the descriptor,
    frequently the descriptor needs to be dynamic or based on class
    introspection. This builder lets us create it at runtime
    with a simple interface. Additionally, we inspect traitlets
    from the target object and generate properties to match.

    The descriptor needs to be placed in the 'dbus' attribute of the
    type before registering the object on the bus. Example:

      api.__class__.dbus = builder.build()
      bus.register_object(path, api, None)
    """
    def __init__(self, obj, interface_name, exclude=None):
        self._interface_name = interface_name
        self._obj = obj
        self._ro_props = OrderedDict()
        self._rw_props = OrderedDict()
        self._methods = []
        self._signals = []
        self._exclude = exclude

        if isinstance(obj, HasTraits):
            self._parse_traits()


    def add_property(self, name: str, signature: str, writable: bool=False):
        if writable:
            self._rw_props[name] = signature
        else:
            self._ro_props[name] = signature
        return self


    def add_method(self, method, *argspecs):
        opts = {}
        opts['name'] = method
        if argspecs is not None and len(argspecs) > 0:
            opts['args'] = argspecs

        self._methods.append(opts)
        return self


    def add_signal(self, signal, *argspecs):
        opts = {}
        opts['name'] = signal
        if argspecs is not None and len(argspecs) > 0:
            opts['args'] = argspecs

        self._signals.append(opts)
        return self


    def _parse_traits(self):
        for name, trait in self._obj.traits().items():
            if self._exclude is not None and name in self._exclude:
                continue

            sig = None
            if hasattr(self._obj, name):
                sig = dbus_prepare(getattr(self._obj, name))[1]

            write_once = False
            if hasattr(trait, 'write_once'):
                write_once = trait.write_once

            self.add_property(snake_to_camel(name), sig, not (trait.read_only or write_once))


    def build(self) -> str:
        val = "<node>\n  <interface name='%s'>\n" % self._interface_name

        for name, sig in self._ro_props.items():
            val += "    <property name='%s' type='%s' access='read' />\n" % \
                (snake_to_camel(name), sig)

        for name, sig in self._rw_props.items():
            val += "    <property name='%s' type='%s' access='readwrite'>\n" % \
                (snake_to_camel(name), sig)
            val += "      <annotation name='org.freedesktop.DBus.Property.EmitsChangedSignal' value='true' />\n"
            val += "    </property>\n"

        for method in self._methods:
            name = snake_to_camel(method['name'])
            if not 'args' in method:
                val += "    <method name='%s' />\n" % name
            else:
                val += "    <method name='%s'>\n" % name
                for argspec in method['args']:
                    val += "      <arg direction='%s' type='%s' name='%s' />\n" % \
                        (argspec.direction, argspec.type, argspec.name)
                val += "    </method>\n"

        for signal in self._signals:
            name = snake_to_camel(signal['name'])
            if not 'args' in signal:
                val += "    <signal name='%s' />\n" % name
            else:
                val += "    <signal name='%s'>\n" % name
                for argspec in signal['args']:
                    val += "      <arg direction='%s' type='%s' name='%s' />\n" % \
                        (argspec.direction, argspec.type, argspec.name)
                val += "    </signal>\n"


        val += "  </interface>\n</node>"

        return val


class TraitsPropertiesMixin(object):
    def __init__(self, *args, **kwargs):
        super(TraitsPropertiesMixin, self).__init__(*args, **kwargs)


    def __getattribute__(self, name):
        # Intercept everything and delegate to the device class by converting
        # names between the D-Bus conventions to Python conventions.
        prop_name = camel_to_snake(name)
        if prop_name != name and self._delegate.has_trait(prop_name):
            value = getattr(self._delegate, prop_name)
            trait = self._delegate.traits()[prop_name]
            if isinstance(trait, UseEnum):
                return value.name.title()
            if isinstance(trait, ColorSchemeTrait):
                return [x.html for x in value]
            if isinstance(trait, ColorTrait):
                if value is None or value is Undefined:
                    return ''
                return value.html
            if isinstance(trait, tuple) and hasattr(trait, '_asdict'):
                return trait._asdict()
            return value

        return super(TraitsPropertiesMixin, self).__getattribute__(name)


    def __setattr__(self, name, value):
        prop_name = camel_to_snake(name)
        if prop_name != name and self._delegate.has_trait(prop_name):
            return self._delegate.set_trait(prop_name, value)

        return super(TraitsPropertiesMixin, self).__setattr__(name, value)