# Copyright 2015 Dustin Spicuzza <dustin@virtualroadside.com>
#           2018 Nikita Churaev <lamefun.x0r@gmail.com>
#           2018 Christoph Reiter <reiter.christoph@gmail.com>
#
# This library 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; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
# USA

import os
from collections import abc
from functools import partial

from gi.repository import GLib, GObject, Gio


def _extract_handler_and_args(obj_or_map, handler_name):
    handler = None
    if isinstance(obj_or_map, abc.Mapping):
        handler = obj_or_map.get(handler_name, None)
    else:
        handler = getattr(obj_or_map, handler_name, None)

    if handler is None:
        raise AttributeError(f"Handler {handler_name} not found")

    args = ()
    if isinstance(handler, abc.Sequence):
        if len(handler) == 0:
            raise TypeError(f"Handler {handler} tuple can not be empty")
        args = handler[1:]
        handler = handler[0]

    elif not callable(handler):
        raise TypeError(f"Handler {handler} is not a method, function or tuple")

    return handler, args


def define_builder_scope():
    from gi.repository import Gtk

    class BuilderScope(GObject.GObject, Gtk.BuilderScope):
        def __init__(self, scope_object=None):
            super().__init__()
            self._scope_object = scope_object

        def do_create_closure(self, builder, func_name, flags, obj):
            current_object = builder.get_current_object() or self._scope_object

            if not self._scope_object:
                current_object = builder.get_current_object()
                if func_name not in current_object.__gtktemplate_methods__:
                    return None

                current_object.__gtktemplate_handlers__.add(func_name)
                handler_name = current_object.__gtktemplate_methods__[func_name]
            else:
                current_object = self._scope_object
                handler_name = func_name

            swapped = int(flags & Gtk.BuilderClosureFlags.SWAPPED)
            if swapped:
                raise RuntimeError(f"{GObject.ConnectFlags.SWAPPED!r} not supported")
                return None

            handler, args = _extract_handler_and_args(current_object, handler_name)

            if obj:
                p = partial(handler, *args, swap_data=obj)
            else:
                p = partial(handler, *args)

            p.__gtk_template__ = True
            return p

    return BuilderScope


def connect_func(builder, obj, signal_name, handler_name, connect_object, flags, cls):
    if handler_name not in cls.__gtktemplate_methods__:
        return

    method_name = cls.__gtktemplate_methods__[handler_name]
    template_inst = builder.get_object(cls.__gtype_name__)
    template_inst.__gtktemplate_handlers__.add(handler_name)
    handler = getattr(template_inst, method_name)

    after = int(flags & GObject.ConnectFlags.AFTER)
    swapped = int(flags & GObject.ConnectFlags.SWAPPED)
    if swapped:
        raise RuntimeError(f"{GObject.ConnectFlags.SWAPPED!r} not supported")

    if connect_object is not None:
        func = obj.connect_object_after if after else obj.connect_object
        func(signal_name, handler, connect_object)
    else:
        func = obj.connect_after if after else obj.connect
        func(signal_name, handler)


def register_template(cls):
    from gi.repository import Gtk

    bound_methods = {}
    bound_widgets = {}

    for attr_name, obj in list(cls.__dict__.items()):
        if isinstance(obj, CallThing):
            setattr(cls, attr_name, obj._func)
            handler_name = obj._name
            if handler_name is None:
                handler_name = attr_name

            if handler_name in bound_methods:
                old_attr_name = bound_methods[handler_name]
                raise RuntimeError(
                    f"Error while exposing handler {handler_name!r} as {attr_name!r}, "
                    f"already available as {old_attr_name!r}"
                )
            bound_methods[handler_name] = attr_name
        elif isinstance(obj, Child):
            widget_name = obj._name
            if widget_name is None:
                widget_name = attr_name

            if widget_name in bound_widgets:
                old_attr_name = bound_widgets[widget_name]
                raise RuntimeError(
                    f"Error while exposing child {widget_name!r} as {attr_name!r}, "
                    f"already available as {old_attr_name!r}"
                )
            bound_widgets[widget_name] = attr_name
            cls.bind_template_child_full(widget_name, obj._internal, 0)

    cls.__gtktemplate_methods__ = bound_methods
    cls.__gtktemplate_widgets__ = bound_widgets

    if Gtk._version == "4.0":
        BuilderScope = define_builder_scope()
        cls.set_template_scope(BuilderScope())
    else:
        cls.set_connect_func(connect_func, cls)

    base_init_template = cls.init_template
    cls.__dontuse_ginstance_init__ = lambda s: init_template(s, cls, base_init_template)
    # To make this file work with older PyGObject we expose our init code
    # as init_template() but make it a noop when we call it ourselves first
    cls.init_template = cls.__dontuse_ginstance_init__


def init_template(self, cls, base_init_template):
    self.init_template = lambda: None

    if self.__class__ is not cls:
        raise TypeError(
            "Inheritance from classes with @Gtk.Template decorators "
            "is not allowed at this time"
        )

    self.__gtktemplate_handlers__ = set()

    base_init_template(self)

    for widget_name, attr_name in self.__gtktemplate_widgets__.items():
        self.__dict__[attr_name] = self.get_template_child(cls, widget_name)

    for handler_name, attr_name in self.__gtktemplate_methods__.items():
        if handler_name not in self.__gtktemplate_handlers__:
            raise RuntimeError(
                f"Handler '{handler_name}' was declared with @Gtk.Template.Callback "
                "but was not present in template"
            )


class Child:
    def __init__(self, name=None, **kwargs):
        self._name = name
        self._internal = kwargs.pop("internal", False)
        if kwargs:
            raise TypeError(f"Unhandled arguments: {kwargs!r}")


class CallThing:
    def __init__(self, name, func):
        self._name = name
        self._func = func


class Callback:
    def __init__(self, name=None):
        self._name = name

    def __call__(self, func):
        return CallThing(self._name, func)


def validate_resource_path(path):
    """Raises GLib.Error in case the resource doesn't exist."""
    try:
        Gio.resources_get_info(path, Gio.ResourceLookupFlags.NONE)
    except GLib.Error:
        # resources_get_info() doesn't handle overlays but we keep using it
        # as a fast path.
        # https://gitlab.gnome.org/GNOME/pygobject/issues/230
        Gio.resources_lookup_data(path, Gio.ResourceLookupFlags.NONE)


class Template:
    def __init__(self, **kwargs):
        self.string = None
        self.filename = None
        self.resource_path = None
        if "string" in kwargs:
            self.string = kwargs.pop("string")
        elif "filename" in kwargs:
            self.filename = kwargs.pop("filename")
        elif "resource_path" in kwargs:
            self.resource_path = kwargs.pop("resource_path")
        else:
            raise TypeError(
                "Requires one of the following arguments: "
                "string, filename, resource_path"
            )

        if kwargs:
            raise TypeError(f"Unhandled keyword arguments {kwargs!r}")

    @classmethod
    def from_file(cls, filename):
        return cls(filename=filename)

    @classmethod
    def from_string(cls, string):
        return cls(string=string)

    @classmethod
    def from_resource(cls, resource_path):
        return cls(resource_path=resource_path)

    Callback = Callback

    Child = Child

    def __call__(self, cls):
        from gi.repository import Gtk

        if not isinstance(cls, type) or not issubclass(cls, Gtk.Widget):
            raise TypeError("Can only use @Gtk.Template on Widgets")

        if "__gtype_name__" not in cls.__dict__:
            raise TypeError(
                f"{cls.__name__!r} does not have a __gtype_name__. Set it to the name "
                "of the class in your template"
            )

        if hasattr(cls, "__gtktemplate_methods__"):
            raise TypeError("Cannot nest template classes")

        if self.string is not None:
            data = self.string
            if not isinstance(data, bytes):
                data = data.encode("utf-8")
            bytes_ = GLib.Bytes.new(data)
            cls.set_template(bytes_)
            register_template(cls)
            return cls
        if self.resource_path is not None:
            validate_resource_path(self.resource_path)
            cls.set_template_from_resource(self.resource_path)
            register_template(cls)
            return cls
        assert self.filename is not None
        file_ = Gio.File.new_for_path(os.fspath(self.filename))
        bytes_ = GLib.Bytes.new(file_.load_contents()[1])
        cls.set_template(bytes_)
        register_template(cls)
        return cls


__all__ = ["Template"]
