# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 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 Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.
#
# Authors: Alessandro Decina <alessandro@fluendo.com>

import os
import platform

import gobject
gobject.threads_init()
import gst

import PIL
from PIL import PngImagePlugin
import Image
import ImageStat

try:
    from hashlib import md5
except ImportError:
    # hashlib is new in Python 2.5
    from md5 import md5

from elisa.core.log import Loggable
from elisa.core.utils import locale_helper
from elisa.core.media_uri import MediaUri, unquote
from elisa.core.components.metadata_provider import MetadataError
from elisa.plugins.gstreamer.changestride import ChangeStride

# expanduser() is broken on windows if we give it a unicode object, so we need
# to do this
_user_path = \
    os.path.expanduser("~").decode(locale_helper.filesystem_encoding())
THUMBNAIL_DIR = os.path.join(_user_path,
                             u".thumbnails",
                             u"large")
THUMBNAIL_SIZE = 256

class ThumbnailError(MetadataError):
    pass

def get_thumbnail_location(uri):
    thumbnail_filename = u"%s.png" % md5(uri).hexdigest()
    location = os.path.join(THUMBNAIL_DIR, thumbnail_filename)
    return location

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),
    }

    is_video = False

    def __init__(self):
        super(ImageSnapshotMixIn, self).__init__()
        self._queue = gst.element_factory_make('queue',
                'queue-thumb')
        self._ffmpegcolorspace = gst.element_factory_make('ffmpegcolorspace',
                'ffmpegcolorspace-thumb')
        self._videoscale = gst.element_factory_make('videoscale',
                'videoscale-thumb')
        self._capsfilter = gst.element_factory_make('capsfilter',
                'capsfilter-thumb')
        self._filter = self.do_create_filter()
        self._encoder = self.do_create_encoder()
       
        # set the caps in order to scale
        caps = gst.Caps('video/x-raw-rgb, '
            'height=%s, pixel-aspect-ratio=(fraction)1/1' % THUMBNAIL_SIZE)
        self._capsfilter.props.caps = caps

        self.add(self._queue, self._videoscale, 
                self._ffmpegcolorspace, self._capsfilter, self._filter, 
                self._encoder)

        if self.is_video and platform.system() == 'Windows':
            self._changestride = ChangeStride()
            self.add(self._changestride)
        else:
            self._changestride = None

        # link the elements
        gst.element_link_many(self._ffmpegcolorspace, self._videoscale, 
                self._capsfilter, self._filter, self._encoder)

        if self._changestride is None:
            self._queue.link(self._ffmpegcolorspace)
        else:
            gst.element_link_many(self._queue, self._changestride,
                    self._ffmpegcolorspace)

        # bin pads
        # sink ghost pad with target ffmpegcolorspace:sink
        target = self._queue.get_pad('sink')
        self._sinkpad = gst.GhostPad('sink', target)
        self._sinkpad.set_active(True)
        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)

    def do_change_state(self, transition):
        self.log("transition %s" % transition)
        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 __init__(self):
        gst.Bin.__init__(self)
        Loggable.__init__(self)
        ImageSnapshotMixIn.__init__(self)

    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
        element.props.snapshot = True

        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)

    is_video = True
 
    def __init__(self):
        gst.Bin.__init__(self)
        Loggable.__init__(self)
        ImageSnapshotMixIn.__init__(self)

    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
        element.props.snapshot = True

        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)

