#
# Copyright (C) 2010, Nokia <ivan.frade@nokia.com>
# Copyright (C) 2018, Sam Thursfield <sam@afuera.me.uk>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


import gi

gi.require_version("Tsparql", "3.0")
from gi.repository import Gio, GLib, GObject
from gi.repository import Tsparql

import logging

import trackertestutils.mainloop

import configuration


log = logging.getLogger(__name__)


class AwaitException(RuntimeError):
    pass

class WakeupCycleTimeoutException(RuntimeError):
    pass

DEFAULT_TIMEOUT = 10

class await_endpoint():
    def __init__(self, proxy, dbus_signal, uri, timeout=DEFAULT_TIMEOUT):
        self.proxy = proxy
        self.dbus_signal = dbus_signal
        self.uri = uri
        self.timeout = timeout
        self.result = False
        self.loop = trackertestutils.mainloop.MainLoop()

    def __enter__(self):
        def dbus_signal_cb(proxy, service, method, data):
            if method == self.dbus_signal:
                [uri, object_path] = data
                if self.uri == uri:
                    self.result = True
                    self.loop.quit()

        def timeout_cb():
            log.info("Timeout fired after %s seconds", self.timeout)
            self.loop.quit()
            raise Exception(
                f"Timeout ({self.timeout}s) awaiting new endpoint for {self.uri}")

        self.signal_id = self.proxy.connect('g-signal', dbus_signal_cb)
        self.timeout_id = GLib.timeout_add_seconds(self.timeout, timeout_cb)

        return self.result

    def __exit__(self, etype, evalue, etraceback):
        if etype is not None:
            return False

        while not self.result:
            self.loop.run_checked()

        GLib.source_remove(self.timeout_id)
        GObject.signal_handler_disconnect(self.proxy, self.signal_id)
        return True

class MinerFsHelper:

    MINERFS_BUSNAME = "org.freedesktop.LocalSearch3"
    MINERFS_OBJ_PATH = "/org/freedesktop/Tracker3/Miner/Files"
    MINER_IFACE = "org.freedesktop.Tracker3.Miner"
    MINERFS_CONTROL_BUSNAME = "org.freedesktop.LocalSearch3.Control"
    MINERFS_INDEX_OBJ_PATH = "/org/freedesktop/Tracker3/Miner/Files/Index"
    MINER_INDEX_IFACE = "org.freedesktop.Tracker3.Miner.Files.Index"

    def __init__(self, dbus_connection):
        self.log = logging.getLogger(__name__)

        self.bus = dbus_connection

        self.loop = trackertestutils.mainloop.MainLoop()

        self.miner_fs = Gio.DBusProxy.new_sync(
            self.bus,
            Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION,
            None,
            self.MINERFS_BUSNAME,
            self.MINERFS_OBJ_PATH,
            self.MINER_IFACE,
        )

        self.index = Gio.DBusProxy.new_sync(
            self.bus,
            Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION,
            None,
            self.MINERFS_CONTROL_BUSNAME,
            self.MINERFS_INDEX_OBJ_PATH,
            self.MINER_INDEX_IFACE,
        )

    def start(self):
        # Ensure the miner service is enabled through an action-less call
        self.miner_fs.GetStatus()

    def get_sparql_connection(self):
        return Tsparql.SparqlConnection.bus_new(
            self.MINERFS_BUSNAME, None, self.bus
        )

    def start_watching_progress(self):
        self._previous_status = None
        self._target_wakeup_count = None
        self._wakeup_count = 0

        def signal_handler(proxy, sender_name, signal_name, parameters):
            if signal_name == "Progress":
                self._progress_cb(*parameters.unpack())

        self._progress_handler_id = self.miner_fs.connect("g-signal", signal_handler)

    def stop_watching_progress(self):
        if self._progress_handler_id != 0:
            self.miner_fs.disconnect(self._progress_handler_id)

    def _progress_cb(self, status, progress, remaining_time):
        if self._previous_status is None:
            self._previous_status = status
        if self._previous_status != "Idle" and status == "Idle":
            self._wakeup_count += 1

        if (
            self._target_wakeup_count is not None
            and self._wakeup_count >= self._target_wakeup_count
        ):
            self.loop.quit()

    def wakeup_count(self):
        """Return the number of wakeup-to-idle cycles the miner-fs completed."""
        return self._wakeup_count

    def await_wakeup_count(self, target_wakeup_count, timeout=DEFAULT_TIMEOUT):
        """Block until the miner has completed N wakeup-and-idle cycles.

        This function is for use by miner-fs tests that should trigger an
        operation in the miner, but which do not cause a new resource to be
        inserted. These tests can instead wait for the status to change from
        Idle to Processing... and then back to Idle.

        The miner may change its status any number of times, but you can use
        this function reliably as follows:

            wakeup_count = miner_fs.wakeup_count()
            # Trigger a miner-fs operation somehow ...
            miner_fs.await_wakeup_count(wakeup_count + 1)
            # The miner has probably finished processing the operation now.

        If the timeout is reached before enough wakeup cycles complete, an
        exception will be raised.

        """

        assert self._target_wakeup_count is None

        if self._wakeup_count >= target_wakeup_count:
            log.debug(
                "miner-fs wakeup count is at %s (target is %s). No need to wait",
                self._wakeup_count,
                target_wakeup_count,
            )
        else:

            def _timeout_cb():
                raise WakeupCycleTimeoutException()

            timeout_id = GLib.timeout_add_seconds(timeout, _timeout_cb)

            log.debug(
                "Waiting for miner-fs wakeup count of %s (currently %s)",
                target_wakeup_count,
                self._wakeup_count,
            )
            self._target_wakeup_count = target_wakeup_count
            self.loop.run_checked()

            self._target_wakeup_count = None
            GLib.source_remove(timeout_id)

    def index_location(self, uri, graphs=None, flags=None):
        return self.index.IndexLocation("(sasas)", uri, graphs or [], flags or [])

    def removable_device_object_path(self, mountpoint_path):
        path = str(mountpoint_path)
        object_path = ''
        for c in path:
            if (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9'):
                object_path += c
            else:
                object_path += '_%x' % ord(c)
        return '/org/freedesktop/LocalSearch3/%s' % object_path

    def await_endpoint_added (self, uri):
        return await_endpoint(self.miner_fs, 'EndpointAdded', uri)

    def await_endpoint_removed (self, uri):
        return await_endpoint(self.miner_fs, 'EndpointRemoved', uri)
