# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

from __future__ import absolute_import
from __future__ import print_function

from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import GET_SCHEMA_RE
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _

# Default our global support flag
NOTIFY_DBUS_SUPPORT_ENABLED = False

# Image support is dependant on the GdkPixbuf library being available
NOTIFY_DBUS_IMAGE_SUPPORT = False

# The following are required to hook into the notifications:
NOTIFY_DBUS_INTERFACE = 'org.freedesktop.Notifications'
NOTIFY_DBUS_SETTING_LOCATION = '/org/freedesktop/Notifications'

# Initialize our mainloops
LOOP_GLIB = None
LOOP_QT = None


try:
    # dbus essentials
    from dbus import SessionBus
    from dbus import Interface
    from dbus import Byte
    from dbus import ByteArray
    from dbus import DBusException

    #
    # now we try to determine which mainloop(s) we can access
    #

    # glib
    try:
        from dbus.mainloop.glib import DBusGMainLoop
        LOOP_GLIB = DBusGMainLoop()

    except ImportError:
        # No problem
        pass

    # qt
    try:
        from dbus.mainloop.qt import DBusQtMainLoop
        LOOP_QT = DBusQtMainLoop(set_as_default=True)

    except ImportError:
        # No problem
        pass

    # We're good as long as at least one
    NOTIFY_DBUS_SUPPORT_ENABLED = (
        LOOP_GLIB is not None or LOOP_QT is not None)

    try:
        # The following is required for Image/Icon loading only
        import gi
        gi.require_version('GdkPixbuf', '2.0')
        from gi.repository import GdkPixbuf
        NOTIFY_DBUS_IMAGE_SUPPORT = True

    except (ImportError, ValueError, AttributeError):
        # No problem; this will get caught in outer try/catch

        # A ValueError will get thrown upon calling gi.require_version() if
        # GDK/GTK isn't installed on the system but gi is.
        pass

except ImportError:
    # No problem; we just simply can't support this plugin; we could
    # be in microsoft windows, or we just don't have the python-gobject
    # library available to us (or maybe one we don't support)?
    pass

# Define our supported protocols and the loop to assign them.
# The key to value pairs are the actual supported schema's matched
# up with the Main Loop they should reference when accessed.
MAINLOOP_MAP = {
    'qt': LOOP_QT,
    'kde': LOOP_QT,
    'glib': LOOP_GLIB,
    'dbus': LOOP_QT if LOOP_QT else LOOP_GLIB,
}


# Urgencies
class DBusUrgency(object):
    LOW = 0
    NORMAL = 1
    HIGH = 2


# Define our urgency levels
DBUS_URGENCIES = (
    DBusUrgency.LOW,
    DBusUrgency.NORMAL,
    DBusUrgency.HIGH,
)


class NotifyDBus(NotifyBase):
    """
    A wrapper for local DBus/Qt Notifications
    """

    # The default descriptive name associated with the Notification
    service_name = 'DBus Notification'

    # The default protocols
    # Python 3 keys() does not return a list object, it's it's own dict_keys()
    # object if we were to reference, we wouldn't be backwards compatible with
    # Python v2.  So converting the result set back into a list makes us
    # compatible

    protocol = list(MAINLOOP_MAP.keys())

    # A URL that takes you to the setup/help of the specific protocol
    setup_url = 'https://github.com/caronc/apprise/wiki/Notify_dbus'

    # No throttling required for DBus queries
    request_rate_per_sec = 0

    # Allows the user to specify the NotifyImageSize object
    image_size = NotifyImageSize.XY_128

    # The number of seconds to keep the message present for
    message_timeout_ms = 13000

    # Limit results to just the first 10 line otherwise there is just to much
    # content to display
    body_max_line_count = 10

    # This entry is a bit hacky, but it allows us to unit-test this library
    # in an environment that simply doesn't have the gnome packages
    # available to us.  It also allows us to handle situations where the
    # packages actually are present but we need to test that they aren't.
    # If anyone is seeing this had knows a better way of testing this
    # outside of what is defined in test/test_glib_plugin.py, please
    # let me know! :)
    _enabled = NOTIFY_DBUS_SUPPORT_ENABLED

    # Define object templates
    templates = (
        '{schema}://_/',
    )

    # Define our template arguments
    template_args = dict(NotifyBase.template_args, **{
        'urgency': {
            'name': _('Urgency'),
            'type': 'choice:int',
            'values': DBUS_URGENCIES,
            'default': DBusUrgency.NORMAL,
        },
        'x': {
            'name': _('X-Axis'),
            'type': 'int',
            'min': 0,
            'map_to': 'x_axis',
        },
        'y': {
            'name': _('Y-Axis'),
            'type': 'int',
            'min': 0,
            'map_to': 'y_axis',
        },
        'image': {
            'name': _('Include Image'),
            'type': 'bool',
            'default': True,
            'map_to': 'include_image',
        },
    })

    def __init__(self, urgency=None, x_axis=None, y_axis=None,
                 include_image=True, **kwargs):
        """
        Initialize DBus Object
        """

        super(NotifyDBus, self).__init__(**kwargs)

        # Track our notifications
        self.registry = {}

        # Store our schema; default to dbus
        self.schema = kwargs.get('schema', 'dbus')

        if self.schema not in MAINLOOP_MAP:
            msg = 'The schema specified ({}) is not supported.' \
                .format(self.schema)
            self.logger.warning(msg)
            raise TypeError(msg)

        # The urgency of the message
        if urgency not in DBUS_URGENCIES:
            self.urgency = DBusUrgency.NORMAL

        else:
            self.urgency = urgency

        # Our x/y axis settings
        self.x_axis = x_axis if isinstance(x_axis, int) else None
        self.y_axis = y_axis if isinstance(y_axis, int) else None

        # Track whether or not we want to send an image with our notification
        # or not.
        self.include_image = include_image

        return

    def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
        """
        Perform DBus Notification
        """

        if not self._enabled or MAINLOOP_MAP[self.schema] is None:
            self.logger.warning(
                "{} notifications could not be loaded.".format(self.schema))
            return False

        # Acquire our session
        try:
            session = SessionBus(mainloop=MAINLOOP_MAP[self.schema])

        except DBusException:
            # Handle exception
            self.logger.warning('Failed to send DBus notification.')
            self.logger.exception('DBus Exception')
            return False

        # If there is no title, but there is a body, swap the two to get rid
        # of the weird whitespace
        if not title:
            title = body
            body = ''

        # acquire our dbus object
        dbus_obj = session.get_object(
            NOTIFY_DBUS_INTERFACE,
            NOTIFY_DBUS_SETTING_LOCATION,
        )

        # Acquire our dbus interface
        dbus_iface = Interface(
            dbus_obj,
            dbus_interface=NOTIFY_DBUS_INTERFACE,
        )

        # image path
        icon_path = None if not self.include_image \
            else self.image_path(notify_type, extension='.ico')

        # Our meta payload
        meta_payload = {
            "urgency": Byte(self.urgency)
        }

        if not (self.x_axis is None and self.y_axis is None):
            # Set x/y access if these were set
            meta_payload['x'] = self.x_axis
            meta_payload['y'] = self.y_axis

        if NOTIFY_DBUS_IMAGE_SUPPORT and icon_path:
            try:
                # Use Pixbuf to create the proper image type
                image = GdkPixbuf.Pixbuf.new_from_file(icon_path)

                # Associate our image to our notification
                meta_payload['icon_data'] = (
                    image.get_width(),
                    image.get_height(),
                    image.get_rowstride(),
                    image.get_has_alpha(),
                    image.get_bits_per_sample(),
                    image.get_n_channels(),
                    ByteArray(image.get_pixels())
                )

            except Exception as e:
                self.logger.warning(
                    "Could not load Gnome notification icon ({}): {}"
                    .format(icon_path, e))

        try:
            # Always call throttle() before any remote execution is made
            self.throttle()

            dbus_iface.Notify(
                # Application Identifier
                self.app_id,
                # Message ID (0 = New Message)
                0,
                # Icon (str) - not used
                '',
                # Title
                str(title),
                # Body
                str(body),
                # Actions
                list(),
                # Meta
                meta_payload,
                # Message Timeout
                self.message_timeout_ms,
            )

            self.logger.info('Sent DBus notification.')

        except Exception:
            self.logger.warning('Failed to send DBus notification.')
            self.logger.exception('DBus Exception')
            return False

        return True

    def url(self, privacy=False, *args, **kwargs):
        """
        Returns the URL built dynamically based on specified arguments.
        """

        _map = {
            DBusUrgency.LOW: 'low',
            DBusUrgency.NORMAL: 'normal',
            DBusUrgency.HIGH: 'high',
        }

        # Define any arguments set
        args = {
            'format': self.notify_format,
            'overflow': self.overflow_mode,
            'image': 'yes' if self.include_image else 'no',
            'urgency': 'normal' if self.urgency not in _map
                       else _map[self.urgency],
            'verify': 'yes' if self.verify_certificate else 'no',
        }

        # x in (x,y) screen coordinates
        if self.x_axis:
            args['x'] = str(self.x_axis)

        # y in (x,y) screen coordinates
        if self.y_axis:
            args['y'] = str(self.y_axis)

        return '{schema}://_/?{args}'.format(
            schema=self.schema,
            args=NotifyDBus.urlencode(args),
        )

    @staticmethod
    def parse_url(url):
        """
        There are no parameters nessisary for this protocol; simply having
        gnome:// is all you need.  This function just makes sure that
        is in place.

        """
        schema = GET_SCHEMA_RE.match(url)
        if schema is None:
            # Content is simply not parseable
            return None

        results = NotifyBase.parse_url(url)
        if not results:
            results = {
                'schema': schema.group('schema').lower(),
                'user': None,
                'password': None,
                'port': None,
                'host': '_',
                'fullpath': None,
                'path': None,
                'url': url,
                'qsd': {},
            }

        # Include images with our message
        results['include_image'] = \
            parse_bool(results['qsd'].get('image', True))

        # DBus supports urgency, but we we also support the keyword priority
        # so that it is consistent with some of the other plugins
        urgency = results['qsd'].get('urgency', results['qsd'].get('priority'))
        if urgency and len(urgency):
            _map = {
                '0': DBusUrgency.LOW,
                'l': DBusUrgency.LOW,
                'n': DBusUrgency.NORMAL,
                '1': DBusUrgency.NORMAL,
                'h': DBusUrgency.HIGH,
                '2': DBusUrgency.HIGH,
            }

            try:
                # Attempt to index/retrieve our urgency
                results['urgency'] = _map[urgency[0].lower()]

            except KeyError:
                # No priority was set
                pass

        # handle x,y coordinates
        try:
            results['x_axis'] = int(results['qsd'].get('x'))

        except (TypeError, ValueError):
            # No x was set
            pass

        try:
            results['y_axis'] = int(results['qsd'].get('y'))

        except (TypeError, ValueError):
            # No y was set
            pass

        return results
