# Author: Robert Park <robru@gottengeography.ca>, (C) 2010
# Copyright: See COPYING file included with this distribution.

"""Common classes and datatypes used throughout the app.

The `selected` and `modified` set()s contain Photograph() instances, and
are frequently used for iteration and membership testing throughout the app.

The `points` dict maps epoch seconds to ChamplainCoordinate() instances. This
is used to place photos on the map by looking up their timestamps.
"""


from gi.repository import GObject, Gio, GLib
from functools import wraps

from gg.version import PACKAGE


# These variables are used for sharing data between classes
selected = set()
modified = set()
points = {}


def singleton(cls):
    """Decorate a class with @singleton when There Can Be Only One.

    >>> @singleton
    ... class Highlander: pass
    >>> Highlander() is Highlander() is Highlander
    True
    >>> id(Highlander()) == id(Highlander)
    True
    """
    class single(cls):
        def __call__(self):
            return self
    return single()


def memoize(obj):
    """General-purpose cache for classes, methods, and functions.

    Functions are cached by their arguments:

    >>> @memoize
    ... def doubler(foo):
    ...     print('performing expensive calculation...')
    ...     return foo * 2
    >>> doubler(50)
    performing expensive calculation...
    100
    >>> doubler(50)
    100

    Methods are also cached by all their arguments, including `self`, which
    means that different instances will not share their cache. This is
    primarily used in the Gtk.Builder subclasses, where we want to cache slow
    widget lookups, but we don't want different instances stepping on each
    other's widgets:

    >>> class WidgetFactory:
    ...     @memoize
    ...     def get_by_name(self, name):
    ...         print('Making new widget named', name)
    ...         return '<<{}>>'.format(name)
    >>> one = WidgetFactory()
    >>> one.get_by_name('bob')
    Making new widget named bob
    '<<bob>>'
    >>> one.get_by_name('bob')
    '<<bob>>'
    >>> two = WidgetFactory()
    >>> two.get_by_name('bob')
    Making new widget named bob
    '<<bob>>'

    Finally, class instantiations are also cached based on the arguments passed
    to the constructor:

    >>> @memoize
    ... class Memorable:
    ...     def __init__(self, foo): pass
    >>> Memorable('alpha') is Memorable('alpha')
    True
    >>> Memorable('alpha') is Memorable('beta')
    False
    >>> len(Memorable.instances)
    2
    """
    cache = obj.cache = {}
    obj.instances = cache.values()

    @wraps(obj)
    def memoizer(*args, **kwargs):
        """Do cache lookups and populate the cache in the case of misses."""
        key = args[0] if len(args) is 1 else args
        if key not in cache:
            cache[key] = obj(*args, **kwargs)
        return cache[key]
    return memoizer


class staticmethod(object):
    """Make @staticmethods play nice with @memoize.

    >>> @memoize
    ... class HasStatic:
    ...     @staticmethod
    ...     def do_something():
    ...         print('Invoked with no arguments.')
    >>> HasStatic.do_something()
    Invoked with no arguments.

    Without this class, the above example would raise
    TypeError: 'staticmethod' object is not callable
    """

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        """Call the static method with no instance."""
        return self.func(*args, **kwargs)


@memoize
class Binding(GObject.Binding):
    """Make it easier to bind properties between GObjects."""

    def __init__(self, source, sprop, target, tprop=None,
                 flags=GObject.BindingFlags.DEFAULT):
        super().__init__(
            source=source, source_property=sprop,
            target=target, target_property=tprop or sprop,
            flags=flags)


class GSettings(Gio.Settings):
    """Override GSettings to be more useful to me."""
    get = Gio.Settings.get_value

    def __init__(self, schema='ca.' + PACKAGE, path=None):
        if path is not None:
            path = '/ca/{}/{}s/{}/'.format(PACKAGE, schema, path)
            schema = 'ca.{}.{}'.format(PACKAGE, schema)

        super().__init__(schema, path)

        # These are used to avoid infinite looping.
        self._ignore_key_changed = False
        self._ignore_prop_changed = True

    def bind(self, key, widget, prop='', flags=Gio.SettingsBindFlags.DEFAULT):
        """Don't make me specify the default flags every time."""
        Gio.Settings.bind(self, key, widget, prop or key, flags)

    def bind_with_convert(self, key, widget, prop, key_to_prop, prop_to_key):
        """Recreate g_settings_bind_with_mapping from scratch.

        This method was shamelessly stolen from John Stowers'
        gnome-tweak-tool on May 14, 2012.
        """
        def key_changed(settings, key):
            """Update widget property."""
            if self._ignore_key_changed:
                return
            self._ignore_prop_changed = True
            widget.set_property(prop, key_to_prop(self[key]))
            self._ignore_prop_changed = False

        def prop_changed(widget, param):
            """Update GSettings key."""
            if self._ignore_prop_changed:
                return
            self._ignore_key_changed = True  # pragma: no cover
            self[key] = prop_to_key(widget.get_property(prop))
            self._ignore_key_changed = False

        self.connect('changed::' + key, key_changed)
        widget.connect('notify::' + prop, prop_changed)
        key_changed(self, key)  # init default state


@singleton
class Gst(GSettings):
    """This is the primary GSettings instance for main app settings only.

    It cannot be used to access the relocatable schema, for that you'll have
    to create a new GSettings() instance.
    """

    def set_history(self, value):
        """Convert the map history to an array of tuples."""
        self.set_value('history', GLib.Variant('a(ddi)', value))

    def set_window_size(self, value):
        """Convert the window size to a pair of ints."""
        self.set_value('window-size', GLib.Variant('(ii)', value))

    def set_color(self, color):
        """Convert the GdkColor to a three-int tuple."""
        variant = GLib.Variant('(iii)', (color.red, color.green, color.blue))
        self.set_value('track-color', variant)


class Struct:
    """This is a generic object which can be assigned arbitrary attributes.

    >>> foo = Struct({'one': 2})
    >>> foo.one
    2
    >>> foo.four = 4
    >>> foo.four
    4
    """

    def __init__(self, attributes={}):
        self.__dict__.update(attributes)
