import numpy as np
import os
from skimage.io import ImageCollection

try:
    import pygst
    pygst.require("0.10")
    import gst
    import gobject
    gobject.threads_init()
    from gst.extend.discoverer import Discoverer
    gstreamer_available = True
except ImportError:
    gstreamer_available = False

try:
    import cv
    opencv_available = True
except ImportError:
    opencv_available = False


class CvVideo(object):
    """
    Opencv-based video loader.

    Parameters
    ----------
    source : str
        Media location URI. Video file path or http address of IP camera.
    size: tuple, optional
        Size of returned array.
    """
    def __init__(self, source=None, size=None, backend=None):
        if not opencv_available:
            raise ImportError("Opencv 2.0+ required")
        self.source = source
        self.capture = cv.CreateFileCapture(self.source)
        self.size = size

    def get(self):
        """
        Retrieve a video frame as a numpy array.

        Returns
        -------
        output : array (image)
            Retrieved image.
        """
        img = cv.QueryFrame(self.capture)
        if not self.size:
            self.size = cv.GetSize(img)
        img_mat = np.empty((self.size[1], self.size[0], 3), dtype=np.uint8)
        if cv.GetSize(img) == self.size:
            cv.Copy(img, cv.fromarray(img_mat))
        else:
            cv.Resize(img, cv.fromarray(img_mat))
        # opencv stores images in BGR format
        cv.CvtColor(cv.fromarray(img_mat), cv.fromarray(img_mat),
                    cv.CV_BGR2RGB)
        return img_mat

    def seek_frame(self, frame_number):
        """
        Seek to specified frame in video.

        Parameters
        ----------
        frame_number : int
            Frame position
        """
        cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_FRAMES,
                              frame_number)

    def seek_time(self, milliseconds):
        """
        Seek to specified time in video.

        Parameters
        ----------
        milliseconds : int
            Time position
        """
        cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_MSEC,
                              milliseconds)

    def frame_count(self):
        """
        Returns frame count of video.

        Returns
        -------
        output : int
            Frame count.
        """
        return cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FRAME_COUNT)

    def duration(self):
        """
        Returns time length of video in milliseconds.

        Returns
        -------
        output : int
            Time length [ms].
        """
        return cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FPS) * \
            cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FRAME_COUNT)


class GstVideo(object):
    """
    GStreamer-based video loader.

    Parameters
    ----------
    source : str
        Media location URI. Video file path or http address of IP camera.
    size: tuple, optional
        Size of returned array.
    sync: bool, optional (default False)
        Frames are extracted per frame or per time basis. If enabled the video
        time step continues onward according to the play rate.  Useful for ip
        cameras and other real time video feeds.
    """
    def __init__(self, source=None, size=None, sync=False):
        if not gstreamer_available:
            raise ImportError("GStreamer Python bindings 0.10+ required")
        self.source = source
        self.size = size
        self.video_length = 0
        self.video_rate = 0
        # extract video size
        if not size:
            gobject.idle_add(self._discover_one)
            self.mainloop = gobject.MainLoop()
            self.mainloop.run()
        if not self.size:
            self.size = (640, 480)
        if os.path.exists(self.source):
            self.source = "file://" + self.source
        self._create_main_pipeline(self.source, self.size, sync)

    def _discover_one(self):
        """
        Callback to start media discovery process, used to retrieve video parameters.
        """
        discoverer = Discoverer(self.source)
        discoverer.connect('discovered', self._discovered)
        discoverer.discover()
        return False

    def _discovered(self, d, is_media):
        """
        Callback to on media discovery result.
        """
        if is_media:
            self.size = (d.videowidth, d.videoheight)
            self.video_length = d.videolength / gst.MSECOND
            self.video_rate = d.videorate.num
        self.mainloop.quit()
        return False

    def _create_main_pipeline(self, source, size, sync):
        """
        Create the frame extraction pipeline.
        """
        pipeline_string = "uridecodebin name=decoder uri=%s ! ffmpegcolorspace ! videoscale ! appsink name=play_sink" % self.source
        self.pipeline = gst.parse_launch(pipeline_string)
        caps = "video/x-raw-rgb, width=%d, height=%d, depth=24, bpp=24" % size
        self.decoder = self.pipeline.get_by_name("decoder")
        self.appsink = self.pipeline.get_by_name('play_sink')
        self.appsink.set_property('emit-signals', True)
        self.appsink.set_property('sync', sync)
        self.appsink.set_property('drop', True)
        self.appsink.set_property('max-buffers', 1)
        self.appsink.set_property('caps', gst.caps_from_string(caps))
        if self.pipeline.set_state(gst.STATE_PLAYING) == gst.STATE_CHANGE_FAILURE:
            raise NameError("Failed to load video source %s" % self.source)
        self.appsink.emit('pull-preroll')

    def get(self):
        """
        Retrieve a video frame as a numpy array.

        Returns
        -------
        output : array (image)
            Retrieved image.
        """
        buff = self.appsink.emit('pull-buffer')
        img_mat = np.ndarray(shape=(self.size[1], self.size[0], 3),
                             dtype=np.uint8, buffer=buff.data)
        return img_mat

    def seek_frame(self, frame_number):
        """
        Seek to specified frame in video.

        Parameters
        ----------
        frame_number : int
            Frame position
        """
        self.pipeline.seek_simple(gst.FORMAT_DEFAULT, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, frame_number)

    def seek_time(self, milliseconds):
        """
        Seek to specified time in video.

        Parameters
        ----------
        milliseconds : int
            Time position
        """
        self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, milliseconds / 1000.0 * gst.SECOND)

    def frame_count(self):
        """
        Returns frame count of video.

        Returns
        -------
        output : int
            Frame count.
        """
        return self.video_length / 1000 * self.video_rate

    def duration(self):
        """
        Returns time length of video in milliseconds.

        Returns
        -------
        output : int
            Time length [ms].
        """
        return self.video_length


class Video(object):
    """
    Video loader. Supports Opencv and Gstreamer backends.

    Parameters
    ----------
    source : str
        Media location URI. Video file path or http address of IP camera.
    size: tuple, optional
        Size of returned array.
    sync: bool, optional (default False)
        Frames are extracted per frame or per time basis. Gstreamer only.
        If enabled the video time step continues onward according to the play rate.
        Useful for IP cameras and other real time video feeds.
    backend: str, 'gstreamer' or 'opencv'
        Backend to use.
    """
    def __init__(self, source=None, size=None, sync=False, backend=None):
        if backend is None:
            # select backend that is available
            if gstreamer_available:
                self.video = GstVideo(source, size, sync)
            elif opencv_available:
                self.video = CvVideo(source, size)
            else:
                # if no backend available, raise exception
                self.video = GstVideo(source, size, sync)
        elif backend == "gstreamer":
            self.video = GstVideo(source, size, sync)
        elif backend == "opencv":
            self.video = CvVideo(source, size)
        else:
            raise ValueError("Unknown backend: %s", backend)

    def get(self):
        """
        Retrieve the next video frame as a numpy array.

        Returns
        -------
        output : array (image)
            Retrieved image.
        """
        return self.video.get()

    def seek_frame(self, frame_number):
        """
        Seek to specified frame in video.

        Parameters
        ----------
        frame_number : int
            Frame position
        """
        self.video.seek_frame(frame_number)

    def seek_time(self, milliseconds):
        """
        Seek to specified time in video.

        Parameters
        ----------
        milliseconds : int
            Time position
        """
        self.video.seek_time(milliseconds)

    def frame_count(self):
        """
        Returns frame count of video.

        Returns
        -------
        output : int
            Frame count.
        """
        return self.video.frame_count()

    def duration(self):
        """
        Returns time length of video in milliseconds.

        Returns
        -------
        output : int
            Time length [ms].
        """
        return self.video.duration()

    def get_index_frame(self, frame_number):
        """
        Retrieve a specified video frame as a numpy array.

        Parameters
        ----------
        frame_number : int
            Frame position

        Returns
        -------
        output : array (image)
            Retrieved image.
        """
        self.video.seek_frame(frame_number)
        return self.video.get()

    def get_collection(self, time_range=None):
        """
        Returns an ImageCollection object.

        Parameters
        ----------
        time_range: range (int), optional
            Time steps to extract, defaults to the entire length of video.

        Returns
        -------
        output: ImageCollection
            Collection of images iterator.
        """
        if not time_range:
            time_range = range(int(self.frame_count()))
        return ImageCollection(time_range, load_func=self.get_index_frame)


__all__ = ["Video"]
