# -*- coding: utf-8 -*-
# Elisa - Home multimedia server
# Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Elisa with Fluendo's plugins.
#
# The GPL part of Elisa is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Elisa" in the root directory of this distribution package
# for details on that license.

# Based on work by Jan Jokela <janjokela@gmail.com>

__maintainer__ = 'Philippe Normand <philippe@fluendo.com>'

from elisa.base_components.media_provider import MediaProvider
from elisa.core import media_uri, component, common, db_backend, log
from elisa.core.utils import misc
from elisa.core.observers.dict import DictObservable

from twisted.internet import threads


import os, sys, threading

DEFAULT_DB_PATH = os.path.join(os.path.expanduser('~'),
                               '.gnome2', 'f-spot', 'photos.db')

from elisa.extern.translation import gettexter, N_ 
T_ = gettexter('elisa-fspot')

class FspotDB(log.Loggable):
    """ Retrieves tags and videos from the F-spot sqlite database
    """
    log_category = 'fspot_db'
    
    def __init__(self, db_path=DEFAULT_DB_PATH):
        """ Attempt a connection to the database
        """
        log.Loggable.__init__(self)

        if not os.path.exists(db_path):
            raise db_backend.DBBackendError("FSpot DB not found at %r" % db_path)
        
        options = {'db_backend': 'sqlite', 'database': db_path}
        self._backend = db_backend.DBBackend(**options)
        self._version = None
        self._hidden_id = None

    def get_version(self):
        if self._version is None:
            rows = self._backend.sql_execute("select data from meta where name=?",
                                             "F-Spot Database Version")
            if rows:
                self._version = int(rows[0].data)
            else:
                self._version = 0
                
        return self._version

    def get_hidden_id(self): 
        """ Return an id of the 'Hidden' tag 
        @rtype: int 
        """ 
        if self._hidden_id is None: 
            rows = self._backend.sql_execute("SELECT data FROM meta WHERE name=?", 
                                             "Hidden Tag Id")             
            if rows: 
                self._hidden_id = int(rows[0].data) 
            else: 
                self._hidden_id = 0                 
        return self._hidden_id 

    def get_tag_with_id(self, tag_id):
        tag = None
        query = "SELECT id, name, category_id as parent_id from tags where id=?"
        rows = self._backend.sql_execute(query, tag_id)
        if len(rows):
            tag = rows[0]
        return tag

    def get_tags(self, tag_id=None):
        """ Return a list of tags [(tag id, tag name), (..)]
        @rtype: list
        """
        if tag_id == None:
            query = "SELECT id, name FROM tags WHERE id != ? AND category_id=0 ORDER BY name"
            params = (self.get_hidden_id(),)
        else:
            query = "SELECT id, name FROM tags WHERE category_id=? ORDER BY name"
            params = (tag_id,)
        rows = self._backend.sql_execute(query, *params)
        return rows
      
    def get_photos(self, tag, hidden):
        """ Return a list of photos with the given tag
            [(diectory_path, filename), (..)]

        @param tag: the tag id 
        @type tag:  int
        @param hidden:    showing hidden photos
        @type hidden:    boolean
        @rtype:     list
        """
            
        params = ()
        if self.get_version() < 8:
            query = "SELECT id, directory_path, name FROM photos WHERE "
        else:
            query = "SELECT id, uri FROM photos WHERE "
            
        subq = "(SELECT tag_id FROM photo_tags WHERE photo_id = photos.id)"

        if tag is -2 and not hidden:        # untagged
            query += "%s IS NULL " % subq
        elif tag is -1 and hidden == True:  # hidden, no other tags
            params += (self.get_hidden_id(),)
            query += "? IN %s AND (SELECT COUNT(*) FROM photo_tags WHERE photo_id = photos.id) = 1" % subq
        elif hidden == True:                # hidden within tag
            params += (tag, self.get_hidden_id())
            query += "? IN %s AND ? IN %s " % (subq, subq) 
        elif hidden == False:               # normal within tag
            params += (tag, self.get_hidden_id())
            query += "? IN %s AND ? NOT IN %s " % (subq, subq)
        else:                               # all within tag
            params += (tag, )
            query += "? IN %s " % subq        
        
        query += "ORDER BY photos.time"
        
        rows = self._backend.sql_execute(query, *params)
        return rows
    
    def make_indices(self): # create the indices needed to upgrade db's performance
        query = "CREATE INDEX IF NOT EXISTS elisa_photo_ids ON photo_tags (photo_id);"
        self._backend.sql_execute(query)
        query = "CREATE INDEX IF NOT EXISTS elisa_tag_ids ON photo_tags (tag_id);"
        self._backend.sql_execute(query)
        
    def drop_indices(self): # drop the indices created by elisa
        query = "DROP INDEX IF EXISTS elisa_photo_ids;"
        self._backend.sql_execute(query)
        query = "DROP INDEX IF EXISTS elisa_tag_ids;"
        self._backend.sql_execute(query)

class FspotMedia(MediaProvider):
    """ The Fspot MediaProvider component
    """

    default_config = {'db_path': DEFAULT_DB_PATH,
                      'show_hidden': 1}
    config_doc = {'db_path': 'absolute path to f-spot photos.db file',
                  'show_hidden': 'show hidden photos'}

    def initialize(self):
        self._db_lock = threading.Lock()
        self._db_connections = {}
        self._show_hidden = self.config.get('show_hidden', 1)

        db_path = self.config.get('db_path', DEFAULT_DB_PATH)

        if os.path.dirname(db_path) in ('','..'):
            app_config = common.application.config
            conf_path = app_config.get_config_dir()
            self.debug("Using %r to search F-Spot DB", conf_path)
            db_path = os.path.join(conf_path, db_path)
            
        self._db_path = db_path
        
        # Some rudimentary database checks
        try:
            db = FspotDB(self._db_path)
        # if not, we can't initialize our component
        except db_backend.DBBackendError, error:
            raise component.InitializeFailure(self.name, str(error))
        else:
            version = db.get_version()
            if version == 0:
                msg = "%r is not F-Spot DB" % self._db_path
                raise component.InitializeFailure(self.name, msg)
            
            self.debug("Found a F-Spot DB with version %s at %r", version,
                       self._db_path)
            #FIXME: a proper version number is needed once upstream gets indices  
            if version < 1000:
                db.make_indices()
            if version > 1000:
                db.drop_indices()
        
        plugin_registry = common.application.plugin_registry
        ForeignApplicationMessage = plugin_registry.get_component_class('base:foreign_application_message')

            
        # Lets make sure the user doesn't need to manually add Fspot
        # to the image activity locations
        uri = "fspot:///"
        action_type = ForeignApplicationMessage.ActionType.LOCATION_ADDED
        msg = ForeignApplicationMessage(action_type, "F-Spot", 'fspot',
                                        uri, media_types=['image',],
                                        theme_icon='fspot')
        common.application.bus.send_message(msg)


    def clean(self):
        if self.config.get('db_path') == DEFAULT_DB_PATH:
            del self.config['db_path']
        MediaProvider.clean(self)
        
    def db__get(self):
        db = None

        try:
            self._db_lock.acquire()
            current_thread = threading.currentThread()
            thread_id = id(current_thread)
            db = self._db_connections.get(thread_id)
            if not db:
                db = FspotDB(self._db_path)
                self._db_connections[thread_id] = db
        finally:
            self._db_lock.release()

        return db

    def scannable_uri_schemes__get(self):
        return []

    def supported_uri_schemes__get(self):
        return { 'fspot': 0 }

    def get_real_uri(self, uri):
        if self._blocking_is_directory(uri):
            return uri

        # our photos are files, so let's convert our uri's
        # to 'file' scheme
        if uri.host == '.':
            dirname = os.path.dirname(self._db_path)
        else:
            dirname = uri.host
        real_uri = media_uri.MediaUri("file://%s%s" % (dirname,uri.path))
        self.debug("%r is real URI of %r", real_uri, uri)
        return real_uri

    def _blocking_get_media_type(self, uri):
        """
        @param uri: the URI to analyze
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:     dict
        """
        file_type = ''
        mime_type = ''
        if not self._blocking_is_directory(uri):
            # ok, it's a file, let's ask for a mime and file type
            mime_type, file_type = misc.get_media_infos_from_mime(uri)
            if None in (mime_type, file_type):
                self.warning("Unknown media: %r", uri)
        else:   
            # directories don't have mime_types
            file_type = 'directory'
        return {'file_type': file_type, 'mime_type': mime_type}

    def _blocking_is_directory(self, uri):
        """
        return True if a directory

        @param uri: the URI to analyze
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:     bool
        """
        if uri.path == '/':
            # yup, the sheme root dir is a .. dir 
            return True
        elif uri.get_param('id'): 
            # this means it's a tag, we can assume tags are dirs
            return True
        else: return False
        
    def has_children_with_types(self, uri, media_types):
        return threads.deferToThread(self._blocking_has_children_with_types,
                                     uri, media_types)

    def _blocking_has_children_with_types(self, uri, media_types):
        """
        @param uri:         the URI to scan
        @type uri:          L{elisa.core.media_uri.MediaUri}
        @param media_types: the media_types to look for on the directory
        @type media_types:  list of strings
        @rtype:             bool
        """
        has_children = False
        # determine type of query
        parent_id = int(uri.get_param('id', -1))        
        if parent_id is self.db.get_hidden_id():        # hidden
            parent_id = int(uri.get_param('tag_id'))
            hidden = True
        elif self._show_hidden:                         # all
            hidden = None
        else:                                           # not hidden
            hidden = False                                

        if 'image' in media_types or 'directory' in media_types:
            db = self.db
            if self._blocking_is_directory(uri): 
                if 'directory' in media_types:
                    if parent_id != -1:
                        has_children = len(db.get_tags(parent_id)) > 0
                        if not has_children and 'image' in media_types:
                            has_children = len(db.get_photos(parent_id, hidden)) > 0
                    else:
                        has_children = True
                elif uri.path == '/':
                    tags = db.get_tags()
                    if tags != []:
                        has_children = True
                elif len(uri.path):
                    photos = db.get_photos(parent_id, hidden)
                    if photos != []:
                        has_children = True
        return has_children
    
    def get_direct_children(self, uri, l):
        d = threads.deferToThread(self._blocking_get_direct_children, uri, l)
        return d

    def _blocking_get_direct_children(self, uri, list_of_children):
        db = self.db
        hidden = db.get_tag_with_id(db.get_hidden_id())
        # if the uri path is /, we have to retrieve the tags from the fspot
        # database
        if uri.path == '/':
            # add to the children list a MediaUri representing each tag
            tags = db.get_tags()
            parent_id = -1
            for tag in tags:
                t = media_uri.MediaUri("fspot:///%s" % tag.name)
                t.set_param('id', tag.id)
                t.set_param('tag_id', parent_id)
                list_of_children.append((t, {}))

            # add an "untagged" entry
            t = media_uri.MediaUri("fspot:///%s" % 'Untagged')
            t.set_param('id', -2)
            t.set_param('tag_id', parent_id) 
            t.label = T_(N_('Untagged'))
            list_of_children.append((t, {}))
                    
        elif len(uri.path):
            # it's a tag, lets get our child tags and child photos

            # strip leading /
            path = uri.path[1:]

            # if showing hidden photos
            if int(uri.get_param('id')) is hidden.id:
                parent_id = int(uri.get_param('tag_id'))
                photos = db.get_photos(parent_id, True)                
            else:
                parent_id = int(uri.get_param('id'))
                photos = db.get_photos(parent_id, False)             

                # add child tags
                tags = db.get_tags(parent_id)
                for tag in tags:
                    t = media_uri.MediaUri("fspot:///%s" % tag.name)
                    t.set_param('id', int(tag.id))
                    t.set_param('tag_id', parent_id)
                    list_of_children.append((t, {}))

            # add to the children list a MediaUri representing each photo

            for photo in photos:
                if photo.has_key('uri'):
                    p = media_uri.MediaUri(photo.uri)
                    p.scheme = 'fspot'
                else:
                    photo_path = os.path.join(photo.directory_path, photo.name)
                    p = media_uri.MediaUri("fspot://%s" % photo_path)
                    # we want the photo filename
                    label = photo.name.decode("utf-8")
                    p.label = label
                p.fragment = photo.id
                p.set_param('tag_id', parent_id)
                d = DictObservable({'default_image': self.get_real_uri(p)})
                list_of_children.append((p, d))

        # add the "hidden" entry
        if self._show_hidden and hidden and (int(uri.get_param('id', -1)) is not hidden.id):
            t = media_uri.MediaUri("fspot:///%s" % hidden.name)
            t.set_param('id', hidden.id)
            t.set_param('tag_id', parent_id)
            list_of_children.append((t, {}))
                
        return list_of_children
        
    def _blocking_open(self, uri, mode='r'):
        # we can't open directories
        if self._blocking_is_directory(uri):
            return None

        # lets convert the uri from our scheme to a file scheme
        # and ask the media_manager to provide us a capable
        # component
        uri = self.get_real_uri(uri)
        media_manager = common.application.media_manager
        if media_manager.enabled:
            media = media_manager.open(uri, mode, block)
        else:
            media = None
        return media
