#! /usr/bin/python
# -*- coding: utf-8 -*-
#
#    cameramonitor - The main application.
#
#    Copyright  2005-07 Adolfo González Blázquez <code@infinicode.org>
#               2011 PhobosK <phobosk@kbfx.net>
#
#    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.

"""This is the main application."""

import os
import sys
import errno
import subprocess
from time import time
from signal import signal
from getopt import getopt
from traceback import print_exc

import pygtk
pygtk.require("2.0")
import gtk
import pynotify

from cameramonitor import camglobals as glob
from cameramonitor import preferences
from cameramonitor import utils
from cameramonitor import watch
from cameramonitor import dbus_support
from cameramonitor import autostart

__authors__ = "Adolfo González Blázquez <code@infinicode.org>, PhobosK <phobosk@kbfx.net>"
__copyright__ = "Copyright 2005-11, Adolfo González Blázquez and PhobosK"
__license__ = "GPL"
__version__ = "0.3.2"
__maintainer__ = "PhobosK <phobosk@kbfx.net>"
__status__ = "Development"

debug = 0

gtk.gdk.threads_init()


class CameraMonitor:
    """This is the main class of the application.

    It is created only when the application is run without the -p command line
    option.
    It is threaded.
    """

    def __init__(self):
        """Init method."""

        self.timestamp = 0 # We need this to avoid too much notifications.
        self.device_status = "none"
        self.no_access = False
        utils.about_shown = False

        self.prefs = preferences.CameraMonitorPreferences('nested')

        self.video_device = self.prefs.video_device
        self.icon_visible = self.prefs.icon_visible
        self.notify = self.prefs.notify

        pynotify.init(glob.name)

        self.check_camera()
        self.create_icon()
        self.menu = self.create_menu()

        self.gconf_monitor = utils.preferences_monitor_gconf()

    def check_camera(self):
        """Checks initial camera state or when device is changed.

        It is used to set the application's icon and notification balloon.
        Uses "video_device" attribute to check the path.
        Sets "device_status" param.
        """
        # First check if we have perms to read from device.
        perms = utils.check_perms(self.video_device)
        if perms == 1: # Device exists though no perms to read it, ok to watch.
            utils.display_dialog_warning( \
                "<b>" + glob.message_no_permission_to_use_camera + "</b>\n" \
                + glob.message_verify_device_permissions + "\n\t<b>%s</b>\n\n" \
                % self.video_device \
                + "<i>" + glob.hint_add_to_video_group + "</i>\n\n\n" \
                + "<b><i><big>" + glob.message_will_watch_device + "</big></i></b>")
            self.device_status = "unknown" # We cannot(?) know if the device is already opened so we set it to off.
            self.no_access = False
            return
        if perms == errno.ENOENT:
            self.device_status = "none" # Device doesn't exist yet, ok to watch.
            self.no_access = False
            return
        if perms == errno.EACCES:  # No path access to the device, we will not watch it.
            utils.display_dialog_error( \
                "<b>" + glob.message_no_permission_to_use_camera + "</b>\n" \
                + glob.message_verify_device_path_permissions + "\n\t<b>%s</b>\n\n" \
                % self.video_device \
                + "<b><i><big>" + glob.message_will_not_watch_device + "</big></i></b>")
            self.device_status = "none"
            self.no_access = True
            return

        # Check if the device is in use.
        text = ""
        try:
            process = subprocess.Popen(["fuser", self.video_device], stdout=subprocess.PIPE)
        except:
            if debug: print_exc()
        else:
            text = process.stdout.read()
            process.terminate()

        if text != "":
            self.device_status = "on"
            self.no_access = False
            return

        self.no_access = False
        self.device_status = "off"

    def exit_callback(self, widget):
        """Exit callback to destroy the instance."""
        gtk.main_quit()
        return

    def on_popup_menu(self, status, button = 0, time = 0):
        """Callback for the icon when clicked.

        Default values of 0 are needed so left click to work too (via connecting
        the "activate" signal to this callback).
        """
        self.menu.popup(None, None, gtk.status_icon_position_menu, button, time, self.tray)

    # Creates tray icon
    def create_icon(self):
        """Creates the actual application tray icon."""
        self.tray = gtk.StatusIcon()
        self.tray.connect('activate', self.on_popup_menu) # Connect left click
        self.tray.connect('popup-menu', self.on_popup_menu) # Connect right click
        self.update_icon()

    def update_icon(self, visibility_only = False):
        """Updates the tray icon.

        Confines to the preferences option icon_visibile, so icon not shown if
        state is off, none or unknown.

        @param visibility_only: If to change only visibility
        @param type: Bool
        """
        is_visible = self.tray.get_visible()

        if self.device_status == "on":
            if not is_visible: self.tray.set_visible(True)
        elif self.icon_visible:
            if not is_visible: self.tray.set_visible(True)
        else: self.tray.set_visible(False)

        if not visibility_only:
            self.tray.set_from_file(glob.image[self.device_status])
            self.tray.set_tooltip(glob.tooltip[self.device_status])

        gtk.gdk.flush()

        self.create_notify()


    def create_notify(self):
        """Displays the notification if requested using pynotify.

        In order to prevent too much calls to it we use a timestamp compare set
        to 3 sec., so we do not get too much confusing notifications while for
        example gstreamer-properties checks the available cams.
        """
        if not self.notify: return

        timestamp_compare = time() - self.timestamp
        if timestamp_compare < 3:
            if debug: print "Notification request too often: %s sec. Rejecting it." \
                                                            % timestamp_compare
            return

        n = pynotify.Notification(glob.name_long, glob.tooltip[self.device_status] + \
                    "\n" + glob.message_video_device + "%s" % self.video_device)
        p = gtk.gdk.pixbuf_new_from_file(glob.image[self.device_status])
        n.set_icon_from_pixbuf(p)

        # Attach the notification to status icon if possible.
        try:
            n.attach_to_status_icon(self.tray)
        except:
            if debug: print "Unable to attach to status icon."

        try:
            return_result = n.show()
        except:
            if debug: print "Notification stack limit is reached (50 by default)"
            if debug: print_exc()
        else:
            if debug and not return_result:
                print "Not able to send the notification."

        gtk.gdk.flush()

        self.timestamp = time()


    def create_menu(self):
        """Creates the context menu."""
        menu = gtk.Menu()

        pref = gtk.ImageMenuItem(stock_id=gtk.STOCK_PREFERENCES, accel_group=None)
        pref.connect("activate", self.prefs.preferences_show)

        about = gtk.ImageMenuItem(stock_id=gtk.STOCK_ABOUT, accel_group=None)
        about.connect("activate", self.about_show)

        sep = gtk.SeparatorMenuItem()

        quit = gtk.ImageMenuItem(stock_id=gtk.STOCK_QUIT, accel_group=None)
        quit.connect("activate", self.exit_callback)

        menu.add(pref)
        menu.add(about)
        menu.add(sep)
        menu.add(quit)

        menu.show_all()
        return menu

    def about_show(self, widget):
        """Creates the about dialog.

        Uses show() and modal to avoid problems by gtk.AboutDialog.run() when
        used with threads.
        Shows only one at a time.
        """
        if utils.about_shown: return

        utils.about_shown = True

        about = gtk.AboutDialog()
        about.set_name(glob.name_long + ' '+ glob.version)
        about.set_authors(glob.authors)
        about.set_comments(glob.description)
        about.set_icon_from_file(glob.logo)
        about.set_logo(gtk.gdk.pixbuf_new_from_file(glob.logo))
        about.set_license(glob.license)
        about.set_wrap_license(True)
        about.set_copyright(glob.copyright)

        def openHomePage(widget, url, url2):
            import webbrowser
            webbrowser.open_new_tab(url)

        gtk.about_dialog_set_url_hook(openHomePage,glob.website)
        about.set_website(glob.website)
        about.set_modal(True)
        about.connect('response', utils.dialog_destroy)
        about.show()


def preferences_key_video_device_changed(client, connection_id, entry, args):
    """Callback: someone's changed the gconf key for video_device.

    Arguments come from GConf notify_add response.
    """
    try:
        video_device = entry.get_value().get_string()
    except:
        if debug: print "Changed 'video_device' GConf entry is INVALID."
        return

    global camera, watcher

    gtk.gdk.threads_enter()

    camera.video_device = video_device
    if debug:
        print "Changed 'video_device' GConf entry.\nKey now reads: %s" % camera.video_device

    camera.check_camera()
    watcher.update(camera.video_device)
    if debug: print "Video Device preferences updated."

    camera.update_icon(False)

    gtk.gdk.threads_leave()


def preferences_key_icon_visible_changed(client, connection_id, entry, args):
    """Callback: someone's changed the gconf key for visibility.

    Arguments come from GConf notify_add response.
    """
    try:
        icon_visible = entry.get_value().get_bool()
    except:
        if debug: print "Changed 'visible' GConf entry is INVALID."
        return

    global camera

    gtk.gdk.threads_enter()

    camera.icon_visible = icon_visible
    if debug:
        print "Changed 'visible' GConf entry.\nKey now reads: %s" % camera.icon_visible

    camera.update_icon(True)

    gtk.gdk.threads_leave()


def preferences_key_notify_changed(client, connection_id, entry, args):
    """Callback: someone's changed the gconf key for notify.

    Arguments come from GConf notify_add response.
    """
    try:
        notify = entry.get_value().get_bool()
    except:
        if debug: print "Changed 'notify' GConf entry is INVALID."
        return

    global camera

    gtk.gdk.threads_enter()

    camera.notify = notify
    if debug:
        print "Changed 'notify' GConf entry.\nKey now reads: %s" % camera.notify

    gtk.gdk.threads_leave()


def signal_handler(sig, frame):
    """Handles signals 2 (INT), 3 (QUIT) and 15 (TERM)"""
    gtk.main_quit()
    if debug: print "Exiting by signal handler."


def command_usage():
    """Prints the application command line options.

    These options are:
        -h, --help  Displays this message
        -p, --prefs Display the preferences dialog
        -d, --debug Turn debugging on
    """
    print "Usage: %s [options]" % os.path.basename(sys.argv[0])
    print "\nApplication options:"
    print " -h, --help  Display this message"
    print " -p, --prefs Display the preferences dialog"
    print " -d, --debug Turn debugging on"
    sys.exit(0)


def parse_commandline_options():
    """Parses the command line options.

    These options are:
        -h, --help  Displays this message
        -p, --prefs Display the preferences dialog
        -d, --debug Turn debugging on

    Sets priority of options. Ensures that if the -d option is passed, it is
    always proccessed, no matter before or after another option
    (like in "-p -d" or "-d -p").
    """
    global debug

    try:
        opts, args = getopt(sys.argv[1:],"hpd", ["help","prefs","debug"])
    except:
        print "Invalid options!\n"
        command_usage()
        sys.exit(2)

    for o, a in opts:
        if o in ("-d", "--debug"):
            debug = watch.debug = autostart.debug = \
                    utils.debug = dbus_support.debug = 1

    for o, a in opts:
        if o in ("-p", "--prefs"):
            command_preferences()
            sys.exit(0)

    for o, a in opts:
        if o in ("-h", "--help"):
            command_usage()


def command_preferences():
    """Starts only a preference dialog if it is an unique instance OR shows the
    preferences dialog of the already running application, and exits.
    """
    if not utils.is_unique_instance("alone"):
        if debug: print "This is not an unique instance. Exiting."
        return

    dbus_controller = dbus_support.DbusController("Preferences")

    prefs_alone = dbus_support.prefs_alone = preferences.CameraMonitorPreferences('alone')

    prefs_alone.preferences_show(None)

    gtk.main()
    if debug: print "Exiting preference dialog."


def main():
    """This is the main method of the application that is called first.

    It does (in order of appearance):
        - checks the command line options
        - checks if it is unique instance
        - sets the signal handling
        - initialises the use of threads
        - creates the application
        - sets the device to watch via inotify
        - sets the DBus service
        - starts monitoring the video device path GConf key
    """
    global camera, watcher

    parse_commandline_options()

    if not utils.is_unique_instance("nested"):
        if debug: print "This is not an unique instance. Exiting."
        return

    # Handle signals 2 (INT), 3 (QUIT) and 15 (TERM)
    signal(2, signal_handler)
    signal(3, signal_handler)
    signal(15, signal_handler)

    camera = watch.camera = dbus_support.camera = CameraMonitor()
    watcher = watch.CameraMonitorWatcher(camera.video_device)

    dbus_controller = dbus_support.DbusController()

    camera.gconf_monitor.add_key(preferences_key_video_device_changed)
    camera.gconf_monitor.add_key(preferences_key_icon_visible_changed, \
                                                    glob.gconf_key_icon_visible)
    camera.gconf_monitor.add_key(preferences_key_notify_changed, \
                                                    glob.gconf_key_notify)

    gtk.gdk.threads_enter()
    gtk.main()
    gtk.gdk.threads_leave()

    if debug: print "Exiting main application."
    watcher.stop()


if __name__ == "__main__":
    main()
