#! /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

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


APP_NAME = "unity-lens-video"

#Translation stuff

#Get the local directory since we are not installing anything
LOCAL_PATH = os.path.realpath(os.path.dirname(sys.argv[0]))
# Init the list of languages to support
LANGS = []
#Check the default locale
LC, ENCODING = locale.getdefaultlocale()
if (LC):
    #If we have a default, it's the first in the list
    LANGS = [LC]
# Now lets get all of the supported languages on the system
LANGUAGE = os.environ.get('LANGUAGE', None)
if (LANGUAGE):
    # language comes back something like en_CA:en_US:en_GB:en
    # on linuxy systems, on Win32 it's nothing, so we need to
    # split it up into a list
    LANGS += LANGUAGE.split(":")
# Now add on to the back of the list the translations that we
# know that we have, our defaults
LANGS += ["en_US"]

# Now LANGS is a list of all of the languages that we are going
# to try to use.  First we check the default, then what the system
# told us, and finally the 'known' list

gettext.bindtextdomain(APP_NAME, LOCAL_PATH)
gettext.textdomain(APP_NAME)
# Get the language to use
LANG = gettext.translation(APP_NAME, LOCAL_PATH,
    languages=LANGS, fallback=True)
# Install the language, map _() (which we marked our
# strings to translate with) to self.lang.gettext() which will
# translate them.
_ = LANG.gettext

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

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
ZG = ZeitgeistClient()

# 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_RECENT,
            Gio.ThemedIcon.new(svg_dir + "group-recent.svg"),
            Unity.CategoryRenderer.VERTICAL_TILE))
        cats.append(Unity.Category.new(CAT_ONCOMPUTER,
            Gio.ThemedIcon.new(svg_dir + "group-installed.svg"),
            Unity.CategoryRenderer.VERTICAL_TILE))
        cats.append(Unity.Category.new(CAT_ONLINE,
            Gio.ThemedIcon.new(svg_dir + "group-downloads.svg"),
            Unity.CategoryRenderer.HORIZONTAL_TILE))
        # Specific Home Dash category
        cats.append(Unity.Category.new(CAT_GLOBAL,
            Gio.ThemedIcon.new("/usr/share/unity/5/lens-nav-video.svg"),
            Unity.CategoryRenderer.VERTICAL_TILE))
        self._lens.props.categories = cats

        filters = []
        self._lens.props.filters = filters
        
        
        # 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._lens.connect("notify::active", self.on_lens_active)
        self._scope.connect("filters-changed",self.on_filtering_changed)
        self._scope.props.sources.connect("notify::filtering",
            self.on_filtering_changed)
        self._lens.add_local_scope(self._scope)
        self._lens.export()

    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_lens_active(self, *_):
        """ Run a search when the lens is opened """
        if self._lens.props.active:
            self._scope.queue_search_changed(Unity.SearchType.DEFAULT)

    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:
                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):
        """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+'*' ],
                                          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)
        

    def add_results (self, search_status=None, model=None, 
                    cat=None, cancellable=None, result_list=[], search=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':3, 'results':result_list})
            else:
                if not search:
                    self.zg_call ()
                result_sets.append({'category':1, 'results':result_list})
            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(
                Gio.FILE_ATTRIBUTE_THUMBNAIL_PATH,
                Gio.FileQueryInfoFlags.NONE,
                None)
            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):
        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))
        ZG.find_events_for_templates(
            [event_template, ],
            self.zg_events ,
            timerange = time_range,
            storage_state = datamodel.StorageState.Any,
            num_events = max_amount_results,
            result_type = datamodel.ResultType.MostRecentSubjects
        )

    def zg_events(self, events):
        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)
                    item.append(self.get_icon(path))
                    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"))
                result_list.append(item)

        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])
            self._scope.props.results_model.append(uri, icon_hint, 
                0, "text/html", title, comment, dnd_uri)
        
# pylint: enable=R0903

def main():
    """Connect to the session bus, exit if there is a running instance."""
    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)

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

if __name__ == "__main__":
    main()
