#!/usr/bin/env python

# Copyright (C) 2005-2007 INdT - Instituto Nokia de Tecnologia
#
# 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 St, Fifth Floor, Boston, MA  02110-1301 USA

__author__ = "Artur Duque de Souza"
__author_email__ = "artur.souza@openbossa.org"
__license__ = "GPL"

import os
import ecore
import errno
import array
import logging


import canolad.prefs
from canolad.haltab import Haltab
from canolad.prefs import PluginPrefs, create_initial_prefs

import dbus
import dbus.service

try:
    from e_dbus import DBusEcoreMainLoop
    DBusEcoreMainLoop(set_as_default=True)
except Exception:
    import dbus.ecore


class Daemon(dbus.service.Object):
    """Class to daemonize the media engine

    This class uses the double-fork technique in order to daemonize
    our media engine. It also has basic functions to be executed by
    the class that wants to be daemonized.

    """
    log = logging.getLogger("canola.daemon")
    DBUS_SERVICE_NAME = "br.org.indt.canola.Daemon"
    DBUS_OBJ_PATH     = "/br/org/indt/canola/Daemon"
    DBUS_IFACE        = "br.org.indt.canola.Daemon"

    def __init__(self, pidfile):
        self.pidfile = pidfile
        self.locked = False
        self.session_bus = dbus.SessionBus()
        self.bn = dbus.service.BusName(self.DBUS_SERVICE_NAME,
                                       bus=self.session_bus)

        dbus.service.Object.__init__(self, self.bn, self.DBUS_OBJ_PATH)

    def _daemonize(self):
        """Daemonize the subclass using 'double-fork' trick"""
        # do first fork
        try:
            pid = os.fork()

            if pid > 0:
                # must exit first parent
                os._exit(0)
        except OSError, e:
            self.log.critical("Fork #1 failed %d (%s)", e.errno, e.strerror)
            os._exit(1)

        os.setsid() # run a program in a new session
        os.umask(0)

        # do second fork
        try:
            pid = os.fork()

            if pid > 0:
                # must exit first parent
                os._exit(0)
        except OSError, e:
            self.log.critical("Fork #2 failed %d (%s)", e.errno, e.strerror)
            os._exit(1)

        # write pid to file
        pid = str(os.getpid())
        file(self.pidfile,'w+').write("%s\n" % pid)

    def _getpid(self):
        """Get the pid inside pidfile"""
        try:
            pidfile = open(self.pidfile, "r")
            pid = int(pidfile.readline())
            pidfile.close()
        except IOError:
            pid = None

        if pid and not self._pid_exists(pid):
            self._delpid()
            pid = None

        return pid

    def _pid_exists(self, pid):
        """Check whether certain pid is valid (has a process with it)"""
        try:
            os.kill(pid, 0)
            return True
        except OSError, err:
            return err.errno == errno.EPERM

    def _delpid(self):
        """Delete the pidfile"""
        os.remove(self.pidfile)
        return True

    def start(self):
        """Start the daemon"""
        pid = self._getpid()

        if pid:
            # the daemon is running or we have a problem
            raise LookupError(pid)

        # Start the daemon
        self._daemonize()
        self.run()
        return True

    @dbus.service.method(DBUS_IFACE)
    def stop(self):
        """Stop the daemon, killing the process"""
        # Try to get pid from file
        pid = self._getpid()

        if pid:

            if pid == os.getpid():
                ecore.idler_add(ecore.main_loop_quit)
                self._delpid()
                self.log.info("Process %s@%s stopped by dbus",
                              pid, self.pidfile)
                if self.db_is_locked():
                    self.db_unlocked()
                return True
            else:
                self.log.warning("You should not try to stop MediaEngine"
                                 " through another process")

        else:
            # When we are restarting the daemon this is not an error
            self.log.info("Pidfile %s does not exist. Are you sure the "
                          "daemon is running ?", self.pidfile)
            return True

    def main_event_checker(self, fd_handler, notifier):
        notifier.read_events()
        notifier.process_events()
        return True

    def run(self):
        first_time = PluginPrefs("canolad")
        canolad_configfile = os.path.join(PluginPrefs.prefs_dir, "canolad")
        try:
            if not os.path.exists(canolad_configfile) \
               or first_time["version"] < canolad.prefs.__version__:
                create_initial_prefs(first_time)
        except Exception:
            create_initial_prefs(first_time)

        try:
            # use HAL
            self.log.info("Trying to use HAL")
            self.mger_tab = Haltab(self.dev_added, self.dev_removed,
                                   self.db_locked, self.db_unlocked)
            self.log.info("USING HAL")
        except Exception, e:
            self.log.warning("Could not use HAL: %s", e)
            osso_version = open("/etc/osso_software_version").readline()

            if not os.path.islink("/etc/mtab") and \
               osso_version.find("2007") >= 0 and \
               osso_version.find("HACKER") < 0:
                # use inotify
                from canolad.pmtab import Pmtab
                from pyinotify import WatchManager, Notifier, EventsCodes

                self.log.info("Using Inotify")
                wm = WatchManager()
                self.mger_tab = Pmtab(wm, self.dev_added, self.dev_removed,
                                      self.db_locked, self.db_unlocked)
                notifier = Notifier(wm, self.mger_tab)
                ecore.fd_handler_add(wm._fd, ecore.ECORE_FD_READ,
                                     self.main_event_checker, notifier)
            else:
                # use GnomeVFS (gregale)
                from canolad.gnomevfstab import GnomeVFSTab
                self.log.info("Using GnomeVFS")
                self.mger_tab = \
                    GnomeVFSTab(self.dev_added, self.dev_removed,
                                self.db_locked, self.db_unlocked)

        ecore.main_loop_begin()

    def recreate_str_paths(self, scanlist):
        new_scanlist = []
        for p in scanlist:
            path_array = array.array("B")
            path_array.fromlist(p)
            path = path_array.tostring()
            new_scanlist.append(path)
        return new_scanlist

    @dbus.service.method(DBUS_IFACE)
    def scan_paths(self, audio_path, video_path, photo_path):
        audio_path = self.recreate_str_paths(audio_path)
        video_path = self.recreate_str_paths(video_path)
        photo_path = self.recreate_str_paths(photo_path)
        self.mger_tab.mount_table.scan_paths(audio_path, video_path, photo_path)

    @dbus.service.method(DBUS_IFACE)
    def scan_all_paths(self, clean_database):
        self.mger_tab.mount_table.scan_all_paths(clean_database)

    @dbus.service.method(DBUS_IFACE)
    def check_all_paths(self):
        self.mger_tab.mount_table.check_all_paths()

    @dbus.service.method(DBUS_IFACE)
    def scan_status(self):
        return self.mger_tab.mount_table.get_scan_status()

    @dbus.service.method(DBUS_IFACE)
    def scan_paths_by_type(self, scanlist=None, type="audio"):
        # to be on safe side (to avoid re like: ".*" later)
        if scanlist and scanlist[0] == "":
            scanlist = None
        else:
            scanlist = self.recreate_str_paths(scanlist)
        self.mger_tab.mount_table.scan_paths_by_type(scanlist, type)

    @dbus.service.method(DBUS_IFACE)
    def scan_all_mounted_paths(self):
        self.mger_tab.mount_table.scan_all_mounted_paths()

    @dbus.service.method(DBUS_IFACE)
    def start_monitoring(self):
        self.mger_tab.start_monitoring()

    @dbus.service.method(DBUS_IFACE)
    def db_is_locked(self):
        return self.locked

    @dbus.service.signal(DBUS_IFACE)
    def db_locked(self):
        # Just a signal
        self.locked = True
        self.log.info("Emit db_locked signal")
        return True

    @dbus.service.signal(DBUS_IFACE)
    def db_unlocked(self):
        # Just a signal
        self.locked = False
        self.log.info("Emit db_unlocked signal")
        return True

    @dbus.service.signal(DBUS_IFACE)
    def dev_added(self, mount_point):
        self.log.debug("Emit dev_added signal")

    @dbus.service.signal(DBUS_IFACE)
    def dev_removed(self, mount_point):
        self.log.debug("Emit dev_removed signal")

    def __str__(self):
        return "%s(pidfile=%s)" % (self.__class__.__name__, self.pidfile)
