# -*- 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 2.
# 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.

import gobject
gobject.threads_init()
import gst
import os
import sys
import time
from threading import Lock
import platform

from elisa.base_components.metadata_provider import \
        MetadataProvider, MetadataError
from elisa.core.log import Loggable
from elisa.core.media_uri import MediaUri, unquote
from elisa.core.utils import locale_helper
from elisa.core.component import InitializeFailure
from elisa.extern.priority_queue import PriorityQueue

from twisted.internet import defer, reactor, error
from twisted.internet.protocol import ProcessProtocol
from twisted.internet.stdio import StandardIO
from twisted.spread import pb, flavors, jelly
from twisted.python import failure
from threading import Lock

import pkg_resources

import PIL
from PIL import PngImagePlugin
import Image
import ImageStat
import md5

if platform.system() == 'windows':
    import win32process

SEEK_SCHEDULED = 'scheduled'
SEEK_DONE = 'done'

THUMBNAIL_DIR = os.path.join(os.path.expanduser("~"), ".thumbnails", 'large')
THUMBNAIL_SIZE = 256

__maintainer__ = 'Alessandro Decina <alessandro@fluendo.com>'
 
supported_metadata_keys = set(['artist', 'album', 'song', 'track', 'thumbnail'])
media_type_keys = set(['uri', 'file_type', 'mime_type'])
thumbnail_keys = set(['uri', 'thumbnail'])
supported_keys = supported_metadata_keys.union(media_type_keys)
supported_schemes = ['file', 'http']

class TimeoutError(MetadataError):
    pass

class ThumbnailError(MetadataError):
    pass

class GstMetadataError(MetadataError):
    pass

class UriError(MetadataError):
    pass

def able_to_handle(supported_schemes, supported_keys, metadata):
    uri = metadata.get('uri')
    if not uri or uri.scheme not in supported_schemes:
        return False

    keys = set(metadata.keys())
    if uri.scheme == 'file' and os.path.isdir(uri.path) and \
            keys != media_type_keys:
        return False

    request_keys = supported_keys.intersection(metadata.keys())
    request_empty_keys = \
            [key for key in request_keys if metadata[key] is None]

    if request_empty_keys:
        return True

    return False 

def get_thumbnail_location(uri):
    thumbnail_filename = md5.new(str(uri)).hexdigest() + ".png"

    return os.path.join(THUMBNAIL_DIR, thumbnail_filename)

class ImageSnapshotMixIn(object):
    __gproperties__ = {
        'width': (gobject.TYPE_UINT, 'width',
                'Snapshot width', 16, (2**32) - 1, THUMBNAIL_SIZE, gobject.PARAM_READWRITE),
        'height': (gobject.TYPE_UINT, 'height',
                'Snapshot height', 16, (2**32) - 1, THUMBNAIL_SIZE, gobject.PARAM_READWRITE),
    }

    def __init__(self):
        super(ImageSnapshotMixIn, self).__init__()
        self._ffmpegcolorspace = gst.element_factory_make('ffmpegcolorspace')
        self._videoscale = gst.element_factory_make('videoscale')
        self._queue0 = gst.element_factory_make('queue')
        self._filter = self.do_create_filter()
        self._encoder = self.do_create_encoder()
        
        self.add(self._ffmpegcolorspace,
                self._queue0, self._videoscale,
                self._filter, self._encoder)

        # bin pads
        # sink ghost pad with target ffmpegcolorspace:sink
        target = self._queue0.get_pad('sink')
        self._sinkpad = gst.GhostPad('sink', target)
        self._sinkpad.set_active(True)
        self._sinkpad.connect('notify::caps', self._sinkpad_caps_cb)
        self.add_pad(self._sinkpad)

        # snapshot ghost pad with target encoder:src
        target = self._encoder.get_pad('src')
        self._srcpad = gst.ghost_pad_new_from_template('src',
                target, self.src_template)
        self._srcpad.set_active(True)
        self.add_pad(self._srcpad)
        # link the elements
        gst.element_link_many(self._queue0,
                self._videoscale, self._ffmpegcolorspace) 
        self._filter.link(self._encoder)

        self._capsfilter = None
        self._src_width = None
        self._src_height = None

    def _sinkpad_caps_cb(self, obj, pspec):
        caps = obj.props.caps

        if caps is None:
            return
        
        try:
            width = caps[0]['width']
            height = caps[0]['height']
        except KeyError:
            return

        if self._src_width is not None:
            self.warning("renegotiation oldw %s oldh %s neww %s newh %s"
                % (self._src_width, self._src_height, width, height))

            return

        self.log('caps changed %s' % caps)

        self._src_width = width
        self._src_height = height

        self._plug_capsfilter()
    
    def _plug_capsfilter(self):
        self._capsfilter = gst.element_factory_make('capsfilter')
    
        width = None
        height = None
        if self._src_height > self._src_width:
            width = self._src_width * THUMBNAIL_SIZE / self._src_height
            height = THUMBNAIL_SIZE
        else:
            height = self._src_height * THUMBNAIL_SIZE / self._src_width
            width = THUMBNAIL_SIZE

        caps = gst.Caps('video/x-raw-rgb,width=(int)%d,height=(int)%d' % (width,
        height))
        self._capsfilter.props.caps = caps
        self._capsfilter.set_state(gst.STATE_PAUSED)
        self.add(self._capsfilter)

        self._ffmpegcolorspace.link(self._capsfilter)
        self._capsfilter.link(self._filter)

    def do_change_state(self, transition):
        self.log("transition %s" % transition)

        if transition == gst.STATE_CHANGE_PAUSED_TO_READY:
            if self._capsfilter is not None:
                self.remove(self._capsfilter)
                self._capsfilter.set_state(gst.STATE_NULL)
                self._capsfilter = None
                self._src_width = None
                self._src_height = None

        return gst.Bin.do_change_state(self, transition)

    def do_get_property(self, pspec):
        return getattr(self, '_' + pspec.name)

    def do_set_property(self, pspec, value):
        setattr(self, '_' + pspec.name, value)

    def do_create_encoder(self):
        raise NotImplementedError()

    def do_frame_inspection(self, pad, buffer):
        return buffer

class PngImageSnapshotBin(ImageSnapshotMixIn, gst.Bin, Loggable):
    sink_template = gst.PadTemplate('sink',
            gst.PAD_SINK, gst.PAD_ALWAYS,
            gst.Caps('video/x-raw-rgb, '
                    'bpp = (int) 24, '
                    'depth = (int) 24'))
    
    src_template = gst.PadTemplate('src',
            gst.PAD_SRC, gst.PAD_ALWAYS,
            gst.Caps('image/png, '
                    'width = [16, 4096], '
                    'height = [16, 4096]'))

    __gproperties__ = ImageSnapshotMixIn.__gproperties__

    __gsttemplates__ = (sink_template, src_template)

    def do_create_filter(self):
        return gst.element_factory_make('identity')

    def do_create_encoder(self):
        element = gst.element_factory_make('pngenc')
        element.props.compression_level = 1

        return element

class PngVideoSnapshotBin(ImageSnapshotMixIn, gst.Bin, Loggable):
    sink_template = gst.PadTemplate('sink',
            gst.PAD_SINK, gst.PAD_ALWAYS,
            gst.Caps('video/x-raw-rgb, '
                    'bpp = (int) 24, '
                    'depth = (int)24'))
    
    src_template = gst.PadTemplate('src',
            gst.PAD_SRC, gst.PAD_ALWAYS,
            gst.Caps('image/png, '
                    'width = [16, 4096], '
                    'height = [16, 4096]'))

    __gproperties__ = ImageSnapshotMixIn.__gproperties__
    
    __gsignals__ = {
        'boring': (gobject.SIGNAL_RUN_LAST,
                gobject.TYPE_NONE, (gst.Buffer.__gtype__,))
    }

    __gsttemplates__ = (sink_template, src_template)

    def do_create_filter(self):
        filter = RawVideoVarianceFilter()
        filter.connect('boring', self._proxy_boring_signal)

        return filter

    def do_create_encoder(self):
        element = gst.element_factory_make('pngenc')
        element.props.compression_level = 1

        return element

    def _proxy_boring_signal(self, obj, buffer):
        self.emit('boring', buffer)

gobject.type_register(PngImageSnapshotBin)
# FIXME: broken on old gst-python version
#gst.element_register(PngImageSnapshotBin, 'pngimagesnapshot')

class RawVideoVarianceFilter(gst.Element):
    BORING_IMAGE_VARIANCE = 2000
    
    __gproperties__ = {
        'use-least-boring': (gobject.TYPE_BOOLEAN, 'use least boring',
                'Use the least boring frame if no interesting frames are found',
                True, gobject.PARAM_READWRITE)
    }

    __gsignals__ = {
        'boring': (gobject.SIGNAL_RUN_LAST,
                gobject.TYPE_NONE, (gst.Buffer.__gtype__,))
    }

    sink_template = gst.PadTemplate('sink',
            gst.PAD_SINK, gst.PAD_ALWAYS,
            gst.Caps('video/x-raw-rgb, '
                    'bpp = (int) 24, '
                    'depth = (int)24'))
    
    src_template = gst.PadTemplate('src',
            gst.PAD_SRC, gst.PAD_ALWAYS,
            gst.Caps('video/x-raw-rgb, '
                    'bpp = (int) 24, '
                    'depth = (int)24'))
    
    __gsttemplates__ = (sink_template, src_template)

    def __init__(self):
        gst.Element.__init__(self)

        self.sinkpad = gst.Pad(self.sink_template)
        self.sinkpad.set_chain_function(self.do_chain)
        self.sinkpad.set_event_function(self.do_sink_event)
        self.add_pad(self.sinkpad)
        
        self.srcpad = gst.Pad(self.src_template)
        self.add_pad(self.srcpad)

        self._use_least_boring = True
        self._max_dropped_frames = 3
        self._reset()

    def _reset(self):
        self._dropped_frames = []
        self._seek_point = None

    def do_get_property(self, pspec):
        name = pspec.name.replace('-', '_')
        return getattr(self, name)

    def do_set_property(self, pspec, value):
        name = pspec.name.replace('-', '_')
        if hasattr(self, name):
            setattr(self, name, value)

    def do_change_state(self, transition):
        if transition == gst.STATE_CHANGE_PAUSED_TO_READY:
            self._reset()

        return gst.Element.do_change_state(self, transition)

    def do_sink_event(self, pad, event):
        if event.type == gst.EVENT_FLUSH_STOP:
            self._seek_point = True

        return pad.event_default(event)

    def do_chain(self, pad, buf):
        if self._seek_point is None:
            # first buffer, we assume it's boring so that we can seek from the
            # app
            self._seek_point = False
            self.emit('boring', buf)
            return gst.FLOW_OK
        
        if self._seek_point is False:
            # we only try to snapshot key frames after a seek
            return gst.FLOW_OK

        # handle this buffer and skip the next until a new seek
        self._seek_point = False

        # FIXME: check that buf is a keyframe
        caps = self.sinkpad.props.caps
        if caps is None:
            self.warning('pad %s has no caps, not doing thumbnail' % pad)

            # we need the caps to get width and height of the frame but
            # something is wrong with upstream elements, we don't do the
            # thumbnail and let the sink preroll. This should never happen but
            # seems to happen sometimes with ffmpeg on windows.
            
            # self.srcpad.push_event(gst.event_new_eos())
            return gst.FLOW_UNEXPECTED

        caps = caps[0]
        width = caps['width']
        height = caps['height']
        
        # FIXME: we use PIL to compute the variance but if this is the only
        # place where we depend on PIL we could compute the variance ourselves I
        # guess :)
        try:
            img = Image.frombuffer("RGB", (width, height),
                    buf, "raw", "RGB", 0, 1)
        except Exception, e:
            self.debug("Invalid frame: %s" % e)
            return gst.FLOW_UNEXPECTED

        stat = ImageStat.Stat(img)
        boring = True
        max_variance = 0
        for i in stat.var:
            max_variance = max(max_variance, i)
            if i > self.BORING_IMAGE_VARIANCE:
                boring = False
                break

        if not boring:
            self.debug('not boring')
            self.srcpad.push(buf)
            self.srcpad.push_event(gst.event_new_eos())
            return gst.FLOW_UNEXPECTED

        if len(self._dropped_frames) == self._max_dropped_frames:
            if self._use_least_boring:
                # sort by variance
                self.debug('least boring')
                self._dropped_frames.sort(key=lambda item: item[1])
                self.srcpad.push(self._dropped_frames[0][0])
                self.srcpad.push_event(gst.event_new_eos())
                return gst.FLOW_UNEXPECTED

            # FIXME: post an error here?
            return gst.FLOW_ERROR
        
        self.emit('boring', buf)
        self._dropped_frames.append((buf, max_variance))
        return gst.FLOW_OK

gobject.type_register(RawVideoVarianceFilter)

class GstMetadataPipeline(Loggable):
    reuse_elements = True
    timeout = 2
    thumb_timeout = 1
    
    def __init__(self):
        super(GstMetadataPipeline, self).__init__()
        self._pipeline = None
        self._src = None
        self._decodebin = None
        self._ffmpegcolorspace = None
        self._imgthumbbin = None
        self._videothumbbin = None
        self._plugged_elements = []
        self._frame_locations = [1.0 / 3.0, 2.0 / 3.0, 0.1, 0.9, 0.5]

        # other instance variables that need to be reset for each new metadata
        # request are set directly in _reset()

    def clean(self):
        self._clean_pipeline(finalize=True)

        if self._timeout_call is not None:
            self._timeout_call.cancel()
            self._timeout_call = None

        if self._seek_call is not None:
            self._seek_call.cancel()
            self._seek_call = None

    def initialize(self):
        self._reset()

    def _clean_pipeline(self, finalize=False):
        # reset the pipeline to READY
        if self._pipeline is not None:
            self._bus.set_flushing(True)
            self._pipeline.set_state(gst.STATE_READY)

        if self._src is not None:
            self._pipeline.remove(self._src)
            self._src.set_state(gst.STATE_NULL)
            self._src = None

        if not self.reuse_elements or finalize:
            # destroy the pipeline
            if self._pipeline is not None:
                self._bus.set_flushing(True)
                self._pipeline.set_state(gst.STATE_NULL)
                self._pipeline = None
                self._decodebin = None
                self._ffmpegcolorspace = None
                self._imgthumbbin = None
                self._videothumbbin = None
                self._plugged_elements = []
        else:
            # reusing decodebin leads to problems
            if self._decodebin is not None:
                self._typefind.unlink(self._decodebin)
                self._decodebin.set_state(gst.STATE_NULL)
                self._pipeline.remove(self._decodebin)
                self._decodebin = None

            # remove dynamically plugged elements
            for element in self._plugged_elements:
                self._pipeline.remove(element)
                element.set_state(gst.STATE_NULL)
            self._plugged_elements = []

    def _build_pipeline(self):
        self._pipeline = gst.Pipeline()
        self._bus = self._pipeline.get_bus()
        self._bus.add_signal_watch()
        self._bus.connect('message::application',
                self._bus_message_application_cb)
        self._bus.connect('message::error', self._bus_message_error_cb)
        self._bus.connect('message::eos', self._bus_message_eos_cb)
        self._bus.connect('message::tag', self._bus_message_tag_cb)
        self._bus.connect('message::state-changed',
                self._bus_message_state_changed_cb)
        self._src = None
        self._typefind = gst.element_factory_make('typefind')
        self._typefind.connect('have-type', self._typefind_have_type_cb)
        pad = self._typefind.get_pad('src')
        self._pipeline.add(self._typefind)
        
        self._pipeline.set_state(gst.STATE_READY)

    def _reset(self):
        # NOTE: we call gst_element_set_state so we MUST NOT be called from the
        # streaming thread

        # destroy the current pipeline if reuse_elements == False, otherwise
        # clean it so that it can be reused
        self._clean_pipeline()
        if self._pipeline is None:
            # we're either being called from initialize() or
            # self.reuse_elements == False
            self._build_pipeline()

        # the metadata dictionary of the current request
        self._req_metadata = None
        # the uri value in the metadata dictionary
        self._req_uri = None
        # the deferred that we callback when we finish loading stuff in
        # self._req_metadata
        self._req_defer = None

        # the caps as given by the typefind::have-type signal
        self._typefind_caps = None
        self._typefind_file_type = None
        self._typefind_mime_type = None

        # the video/audio/image caps that we get from decodebin pads when
        # we plug decodebin
        self._video_caps = None
        self._audio_caps = None
        self._image_caps = None

        # the taglist containing all the tags for the stream
        self._tags = gst.TagList()

        # the duration of the current stream, used to seek when doing a
        # thumbnail 
        self._duration = None
        self._seek_status = None
        self._seek_location_index = 0
        self._seek_call = None

        self._timeout_call = None

        # timestamps used for logging purposes
        self._start_timestamp = 0
        self._end_timestamp = 0

    def _bus_message_error_cb(self, bus, message):
        gerror, debug = message.parse_error() 
        if self._typefind_file_type is not None or \
                self._video_caps is not None or \
                self._audio_caps is not None or \
                self._image_caps is not None:
            # we got an error going to PAUSED but we still can report the info
            # that we got from have_type_cb
            self.debug('error going to paused %s: %s', gerror.message, debug)
            self._clean_thumbnail()
            self._done()
        else:
            self._failed(GstMetadataError('error'
                    ' domain: %r code: %r message: %s debug: %s' % 
                    (gerror.domain, gerror.code, gerror.message, debug)))

    def _bus_message_application_cb(self, bus, message):
        if message.structure.get_name() == 'metadata-done':
            self._done()
            return

    def _bus_message_eos_cb(self, bus, message):
        self.log('got EOS')

        self._done()

    def _bus_message_tag_cb(self, bus, message):
        taglist = message.parse_tag()
        self._tags = self._tags.merge(taglist, gst.TAG_MERGE_APPEND)
   
    def _bus_message_state_changed_cb(self, bus, message):
        if message.src is not self._pipeline:
            return

        prev, current, pending = message.parse_state_changed()
        if prev == gst.STATE_READY and current == gst.STATE_PAUSED and \
                self._decodebin is not None and \
                self._decodebin.get_pad('sink').is_linked():
            self.debug('reached PAUSED')

            if self._video_caps is None and self._image_caps is None and \
                self._typefind_file_type not in ('video', 'image'):
                # we have the tags at this point
                self._done()

    def _typefind_have_type_cb(self, typefind, probability, caps):
        self.debug('have type %s' % caps)

        # self._typefind_caps = caps is broken, bug in the bindings
        # FIXME: fix the bug and change this asap
        self._typefind_caps = caps.copy()
        gst_mime_type = self._typefind_mime_type = self._typefind_caps[0].get_name()
        file_type = self._typefind_file_type = gst_mime_type.split('/')[0]

        # NB: id3 tags most of the time are used with mp3 (even if it isn't
        # uncommon to find them with AIFF or WAV). Given that mp3 is by far the
        # most used audio format at the moment we make the common case fast here
        # by assuming that the file_type is audio. By doing this we also set the
        # mime_type to application/x-id3, but this doesn't matter at the moment
        # since we don't use the mime_type anywhere.
        if gst_mime_type == 'application/x-id3':
            file_type = self._typefind_file_type = 'audio'
        elif gst_mime_type == 'audio/x-m4a':
            # FIXME: see http://bugzilla.gnome.org/show_bug.cgi?id=340375 and use this
            # hack until we write our typefinder for this
            file_type = None

        req_keys = set(self._req_metadata.keys())
        if (req_keys == media_type_keys and \
                file_type in ('video', 'audio', 'image'))or \
                (file_type in ('video', 'image') and \
                (not 'thumbnail' in req_keys or self._have_thumbnail())):
            self.debug('got media_type for %s, NOT going to paused',
                    self._req_uri)
            # we are in the streaming thread so we post a message on the bus
            # here and when we read it from the main thread we call _done()
            structure = gst.Structure('metadata-done')
            self._bus.post(gst.message_new_application(self._pipeline, structure))
            return

        # we need tags and/or a thumbnail
        self.debug('we need to go to PAUSED, plugging decodebin '
                '(file_type: %s, have_thumbnail: %s)', file_type,
                self._have_thumbnail())
        self._plug_decodebin()

    def _plug_decodebin(self):
        if self._decodebin is None:
            self._decodebin = gst.element_factory_make('decodebin')
            self._decodebin.connect('new-decoded-pad',
                    self._decodebin_new_decoded_pad_cb)
            self._decodebin.connect('unknown-type',
                    self._decodebin_unknown_type_cb)
            self._pipeline.add(self._decodebin)
        
        self._typefind.link(self._decodebin)
        pad = self._typefind.get_pad('src')
        self._decodebin.set_state(gst.STATE_PAUSED)
        
    def _check_thumbnail_directory(self): 
        if not os.path.exists(THUMBNAIL_DIR):
            try:
                os.makedirs(THUMBNAIL_DIR, 0700)
            except OSError, e:
                msg = "Could not make directory %r: %s. Thumbnail not saved." % (directory, e)
                self.warning(msg)
                raise ThumbnailError(self._req_uri, msg)
 
    def _boring_cb(self, obj, buffer):
        self.debug('boring buffer')
        self._seek_next_thumbnail_location()

    def _plug_video_thumbnailbin(self, video_pad):
        self.debug('pluging video thumbbin')

        self._check_thumbnail_directory()

        if self._videothumbbin is None:
            self._videothumbbin = PngVideoSnapshotBin()
            self._videothumbbin.connect('boring', self._boring_cb)
            self._pipeline.add(self._videothumbbin)

        thumbbin = self._videothumbbin

        filesink = gst.element_factory_make('filesink')
        self._pipeline.add(filesink) 
        filesink.props.location = get_thumbnail_location(self._req_uri)

        video_pad.link(thumbbin.get_pad('sink'))
        thumbbin.get_pad('src').link(filesink.get_pad('sink'))

        thumbbin.set_state(gst.STATE_PAUSED)
        filesink.set_state(gst.STATE_PAUSED)

        self._plugged_elements.append(filesink)
        self.debug('video thumbbin plugged')

    def _plug_image_thumbnailbin(self, image_pad):
        self.debug('plugging image thumbbin')
        
        self._check_thumbnail_directory()
        
        if self._imgthumbbin is None:
        # we can't register the element on old gst-python versions so we can't
        # use gst_element_factory_make
#            self._imgthumbbin = gst.element_factory_make('pngimagesnapshot')
            self._imgthumbbin = PngImageSnapshotBin()
            self._pipeline.add(self._imgthumbbin)
        thumbbin = self._imgthumbbin

        filesink = gst.element_factory_make('filesink')
        self._pipeline.add(filesink)
        filesink.props.location = get_thumbnail_location(self._req_uri)

        image_pad.link(thumbbin.get_pad('sink'))
        thumbbin.get_pad('src').link(filesink.get_pad('sink'))

        thumbbin.set_state(gst.STATE_PAUSED)
        filesink.set_state(gst.STATE_PAUSED)

        self._plugged_elements.append(filesink)
        self.debug('image thumbbin plugged')

    def _have_thumbnail(self):
        location = get_thumbnail_location(self._req_uri)
        if os.path.exists(location):
            stat = os.stat(location)
            if stat.st_size != 0:
                return True

        return False

    def _find_decoder(self, pad):
        target = pad.get_target()
        element = target.get_parent()
        klass = element.get_factory().get_klass()
        if 'Decoder' in klass:
            return element
        return None

    def _get_type_from_decoder(self, decoder):
        klass = decoder.get_factory().get_klass()
        parts = klass.split('/', 2)
        if len(parts) != 3:
            return None

        return parts[2].lower()

    def _seek_next_thumbnail_location(self):
        self._seek_status = SEEK_SCHEDULED

        self._seek_call = \
            reactor.callLater(0, self._seek_next_thumbnail_location_real)

    def _seek_next_thumbnail_location_real(self):
        self._seek_call = None
        self._seek_status = SEEK_DONE

        if self._duration is None:
            # first seek, get the duration
            try:
                self._duration, format = self._pipeline.query_duration(gst.FORMAT_TIME)
            except gst.QueryError, e:
                self.debug('duration query failed: %s', e)
                
                return

            if self._duration == -1:
                self.debug('invalid duration, not seeking')
                return
           
            self.debug('stream duration %s' % self._duration)
        
        if self._seek_location_index == len(self._frame_locations):
            self.debug('no more seek locations')
            return self._failed(ThumbnailError('no more seek locations'))

        location = self._frame_locations[self._seek_location_index]
        self.debug('seek to location %d, time %s duration %s' %
                (self._seek_location_index,
                gst.TIME_ARGS(int(location * self._duration)),
                gst.TIME_ARGS(self._duration)))
        self._seek_location_index += 1
        
        res = self._pipeline.seek(1.0, gst.FORMAT_TIME,
                gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT,
                gst.SEEK_TYPE_SET, int(location * self._duration),
                gst.SEEK_TYPE_NONE, 0)
        
        self.debug('seek done res %s' % res)

    def _close_pad(self, pad):
        queue = gst.element_factory_make('queue')
        # set the queue leaky so that if we take some time to do the thumbnail
        # the demuxer doesnt' block on full queues
        queue.props.leaky = 1
        sink = gst.element_factory_make('fakesink')
        self._pipeline.add(queue, sink)
        # add sink before queue so when we iterate over the elements to clean
        # them we clean the sink first and unblock the queue if it's blocked
        # prerolling
        self._plugged_elements.append(sink)
        self._plugged_elements.append(queue)
        pad.link(queue.get_pad('sink'))
        queue.link(sink)
        queue.set_state(gst.STATE_PAUSED)
        sink.set_state(gst.STATE_PAUSED)

    def _get_pad_type(self, pad):
        decoder = self._find_decoder(pad)
        if decoder:
            return self._get_type_from_decoder(decoder)
        
        return pad.get_caps()[0].get_name().split('/', 1)[0]

    def _get_pad_caps(self, pad):
        decoder = self._find_decoder(pad)
        if decoder:
            return decoder.get_pad('sink').get_caps()

        return pad.get_caps()

    def _decodebin_new_decoded_pad_cb(self, decodebin, pad, is_last):
        self.debug('new decoded pad %s, caps %s, is_last %s' % (pad,
                pad.get_caps(), is_last))

        typ = self._get_pad_type(pad)
        caps = self._get_pad_caps(pad)

        if typ == 'audio':
            if self._audio_caps is None:
                self._audio_caps = caps
        elif typ == 'video':
            if self._video_caps is None:
                self._video_caps = caps
                # do a thumbnail of the first video track
                self._plug_video_thumbnailbin(pad)
        elif typ == 'image':
            if self._image_caps is None:
                self._image_caps = caps
                self._plug_image_thumbnailbin(pad)

        if not pad.is_linked():
            self._close_pad(pad)
    
    def _decodebin_unknown_type_cb(self, decodebin, pad, caps):
        self.debug('unknown pad %s, caps %s' % (pad, caps))

    def _plug_src(self, uri):
        src = gst.element_make_from_uri(gst.URI_SRC, str(uri))
        # FIXME: workaround for jpegdec that does a gst_buffer_join for each
        # gst_pad_chain.
        src.props.blocksize = 3 * 1024 * 1024

        return src

    def get_metadata(self, requested_metadata):
        assert self._timeout_call is None

        self._req_metadata = requested_metadata
        self._req_uri = requested_metadata['uri']
        self._req_defer = defer.Deferred()

        self.debug('getting metadata %s' % self._req_metadata)

        self._start_timestamp = time.time()

        self._src = self._plug_src(self._req_uri)
        self._pipeline.add(self._src)
        self._src.link(self._typefind)

        self._timeout_call = reactor.callLater(self.timeout, self._timeout)

        # reset the bus in case this is not the first request  
        self._bus.set_flushing(False)
        self._pipeline.set_state(gst.STATE_PLAYING)

        return self._req_defer

    def _get_media_type_from_caps(self, caps):
        res = {}
        mime_type = caps[0].get_name()
        file_type = mime_type.split('/', 1)[0]

        return {'file_type': file_type, 'mime_type': mime_type}

    def _done(self):
        if not self._timeout_call.called:
            self._timeout_call.cancel()

        # we can't check self._seek_call.called here because we don't know if we
        # scheduled a seek call at all
        if self._seek_call is not None:
            self._seek_call.cancel()
            self._seek_call = None

        self._end_timestamp = time.time()

        metadata = self._req_metadata
        metadata_defer = self._req_defer

        available_metadata = {}
        for caps in (self._video_caps, self._audio_caps,
                self._image_caps):
            if caps is not None:
                available_metadata.update(self._get_media_type_from_caps(caps))
                break

        # fallback to typefind caps
        if available_metadata.get('file_type') is None:
            available_metadata['file_type'] = self._typefind_file_type
            available_metadata['mime_type'] = self._typefind_mime_type

        if available_metadata['file_type'] in ('video', 'image') and \
            self._have_thumbnail():
            available_metadata['thumbnail'] = \
                    get_thumbnail_location(self._req_uri)

        tags = self._tags

        try:
            del tags['extended-comment']
        except KeyError:
            pass

        tag_keys = tags.keys()
        for gst_key, elisa_key in (('track-number', 'track'),
                    ('title', 'song')):
            try:
                available_metadata[elisa_key] = tags[gst_key]
            except KeyError:
                pass
            
        for key in tag_keys:
            value = tags[key]
            # FIXME: this was an old assumption, let's keep it until we update
            # all the old code
            if isinstance(value, list):
                try:
                    value = value[0]
                except IndexError:
                    continue

            available_metadata[key] = value
        
        for key, value in available_metadata.iteritems():
            try:
                if metadata[key] is None:
                    metadata[key] = value
            except KeyError:
                pass

        self.debug('finished getting metadata %s, elapsed time %s' % 
                (metadata, self._end_timestamp - self._start_timestamp))
        
        self._reset()
        metadata_defer.callback(metadata)

    def _timeout(self, thumb_timeout=False):
        self.debug('timeout thumb %s video caps %s',
                thumb_timeout, self._video_caps)

        if not thumb_timeout and (self._typefind_file_type == 'video' or
                self._video_caps is not None):
            # give some more time to the pipline if we are trying to make a
            # thumbnail
            self._timeout_call = \
                reactor.callLater(self.thumb_timeout, self._timeout, True)
        else:
            self._clean_thumbnail()

            keys = set(self._req_metadata.keys())
            if keys != thumbnail_keys and \
                    (self._typefind_file_type is not None or \
                    self._video_caps is not None or \
                    self._audio_caps is not None or \
                    self._image_caps is not None):
                # timeout while going to paused. This can happen on really slow
                # machines while doing the thumbnail. Even if we didn't do the
                # thumbnail, we have some clue about the media type here.
                self._done()
            else:
                self._failed(TimeoutError('timeout'))

    def _clean_thumbnail(self):
        # if we fail doing a thumbnail we need to remove the file
        if self._imgthumbbin is not None or self._videothumbbin is not None:
            location = get_thumbnail_location(self._req_uri)
            try:
                os.unlink(location)
            except OSError:
                pass

    def _failed(self, error):
        # cancel delayed calls
        if not self._timeout_call.called:
            self._timeout_call.cancel()

        if self._seek_call is not None:
            self._seek_call.cancel()
            self._seek_call = None
 
        self._end_timestamp = time.time()

        metadata = self._req_metadata
        metadata_defer = self._req_defer
        self.debug('error getting metadata %s, error: %s, '
                'elapsed time: %s, timeout %s' % (metadata, error,
                self._end_timestamp - self._start_timestamp,
                self._timeout_call.called))

        self._clean_thumbnail()

        self._reset()

        metadata_defer.errback(error)

class Request(object):
    def __init__(self, metadata, dfr):
        self.metadata = metadata
        self.defer = dfr

class GstMetadata(MetadataProvider):
    def __init__(self, next_interval=0.01):
        super(GstMetadata, self).__init__()
        self._pipeline = GstMetadataPipeline()
        self._uri_cache = {}
        self._requests = PriorityQueue(None)
        self._running = False
        self._process_next_call = None
        self._process_next_interval = next_interval
        self._failed_uris = {}
    
    def clean(self):
        if self._process_next_call is not None:
            self._process_next_call.cancel()
            self._process_next_call = None
        self._pipeline.clean()

    def initialize(self):
        # load the plugins so we can override the ranks... yuck!
        for plugin_name in ('typefindfunctions', 'png', 'jpeg', 'ffmpeg', 'soup'):
            gst.plugin_load_by_name(plugin_name)

        registry = gst.registry_get_default()
        typefinders = gst.type_find_factory_get_list()
        for typefinder in typefinders:
            name = typefinder.get_name()

            if name in ('image/jpeg', 'image/png'):
                # override the rank of the jpeg and png typefinders in order to speed up
                # typefinding when browsing Pictures
                typefinder.set_rank(gst.RANK_PRIMARY + 1)
            
            elif name in ('fftype_wsaud',):
                # we don't really want to typefind .py files as audio files...
                registry.remove_feature(typefinder)
        
        # raise the rank of souphttpsrc as it's better than gnomevfs for http
        soupfactory = gst.element_factory_find('souphttpsrc')
        if soupfactory:
            soupfactory.set_rank(gst.RANK_PRIMARY + 1)

        try:
            self._pipeline.initialize()
        except Exception, exc:
            msg = "Could not initialize the Pipeline: %s" % exc
            raise InitializeFailure(msg)

    def _reset(self):
        # timestamps used for logging purposes
        pass

    def get_rank(self):
        return 10

    def able_to_handle(self, metadata):
        return able_to_handle(supported_schemes,
                supported_keys, metadata)

    def get_metadata(self, metadata, low_priority=False):

        blacklist_value = self._failed_uris.get(unicode(metadata['uri']))

        keys = set(metadata.keys())
        
        if keys == thumbnail_keys:
            if blacklist_value == 'thumbnail-failed':
                return defer.fail(UriError())

            location = get_thumbnail_location(metadata['uri'])
            if os.path.exists(location):
                stat = os.stat(location)
                if stat.st_size != 0:
                    self.debug('thumbnail cached %s' % location)
                    metadata['thumbnail'] = location
                    return defer.succeed(metadata)

        if keys == media_type_keys and blacklist_value == 'media-type-failed':
            return defer.fail(UriError())

        dfr = defer.Deferred()

        priority = 10
        if low_priority:
            priority += 10

        self._requests.put((Request(metadata, dfr), priority))

        if not self._running:
            self._running = True
            self._process_next()

        return dfr

    def _process_next(self):
        if self._process_next_call is not None:
            return

        self._process_next_call = \
                reactor.callLater(self._process_next_interval,
                        self._process_next_real)

    def _process_next_real(self):
        self._process_next_call = None

        if self._requests.empty():
            self.debug('metadata queue empty')
            self._running = False
            return
        
        request = self._requests.get()
        self.debug('getting metadata %s, queue length %d' %
                (request.metadata, self._requests.qsize()))

        metadata_defer = None
        uri = request.metadata['uri']
        # FIXME uri.path should probably be unquoted inside MediaUri
        if uri.scheme == 'file' and os.path.isdir(unquote(uri.path)):
            request.metadata['file_type'] = 'directory'
            metadata_defer = defer.succeed(request.metadata)
        else:
            try:
                metadata_defer = self._pipeline.get_metadata(request.metadata)
            except:
                error = failure.Failure()
                metadata_defer = defer.fail(error)

        metadata_defer.addCallbacks(self._done, self._failed,
                callbackArgs=(request,), errbackArgs=(request,))

    def _done(self, metadata, request):
        self._process_next()
        # NOTE: this can't be called from the streaming thread
        self._reset()

        request.defer.callback(metadata)

    def _failed(self, failure, request):
        self._process_next()
        self._reset()

        keys = set(request.metadata)
        if keys == thumbnail_keys:
            blacklist_value = 'thumbnail-failed'
        else:
            blacklist_value = 'media-type-failed'
        
        self._failed_uris[unicode(request.metadata['uri'])] = blacklist_value

        request.defer.errback(failure)

class GstMetadataFast(MetadataProvider):
    def __init__(self):
        super(GstMetadataFast, self).__init__()
        self.media_type_provider = GstMetadata(0.05)
        self.metadata_provider = GstMetadata()

    def initialize(self):
        self.media_type_provider.initialize()
        self.metadata_provider.initialize()
    
    def clean(self):
        self.media_type_provider.clean()
        self.metadata_provider.clean()

    def get_rank(self):
        return 10

    def able_to_handle(self, metadata):
        return able_to_handle(supported_schemes,
                supported_keys, metadata)

    def get_metadata(self, metadata, low_priority=False):
        req_keys = set(metadata.keys())
        if req_keys == media_type_keys:
            return self.media_type_provider.get_metadata(metadata, low_priority)
        
        return self.metadata_provider.get_metadata(metadata, low_priority)

class MetadataClientProcessProtocol(ProcessProtocol):
    def __init__(self):
        self.broker = pb.Broker(True, jelly.DummySecurityOptions())
        
    def makeConnection(self, transport):
        self.broker.makeConnection(transport)
        ProcessProtocol.makeConnection(self, transport)

    def connectionMade(self):
        ProcessProtocol.connectionMade(self)
        self.broker.connectionMade()

    def outReceived(self, data):
        self.broker.dataReceived(data)
    
    def errReceived(self, data):
        self.factory.debug('Remote output:\n' + data)

    def processEnded(self, reason):
        ProcessProtocol.processEnded(self, reason)
        self.broker.connectionLost(reason)
        self.factory.processEnded(reason)

class MetadataClientProcessLauncher(Loggable, pb.PBClientFactory):
    max_retries = 3
    server_script = \
            pkg_resources.resource_filename(__module__,
                                            'gst_metadata_runner.py')

    log_category = 'gst_metadata_client_process_launcher'

    def __init__(self, *args, **kw):
        Loggable.__init__(self, *args, **kw)
        pb.PBClientFactory.__init__(self, *args, **kw)
        Loggable.__init__(self)

        self.protocol = MetadataClientProcessProtocol
        self.path = os.path.split(sys.modules['elisa'].__path__[0])[0]
        self.env = dict(os.environ)
        self.env['PYTHONPATH'] = \
                os.pathsep.join(sys.path + [self.path])
        if hasattr(sys, 'frozen'):
            self.args = [sys.executable, 'config_file', self.server_script]
        else:
            self.args = [sys.executable, '-i', self.server_script]
        self.process = None
        self.retries = 0
        self.start_defer = None
        self.stop_defer = None

    def buildProtocol(self, addr):
        protocol = pb.PBClientFactory.buildProtocol(self, addr)
        protocol.broker.factory = self

        return protocol

    def startProcess(self):
        def get_root_object_done(component):
            self.debug('initializing remote component')
            dfr = component.callRemote('initialize')
            dfr.addCallbacks(initialize_done, initialize_failure,
                callbackArgs=(component,), errbackArgs=(component,))

            return dfr

        def initialize_done(result, component):
            self.info('metadata server started')
            start_defer = self.start_defer
            self.start_defer = None
            res = start_defer.callback(component)

            return res

        def initialize_failure(failure, component):
            self.warning('failed to initialize remote component: %s', failure)
            start_defer = self.start_defer
            self.start_defer = None
            res = start_defer.errback(failure)

            return res

        self.info('starting metadata server')
        
        assert self.start_defer is None
        # start_defer will be called back after initialize() has been called on
        # the remote object
        self.start_defer = defer.Deferred()

        protocol = self.buildProtocol(None)
        self.process = reactor.spawnProcess(protocol,
                sys.executable, self.args, env=self.env)
        
        if platform.system() == 'Windows':
            import win32process
            win32process.SetPriorityClass(self.process.hProcess,
                    win32process.IDLE_PRIORITY_CLASS)

        self.retries = 0
        self.debug('metadata process started')

        # get the remote object and call initialize() on it
        dfr = self.getRootObject()
        dfr.addCallback(get_root_object_done)

        return self.start_defer

    def stopProcess(self):
        assert self.stop_defer is None
        
        self.info('stopping metadata server')

        if self.process is None:
            return defer.succeed(None)

        def get_component_done(component):
            self.debug('cleaning remote component')
            return component.callRemote('clean')

        def clean_done(component):
            self.debug('clean done')
            self.process.loseConnection()

            return component

        # stop_defer will be fired in processEnded after we terminate the child
        # process
        self.stop_defer = defer.Deferred()

        # get the component and call clean() on it
        dfr = self.get_component()
        # FIXME: handle errback
        dfr.addCallback(get_component_done)
        dfr.addCallback(clean_done)

        return self.stop_defer

    def get_component(self):
        if self.process is None:
            return defer.fail(Exception('process not started'))

        if self.start_defer is not None:
            # the remote object is being started
            return self.start_defer
        else:
            return self.getRootObject()

    # FIXME: this should be called by BaseConnector 
    #def clientConnectionLost(self, connector, reason, reconnecting):

    # MetadataClientProcessProtocol callback
    def processEnded(self, reason):
        self.process = None

        self.log('process terminated %s', reason.getErrorMessage())

#        if reason.type == error.ProcessDone:
        # FIXME: for some reason sometime we get ProcessDone and sometime we get
        # ProcessTerminated when self.process.loseConnection() is called in
        # stopProcess(). The problem is that ProcessTerminated is also used for
        # segfault so we can't check for that here.
        if self.stop_defer is not None:
            self.info('metadata server stopped')
            stop_defer = self.stop_defer
            self.stop_defer = None
            self._failAll(reason)
            if stop_defer:
                stop_defer.callback(None)
            return

        if self.retries == self.max_retries:
            self.info('%d tries done, giving up' % self.retries)
            # FIXME: this should be called from clientConnectionLost but i don't
            # think that that's called at all with the process api...
            self._failAll(reason)
            
            return

        self.retries += 1
        self.startProcess()

class GstMetadataClient(MetadataProvider):
    # FIXME see able_to_handle 
    def __init__(self):
        super(GstMetadataClient, self).__init__()
        self.launcher = MetadataClientProcessLauncher()

    def initialize(self):
        def start_process_done(component):
            return self

        dfr = self.launcher.startProcess()
        dfr.addCallback(start_process_done)

        return dfr

    def clean(self):
        def stop_process_done(result):
            return super(GstMetadataClient, self).clean()

        dfr = self.launcher.stopProcess()
        dfr.addCallback(stop_process_done)
        
        return dfr

    def get_rank(self):
        return 10

    def able_to_handle(self, metadata):
        return able_to_handle(supported_schemes,
                supported_keys, metadata)

    def get_metadata(self, metadata, low_priority=False):
        def get_metadata_done(remote_metadata):
            # we get a normal dictionary as the result of a remote get_metadata
            # call but we need to return the metadata dictionary that was passed
            # as argument
            for key, value in remote_metadata.iteritems():
                if value is None:
                    continue
                
                metadata[key] = value

            return metadata

        def got_root(root):
            # FIXME: DictObservable can't be marshalled correctly over perspective
            # broker
            dic = dict(metadata)
            dfr = root.callRemote('get_metadata', dic, low_priority)
            dfr.addCallback(get_metadata_done)

            return dfr

        root_dfr = self.launcher.get_component()
        root_dfr.addCallback(got_root)

        return root_dfr
