import inspect
import enum
import dataclasses
import pathlib
from typing import Callable, Optional, Tuple, Any, Union, List, Type

from gi.repository import Gio, GLib


NoneType = type(None)


def run_action(widget, name, *args):
    print('run action', name, args)

    group, action = name.split('.')
    action_group = widget.get_action_group(group)

    if args:
        type_str = action_group.get_action_parameter_type(action)
        assert type_str

        if len(args) > 1:
            param = _create_variant(type_str.dup_string(), tuple(args))
        else:
            param = _create_variant(type_str.dup_string(), args[0])
    else:
        param = None

    action_group.activate_action(action, param)


def register_dataclass_actions(group, data, after=None):
    fields = dataclasses.fields(type(data))

    for field in fields:
        if field.name[0] == '_':
            continue

        def set_field(value, name=field.name):
            setattr(data, name, value)
            if after:
                after()

        name = field.name.replace('_', '-')
        try:
            register_action(group, set_field, name=f'set-{name}', types=(field.type,))
        except ValueError:
            continue


def register_action(group, fn: Callable, name: Optional[str] = None, types: Tuple[Type] = None):
    if name is None:
        name = fn.__name__.replace('_', '-')

    # Determine the type from the signature
    if types is None:
        signature = inspect.signature(fn)
        types = tuple(p.annotation for p in signature.parameters.values())

    if types:
        if inspect.Parameter.empty in types:
            raise ValueError('Missing parameter annotation for action ' + name)

        has_multiple = len(types) > 1
        if has_multiple:
            param_type = Tuple.__getitem__(types)
        else:
            param_type = types[0]

        type_str = variant_type_from_python(param_type)
        var_type = GLib.VariantType(type_str)

        build = generate_build_function(param_type)
        if not build:
            build = lambda a: a
    else:
        var_type = None

    action = Gio.SimpleAction.new(name, var_type)
    def activate(action, param):
        if param is not None:
            if has_multiple:
                fn(*build(param.unpack()))
            else:
                fn(build(param.unpack()))
        else:
            fn()
    action.connect('activate', activate)

    if hasattr(group, 'add_action'):
        group.add_action(action)
    else:
        group.insert(action)


def variant_type_from_python(py_type: type) -> str:
    if py_type is bool:
        return 'b'
    elif py_type is int:
        return 'x'
    elif py_type is float:
        return 'd'
    elif py_type is str:
        return 's'
    elif py_type is Any:
        return 'v'
    elif isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath):
        return 's'
    elif isinstance(py_type, type) and issubclass(py_type, enum.Enum):
        return variant_type_from_python(type(list(py_type)[0].value))
    elif dataclasses.is_dataclass(py_type):
        types = (f.type for f in dataclasses.fields(py_type))

        return '(' + ''.join(map(variant_type_from_python, types)) + ')'
    else:
        origin = py_type.__origin__

        if origin is list:
            assert len(py_type.__args__) == 1

            return 'a' + variant_type_from_python(py_type.__args__[0])
        elif origin is tuple:
            return '(' + ''.join(map(variant_type_from_python, py_type.__args__)) + ')'
        elif origin is dict:
            assert len(py_type.__args__) == 2

            key = variant_type_from_python(py_type.__args__[0])
            value = variant_type_from_python(py_type.__args__[1])
            return 'a{' + key + value + '}'
        elif origin is Union:
            non_maybe = [t for t in py_type.__args__ if t is not NoneType]
            has_maybe = len(non_maybe) != len(py_type.__args__)

            if has_maybe and len(non_maybe) == 1:
                return 'm' + variant_type_from_python(non_maybe[0])

            return ('m[' if has_maybe else '[') + ''.join(''.join(map(variant_type_from_python, non_maybe))) + ']'
        else:
            raise ValueError('{} does not have an equivalent'.format(py_type))


def unbuilt_type(py_type: type) -> type:
    if isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath):
        return str
    elif isinstance(py_type, type) and issubclass(py_type, enum.Enum):
        return type(list(py_type)[0].value)
    elif dataclasses.is_dataclass(py_type):
        return tuple
    return py_type


def generate_build_function(py_type: type) -> Optional[Callable]:
    """
    Return a function for reconstructing dataclasses and enumerations after
    unpacking a GVariant. When no reconstruction is needed None is returned.
    """

    if isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath):
        return py_type
    elif isinstance(py_type, type) and issubclass(py_type, enum.Enum):
        return py_type
    elif dataclasses.is_dataclass(py_type):
        types = tuple(f.type for f in dataclasses.fields(py_type))
        tuple_build = generate_build_function(Tuple.__getitem__(types))

        if not tuple_build:
            return lambda values: py_type(*values)

        return lambda values: py_type(*tuple_build(values))
    elif hasattr(py_type, '__origin__'):
        origin = py_type.__origin__

        if origin is list:
            assert len(py_type.__args__) == 1

            build = generate_build_function(py_type.__args__[0])

            if build:
                return lambda values: [build(v) for v in values]

        elif origin is tuple:
            builds = list(map(generate_build_function, py_type.__args__))

            if not any(builds):
                return None

            return lambda values: tuple((build(value) if build else value) for build, value in zip(builds, values))
        elif origin is dict:
            assert len(py_type.__args__) == 2

            build_key = generate_build_function(py_type.__args__[0])
            build_value = generate_build_function(py_type.__args__[1])

            if not build_key and not build_value:
                return None

            return lambda values: {
                (build_key(key) if build_key else key): (build_value(value) if build_value else value)
                for key, value in values.items()}
        elif origin is Union:
            builds = list(map(generate_build_function, py_type.__args__))

            if not any(builds):
                return None

            unbuilt_types = list(map(unbuilt_type, py_type.__args__))

            def build(value):
                for bld, type_ in zip(builds, unbuilt_types):
                    if isinstance(value, type_):
                        if bld:
                            return bld(value)
                        else:
                            return value
                return value
            return build
    return None


_VARIANT_CONSTRUCTORS = {
    'b': GLib.Variant.new_boolean,
    'y': GLib.Variant.new_byte,
    'n': GLib.Variant.new_int16,
    'q': GLib.Variant.new_uint16,
    'i': GLib.Variant.new_int32,
    'u': GLib.Variant.new_uint32,
    'x': GLib.Variant.new_int64,
    't': GLib.Variant.new_uint64,
    'h': GLib.Variant.new_handle,
    'd': GLib.Variant.new_double,
    's': GLib.Variant.new_string,
    'o': GLib.Variant.new_object_path,
    'g': GLib.Variant.new_signature,
    'v': GLib.Variant.new_variant,
}

from gi._gi import variant_type_from_string

def _create_variant(type_str, value):
    assert type_str

    if isinstance(value, enum.Enum):
        value = value.value
    elif isinstance(value, pathlib.PurePath):
        value = str(value)
    elif dataclasses.is_dataclass(type(value)):
        fields = dataclasses.fields(type(value))
        value = tuple(getattr(value, field.name) for field in fields)

    vtype = GLib.VariantType(type_str)

    if type_str in _VARIANT_CONSTRUCTORS:
        return _VARIANT_CONSTRUCTORS[type_str](value)

    builder = GLib.VariantBuilder.new(vtype)
    if value is None:
        return builder.end()

    if vtype.is_maybe():
        builder.add_value(_create_variant(vtype.element().dup_string(), value))
        return builder.end()

    try:
        iter(value)
    except TypeError:
        raise TypeError("Could not create array, tuple or dictionary entry from non iterable value %s %s" %
                        (type_str, value))

    if vtype.is_tuple() and vtype.n_items() != len(value):
        raise TypeError("Tuple mismatches value's number of elements %s %s" % (type_str, value))
    if vtype.is_dict_entry() and len(value) != 2:
        raise TypeError("Dictionary entries must have two elements %s %s" % (type_str, value))

    if vtype.is_array():
        element_type = vtype.element().dup_string()
        if isinstance(value, dict):
            value = value.items()
        for i in value:
            builder.add_value(_create_variant(element_type, i))
    else:
        remainer_format = type_str[1:]
        for i in value:
            dup = variant_type_from_string(remainer_format).dup_string()
            builder.add_value(_create_variant(dup, i))
            remainer_format = remainer_format[len(dup):]

    return builder.end()
