#! /usr/bin/python
# -*- coding: utf-8 -*-

#    Copyright (c) 2011 David Calle <davidc@framli.eu>

#    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 3 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, see <http://www.gnu.org/licenses/>.

"""The unity video lens. Indexing videos in ~/Videos and generating thumbnails if needed."""

import gettext
import locale
import os
import sys
from zeitgeist.client import ZeitgeistClient
from zeitgeist import client, datamodel
import time
import fractions
import dbus

#pylint: disable=E0611
from gi.repository import (
    GLib,
    GObject,
    Gio,
    Unity,
    Dee
)
#pylint: enable=E0611

APP_NAME = "unity-lens-video"
LOCAL_PATH = "/usr/share/locale/"

gettext.bindtextdomain(APP_NAME, LOCAL_PATH)
gettext.textdomain(APP_NAME)
_ = gettext.gettext

# Translatable strings
CAT_ONCOMPUTER = _("My Videos")
CAT_ONLINE = _("Online")
CAT_GLOBAL = _("Videos")
CAT_RECENT = _("Recently Viewed")
CAT_MORE_SUGGESTIONS = _("More suggestions")
HINT = _("Search Videos")
SOURCES = _("Sources")
LOCAL_VIDEOS = _("My Videos")

CAT_INDEX_MY_VIDEOS = 0
CAT_INDEX_ONLINE = 1
CAT_INDEX_MORE = 2

BUS_NAME = "net.launchpad.lens.video"
FOLDER = GLib.get_user_special_dir(GLib.USER_DIRECTORY_VIDEOS)
HOME_FOLDER = GLib.get_home_dir()
CACHE = "%s/unity-lens-video" % GLib.get_user_cache_dir()
DB = "videos.db"
Q = []
Q_MAX = 3
try:
    ZG = ZeitgeistClient()
except:
    raise SystemExit(1)

REFRESH_TIMEOUT = 300
PREVIEW_PLAYER_DBUS_NAME = "com.canonical.Unity.Lens.Music.PreviewPlayer"
PREVIEW_PLAYER_DBUS_PATH = "/com/canonical/Unity/Lens/Music/PreviewPlayer"
PREVIEW_PLAYER_DBUS_IFACE = PREVIEW_PLAYER_DBUS_NAME

# pylint: disable=R0903
class Daemon:
    
    """Creation of a lens with a local scope."""

    def __init__(self):
        #Create the lens
        self._lens = Unity.Lens.new("/net/launchpad/lens/video", "video")
        self._lens.props.search_hint = HINT
        self._lens.props.visible = True
        self._lens.props.search_in_global = True
        self._lens.props.sources_display_name = SOURCES

        svg_dir = "/usr/share/icons/unity-icon-theme/places/svg/"
        cats = []
        cats.append(Unity.Category.new(CAT_ONCOMPUTER,
            Gio.ThemedIcon.new(svg_dir + "group-videos.svg"),
            Unity.CategoryRenderer.VERTICAL_TILE))
        cats.append(Unity.Category.new(CAT_ONLINE,
            Gio.ThemedIcon.new(svg_dir + "group-internet.svg"),
            Unity.CategoryRenderer.VERTICAL_TILE))
        cats.append(Unity.Category.new(CAT_MORE_SUGGESTIONS,
            Gio.ThemedIcon.new(svg_dir + "group-treat-yourself.svg"),
            Unity.CategoryRenderer.VERTICAL_TILE))
        self._lens.props.categories = cats

        filters = []
        self._lens.props.filters = filters
        
        self.bus = dbus.SessionBus()
        
        # Create the scope
        self._scope = Unity.Scope.new("/net/launchpad/lens/video/main")
        self._scope.search_in_global = True
        self._scope.props.sources.add_option('local', LOCAL_VIDEOS, None)
        self._scope.connect("search-changed", self.on_search_changed)
        self._scope.connect("filters-changed",self.on_filtering_changed)
        self._scope.props.sources.connect("notify::filtering",
            self.on_filtering_changed)
        self._scope.connect('preview-uri', self.on_preview_uri)
        self._lens.add_local_scope(self._scope)
        self._lens.export()

        GLib.timeout_add_seconds(REFRESH_TIMEOUT, self.refresh_results)
        self._scope.queue_search_changed(Unity.SearchType.DEFAULT)

    def refresh_results(self, *_):
        """Update the results on a timeout."""
        print "Queuing new search because of timeout"
        self._scope.queue_search_changed(Unity.SearchType.DEFAULT)
        return True

    def on_filtering_changed(self, *_):
        """Run another search when a filter change is notified."""
        self._scope.queue_search_changed(Unity.SearchType.DEFAULT)

    def source_activated(self, source):
        """Return the state of a sources filter option."""
        active = self._scope.props.sources.get_option(source).props.active
        filtering = self._scope.props.sources.props.filtering
        if (active and filtering) or (not active and not filtering):
            return True
        else:
            return False
    
    def on_preview_closed(self, preview):
        try:                        
            player = self.bus.get_object (PREVIEW_PLAYER_DBUS_NAME, PREVIEW_PLAYER_DBUS_PATH)
            dbus.Interface (player, PREVIEW_PLAYER_DBUS_IFACE).Close()
        except Exception as e:
            print "Failed to send close signal to preview player:", e

    def on_preview_uri(self, scope, uri):
        """Preview request handler"""
        preview = None
        model = self._scope.props.results_model
        iter = model.get_first_iter()
        while not model.is_last(iter):
            if model.get_string(iter, 0) == uri:
                title = model.get_string(iter, 4);
                try:
                    subtitle = time.strftime("%x, %X", time.localtime(os.path.getmtime(GLib.filename_from_uri(uri, None))))
                except:
                    # Instead of empty, maybe the date/time of the zg event?
                    subtitle = ''
                desc = model.get_string(iter, 5);
                preview = Unity.MoviePreview.new(title, subtitle, desc, None)
                preview.connect('closed', self.on_preview_closed)

                # we may get remote uris from zeitgeist - fetch details for local files only
                if uri.startswith("file://"):
                    local_video = True
                    preview.props.image_source_uri = uri
                    try:                        
                        player = self.bus.get_object (PREVIEW_PLAYER_DBUS_NAME, PREVIEW_PLAYER_DBUS_PATH)
                        props = dbus.Interface (player, PREVIEW_PLAYER_DBUS_IFACE).VideoProperties(uri)
                        width = props['width']
                        height = props['height']
                        codec = props['codec']
                        dimensions = str(width) + "*" + str(height)
                        if width > 0 and height > 0:
                            gcd = fractions.gcd(width, height)
                            dimensions += ", " + str(width / gcd) + ":" + str(height / gcd)
                        preview.add_info(Unity.InfoHint.new("format", _("Format"), None, codec))
                        preview.add_info(Unity.InfoHint.new("dimensions", _("Dimensions"), None, dimensions))
                        preview.add_info(Unity.InfoHint.new("size", _("Size"), None, GLib.format_size(os.path.getsize(GLib.filename_from_uri(uri, None)))))
                    except Exception as e:
                        print "Couldn't get video details", e
                else:
                    local_video = False
                    preview.props.image_source_uri = model.get_string(iter, 1)
                play_video = Unity.PreviewAction.new("play", _("Play"), None)
                preview.add_action(play_video)
                if local_video:
                    show_folder = Unity.PreviewAction.new("show-in-folder", _("Show in Folder"), None)
                    show_folder.connect('activated', self.show_in_folder)
                    preview.add_action(show_folder)
                break
            iter = model.next(iter)
        if preview == None:
            print "Couldn't find model row for requested preview uri:", uri
        return preview

    def show_in_folder(self, scope, uri):
        """ Open folder that contains given video """
        file = Gio.file_new_for_uri (uri)
        parent = file.get_parent()
        if parent == None:
            parent = Gio.file_new_for_path("/")
        try:
            Gio.app_info_launch_default_for_uri(parent.get_uri(), None)
        except Exception as e:
            print "Couldn't launch default app for uri", parent.get_uri(), e
            return Unity.ActivationResponse(handled=Unity.HandledType.NOT_HANDLED)
        return Unity.ActivationResponse(handled=Unity.HandledType.HIDE_DASH)

    def on_search_changed(self, scope, search, search_type, cancellable):
        """On a new search, differentiate between lens view 
        and global search before updating the model"""
        search_status = search
        search_string = search.props.search_string.strip()
        print "Search changed to \"%s\"" % search_string
        model = search.props.results_model
        if self.source_activated('local'):
            if search_type is Unity.SearchType.GLOBAL:
                if search_string == '':
                    model.clear ()
                    if search_status:
                        search_status.finished ()
                    print "Global view without search string : hide"
                    
                else:
                    self.update_results_model(search_string, model, 'global', cancellable, search_status)
            else:
                if not search_string:
                    # this will call update_results_model as well
                    self.zg_call(cancellable, search_status)
                else:
                    self.update_results_model(search_string, model, 'lens', cancellable, search_status)
        else:
            model.clear()
            if search_status:
                search_status.finished ()

    def update_results_model(self, search, model, cat, cancellable, search_status, clear_model=True):
        """Check for the existence of the cache folder, create it if needed,
        and run the search method."""
        if not Gio.file_new_for_path(CACHE).query_exists(None):
            Gio.file_new_for_path(CACHE).make_directory(None)
        if FOLDER != HOME_FOLDER:
            try:
                GLib.spawn_async(['/usr/bin/updatedb', '-o', CACHE+'/'+DB,
                                  '-l', '0', '-U', FOLDER])
            except GLib.GError:
                print "Can't create the database, will retry."

        if self.is_file(CACHE+'/'+DB):
            try:
                results = GLib.spawn_sync(None,
                                          ['/usr/bin/locate',
                                           '-id', CACHE+'/'+DB,
                                           FOLDER+'*'+search.replace (" ","*")+'*' ],
                                          None, 0, None, None)
            except GLib.GError:
                results = None
            else:
                # spawn_sync returns bool, stdout, stderr, exit_status
                if results[3] == 0:
                    results = results[1]
                else:
                    results = None
        else:
            results = None
        result_list = []
        blacklist = self.get_blacklist ()
        if results:
                video_counter = 0
                print len(results.split('\n'))
                for video in results.split('\n'):
                    if video_counter < 100:
                        if self.is_video(video) and not self.is_hidden(video, blacklist):
                            video_counter+= 1
                            title = self.get_name(video)
                            comment = ''
                            uri = 'file://%s' % video
                            icon = self.get_icon(video)
                            if title:
                                item = []
                                item.append(title)
                                item.append(comment)
                                item.append(uri)
                                item.append(icon)
                                result_list.append(item)
                result_list = self.sort_alpha(result_list)
        
        GLib.idle_add(self.add_results, search_status, model, cat, cancellable, result_list, search, clear_model)
        

    def add_results (self, search_status=None, model=None, 
                    cat=None, cancellable=None, result_list=[], search=None,
                    clear_model=None):
        result_sets = []
        
        if cancellable and not cancellable.is_cancelled():
            if cat == 'global':
                # Create only one result set for the Global search
                result_sets.append({'category':CAT_INDEX_MY_VIDEOS, 'results':result_list})
            else:
                result_sets.append({'category':CAT_INDEX_MY_VIDEOS, 'results':result_list})
            if clear_model: model.clear()
            for result_set in result_sets:
                cat = result_set['category']
                for i in result_set['results']:
                    title = str(i[0])
                    comment = str(i[1])
                    uri = str(i[2])
                    dnd_uri = str(i[2])
                    icon_hint = str(i[3])
                    model.append(uri, icon_hint, cat, "text/html",
                        title, comment, dnd_uri)
            if search_status:
                print "Search finished"
                search_status.finished()
        else:
            print "Search cancelled"
        return False

    def is_file(self, uri):
        """Check if the file is an actual file"""
        g_file = Gio.file_new_for_path(uri)
        if g_file.query_exists(None):
            file_type = g_file.query_file_type(Gio.FileQueryInfoFlags.NONE,
                None)
            if file_type is Gio.FileType.REGULAR:
                return True

    def get_blacklist(self):
        """Get zeitgeist blacklist"""
        bt_list = []
        try:
            iface = client.ZeitgeistDBusInterface()
        except:
            print "Unable to connect to Zeitgeist, won't handle blacklists."
            iface = None
        if iface:
            blacklist = iface.get_extension("Blacklist", "blacklist")
            bt_list = blacklist.GetTemplates ()
        return bt_list

    def is_hidden(self, uri, blacklist):
        """Check if the file is hidden"""
        g_file = Gio.file_new_for_path(uri)
        hidden = g_file.query_info(
            Gio.FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
            Gio.FileQueryInfoFlags.NONE,
            None).get_attribute_boolean('standard::is-hidden')
        blacklisted = False
        for bt in blacklist:
            bt = bt.replace('dir-/', '/').encode("utf-8")
            if uri.startswith(bt):
                blacklisted = True
        if hidden or uri.find('/.') > -1 or blacklisted:
            return True

    def sort_alpha(self, results):
        """Sort results in several ways, depending on the category"""
        results.sort(key=lambda x: x[0].lower(), reverse=False)
        return results

    def is_video(self, uri):
        """Check if the file is a video"""
        if self.is_file(uri):
            g_file = Gio.file_new_for_path(uri)
            content_type = g_file.query_info('standard::content-type',
                Gio.FileQueryInfoFlags.NONE,
                None).get_content_type()
            if 'video' in content_type:
                return True

    def get_name(self, uri):
        """Get the display name of the file"""
        g_file = Gio.file_new_for_path(uri)
        name = g_file.query_info(
            Gio.FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
            Gio.FileQueryInfoFlags.NONE,
            None)
        display_name = name.get_attribute_as_string(
            Gio.FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME)
        return display_name

    def get_icon(self, uri):
        """This method checks several locations for a video thumbnail.

        1) <filename>.jpg file in the same folder (not activated for now)
        1) Nautilus thumbnails
        2) Cached thumbnails generated by the scope

        If nothing has been found, it tries to generate a thumbnail with Totem,
        stores and uses it.
        If the generation fails or is slow, it fallbacks to the standard video
        icon.
        """
        icon_path = None
        g_file = Gio.file_new_for_path(uri)
        video_path = g_file.get_path()
        thumb_name = video_path.replace(FOLDER, '').replace('/', '_')
        icon_check = '%s/thumb_%s.png' % (CACHE, thumb_name)
    #       if not icon_path:
    #           print 'Check for local cover'
    #           local_cover = uri.replace('.%s' % uri.split('.')[-1], '.jpg')
    #           if self.is_file(local_cover):
    #               icon_path = local_cover
        if not icon_path:
            icon = g_file.query_info(
                ','.join((Gio.FILE_ATTRIBUTE_THUMBNAIL_PATH,
                          Gio.FILE_ATTRIBUTE_THUMBNAILING_FAILED)),
                Gio.FileQueryInfoFlags.NONE,
                None)
            if icon.get_attribute_boolean(Gio.FILE_ATTRIBUTE_THUMBNAILING_FAILED):
                icon_path = 'video'
            else:
                icon_path = icon.get_attribute_as_string(
                    Gio.FILE_ATTRIBUTE_THUMBNAIL_PATH)
        if not icon_path:
            if self.is_file(icon_check):
                icon_path = icon_check
        if not icon_path:
            # Check if processes can be removed from the thumbnailing queue
            for process in Q:
                if not Gio.file_new_for_path(
                    "/proc/"+str(process)).query_exists(None):
                    Q.remove(process)
            if len(Q) < Q_MAX:
                try:
                    p = GLib.spawn_async(['/usr/bin/totem-video-thumbnailer', 
                                          video_path, icon_check])
                    Q.append(p[0])
                    if self.is_file(icon_check):
                        icon_path = icon_check
                except:
                    print "Warning : the file may have been removed."
        if not icon_path:
            icon_path = 'video'
        return icon_path

    def zg_call (self, cancellable, search_status):
        active = self._scope.props.sources.get_option("local").props.active
        filtering = self._scope.props.sources.props.filtering
        if active and filtering:
            uri = "file:*"
        else:
            uri = "*"
        time_range = datamodel.TimeRange.until_now ()
        max_amount_results = 24
        event_template = datamodel.Event()
        interpretation = datamodel.Interpretation.VIDEO
        event_template.append_subject(
            datamodel.Subject.new_for_values(uri=uri,interpretation=interpretation))
        def wrap(callback):
            def wrapped(events):
                callback(events, cancellable, search_status)

            return wrapped

        ZG.find_events_for_templates(
            [event_template, ],
            wrap(self.progress_zg_events),
            timerange = time_range,
            storage_state = datamodel.StorageState.Any,
            num_events = max_amount_results,
            result_type = datamodel.ResultType.MostRecentSubjects
        )

    def progress_zg_events(self, events, cancellable, search_status):
        if cancellable.is_cancelled(): return
        blacklist = self.get_blacklist ()
        result_list = []
        for event in events:
            item = []
            uri = event.get_subjects()[0].uri
            if uri.startswith('file://'):
                # If the file is local, we use the same methods 
                # as other result items.
                g_file = Gio.file_new_for_uri(uri)
                path = g_file.get_path()
                if self.is_video(path) and not self.is_hidden(path, blacklist):
                    item.append(self.get_name(path))
                    item.append('')
                    item.append(uri.encode("utf-8"))
                    item.append(self.get_icon(path))
                    item.append(CAT_INDEX_MY_VIDEOS)
                    result_list.append(item)
            elif uri.startswith('http'):
                # If the file is distant, we take 
                # all we need from Zeitgeist
                #  this one can be any unicode string:
                item.append(event.get_subjects()[0].text.encode("utf-8"))
                item.append('')
                #  these two *should* be ascii, but it can't hurt to be safe
                item.append(event.get_subjects()[0].uri.encode("utf-8"))
                item.append(event.get_subjects()[0].storage.encode("utf-8"))
                item.append(CAT_INDEX_ONLINE)
                result_list.append(item)
        search_status.props.results_model.clear ()
        for i in result_list:
            title = str(i[0])
            comment = str(i[1])
            uri = str(i[2])
            dnd_uri = str(i[2])
            icon_hint = str(i[3])
            category = i[4]
            self._scope.props.results_model.append(uri, icon_hint,
                category, "text/html", title, comment, dnd_uri)

        self.update_results_model("", search_status.props.results_model, 'lens', cancellable, search_status, False)
        
# pylint: enable=R0903

def main():
    """Connect to the session bus, exit if there is a running instance."""
    try:
        session_bus_connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
        session_bus = Gio.DBusProxy.new_sync(session_bus_connection, 0, None,
                                                'org.freedesktop.DBus',
                                                '/org/freedesktop/DBus',
                                                'org.freedesktop.DBus', None)
        result = session_bus.call_sync('RequestName',
                                        GLib.Variant("(su)", (BUS_NAME, 0x4)),
                                        0, -1, None)

        # Unpack variant response with signature "(u)". 1 means we got it.
        result = result.unpack()[0]

        if result != 1:
            print >> sys.stderr, "Failed to own name %s. Bailing out." % BUS_NAME
            raise SystemExit(1)
    except:
        raise SystemExit(1)

    daemon = Daemon()
    GObject.MainLoop().run()

if __name__ == "__main__":
    main()
