# -*- mode: python; coding: utf-8 -*-
#
# Pigment Python tools
#
# Copyright © 2006, 2007, 2008 Fluendo Embedded S.L.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

import pgm
from pgm.graph.group import Group
from pgm.graph.image import Image
from pgm.timing import implicit
from pgm.widgets import const
from pgm.utils import maths

import math
import gobject
import time as mod_time

class List(Group):

    __gsignals__ = {
        'child-clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                (gobject.TYPE_INT,)),
        'selected-item-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                (gobject.TYPE_INT,)),
        'scrolled': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_BOOLEAN,
                (gobject.TYPE_FLOAT, gobject.TYPE_FLOAT, gobject.TYPE_FLOAT,
                pgm.ScrollDirection, gobject.TYPE_UINT)),
    }
    
    # time between two drag events in milliseconds
    drag_motion_resolution = 30

    # amount of movement needed to activate dragging in canvas coordinates
    drag_threshold = 0.05

    # number of items that are not yet visible that should be preloaded
    preloaded = 5

    # animation mode of the widget: animated or not animated
    animated = True

    def __init__(self, width=1.0, height=2.0, visible_range_size=7,
                 orientation=const.VERTICAL):
        super(List, self).__init__()

        self.widgets = []

        self._selected_item = 0
        self._visible_range_start = 0.0
        self._orientation = orientation
        self._preloaded_widgets = []

        self._width = width
        self._height = height

        self._last_drag_motion = 0
        self.visible_range_size = visible_range_size

        # mouse support
        self._drag_zone = Image()
        self._drag_zone.bg_color = (255, 0, 0, 0)
        self._drag_zone.size = (width, height)
        self._drag_zone.visible = True
        Group.add(self, self._drag_zone)
        self._drag_zone.connect("drag-begin", self._drag_begin)
        self._drag_zone.connect("drag-motion", self._drag_motion)
        self._drag_zone.connect("drag-end", self._drag_end)
        self._drag_zone.connect("clicked", self._clicked)
        self._drag_zone.connect("double-clicked", self._double_clicked)
        self._drag_zone.connect("scrolled", self._scrolled)
        
        # animation support
        attributes = ("visible_range_size", "visible_range_start")
        self._animated = implicit.AnimatedObject(self, attributes)
        self._animated.setup_next_animations(duration=500,
                                             transformation=implicit.DECELERATE)
        self._animated.mode = implicit.REPLACE

        self._dragging = False
        self._drag_accum = 0

    def _scrolled(self, drawable, x, y, z, direction, time):
        return self.emit('scrolled', x, y, z, direction, time)

    def _drag_begin(self, drawable, x, y, z, button, time):
        self._dragging = True
        return self.emit('drag-begin', x, y, z, button, time)

    def _drag_motion(self, drawable, x, y, z, button, time):
        time_since_last = time - self._last_drag_motion
        if time_since_last > self.drag_motion_resolution:
            self._last_drag_motion = time
            return self.emit('drag-motion', x, y, z, button, time)

    def _drag_end(self, drawable, x, y, z, button, time):
        self._dragging = False
        return self.emit('drag-end', x, y, z, button, time)

    def _clicked(self, drawable, x, y, z, button, time):
        if self._dragging and self._drag_accum > self.drag_threshold:
            return True

        return self.emit('clicked', x, y, z, button, time)

    def _double_clicked(self, drawable, x, y, z, button, time):
        if self._dragging:
            return False

        return self.emit('double-clicked', x, y, z, button, time)

    def width__get(self):
        return self._width

    def width__set(self, width):
        self._width = width
        self._drag_zone.width = width

        self._widget_width = self.compute_width(0)
        self._layout()

    def height__get(self):
        return self._height

    def height__set(self, height):
        self._height = height
        self._drag_zone.height = height

        self._widget_height = self.compute_height(0)
        self._layout()

    def size__get(self):
        return (self._width, self._height)

    def size__set(self, size):
        self.width = size[0]
        self.height = size[1]

    def visible_range_size__get(self):
        return self._visible_range_size

    def visible_range_size__set(self, visible_range_size):
        self._visible_range_size = visible_range_size

        self._widget_height = self.compute_height(0)
        self._widget_width = self.compute_width(0)
        self._layout()

    def compute_height(self, index):
        if self._orientation == const.VERTICAL:
            return self._height / self._visible_range_size
        elif self._orientation == const.HORIZONTAL:
            return self._height

    def compute_width(self, index):
        if self._orientation == const.VERTICAL:
            return self._width
        elif self._orientation == const.HORIZONTAL:
            return self._width / self._visible_range_size

    def compute_x(self, index):
        if self._orientation == const.VERTICAL:
            return 0.0
        elif self._orientation == const.HORIZONTAL:
            return index * self._widget_width

    def compute_y(self, index):
        if self._orientation == const.VERTICAL:
            return index * self._widget_height
        elif self._orientation == const.HORIZONTAL:
            return 0.0

    def compute_z(self, index):
        return 0.0

    def compute_opacity(self, index):
        full_opacity = 255
        shaded_opacity = 100
        invisible_opacity = -80

        # make the transformation symmetrical
        if index <= self._visible_range_size/2.0:
            # beginning of the visible items case
            index = index
            start = self._visible_range_start
        else:
            # end of the visible items case
            index = self._visible_range_size - index - 1
            start = len(self) - self._visible_range_size -\
                    self._visible_range_start

        if start <= 0 or index >= 1 or self._visible_range_size <= 1.0:
            opacity = full_opacity
        elif index >= 0:
            opacity = maths.lerp(shaded_opacity, full_opacity, index)
        elif index < 0:
            if start >= 1:
                opacity = maths.lerp(invisible_opacity, shaded_opacity, index+1)
            else:
                opacity = maths.lerp(invisible_opacity, full_opacity, index+1)

        opacity = max(min(255, opacity), 0)

        return opacity

    def compute_zoom_width(self, index):
        return 1.0

    def compute_zoom_height(self, index):
        return 1.0

    def visible_range_start__set(self, visible_range_start):
        self._visible_range_start = visible_range_start
        self._layout()

    def visible_range_start__get(self):
        return self._visible_range_start

    def selected_item__get(self):
        return self._selected_item

    def selected_item__set(self, index):
        self._stop_deceleration()
        index = maths.clamp(index, 0, len(self.widgets)-1)
        index = int(round(index))

        half_size = (self.visible_range_size-1.0)/2.0
        prev_selected = self._selected_item
        self._selected_item = index
        if index <= half_size or len(self.widgets) <= self.visible_range_size:
            visible_range_start = 0.0
        elif index >= len(self.widgets)-half_size:
            visible_range_start = len(self.widgets)-self.visible_range_size
        else:
            visible_range_start = index-half_size

        if self.animated:
            self._animated.visible_range_start = visible_range_start
        else:
            self.visible_range_start = visible_range_start

        if prev_selected != index:
            self.emit('selected-item-changed', index)

    def range_start_to_selected(self):
        half_size = (self.visible_range_size-1.0)/2.0
        selected = self.visible_range_start + half_size
        selected = int(round(selected))
        selected = maths.clamp(selected, 0, len(self.widgets)-1)
        return selected

    def layout_widget(self, widget, position):
        width = self._widget_width*self.compute_zoom_width(position)
        height = self._widget_height*self.compute_zoom_height(position)

        # update widget properties
        x = self.compute_x(position)-(width-self._widget_width)/2.0
        y = self.compute_y(position)-(height-self._widget_height)/2.0
        z = self.compute_z(position)
        widget.position = (x, y, z)
        widget.size = (width, height)
        widget.opacity = self.compute_opacity(position)

    def visible__set(self, value):
        old_value = self.visible
        super(List, self).visible__set(value)
        if value and not old_value:
            self._layout()

    def _layout(self):
        if not self.visible:
            return

        # widgets that will be unloaded once the layout is done
        to_unload = set(self._preloaded_widgets)

        # preloads widgets at the edge of the visible range
        # update properties for visible widgets only
        inf = int(math.floor(self._visible_range_start))
        sup = int(math.ceil(self._visible_range_start+self._visible_range_size))

        inf_preloaded = inf - self.preloaded
        sup_preloaded = sup + self.preloaded

        inf_preloaded = max(0, inf_preloaded)
        sup_preloaded = min(len(self.widgets), sup_preloaded)

        inf = max(0, inf)
        sup = min(len(self.widgets), sup)

        self._preloaded_widgets = self.widgets[inf_preloaded:sup_preloaded]
        for i, widget in enumerate(self._preloaded_widgets):
            i += inf_preloaded
            position = -self._visible_range_start + i

            # update to_unload accordingly
            if widget in to_unload:
                to_unload.remove(widget)
            else:
                self.load_item(i)

            if i >= inf and i < sup:
                # widget is in the visible range and therefore needs to get
                # its properties set (position, size, etc.)
                self.layout_widget(widget, position)

                # adding the widget to the list group so that it's eventually
                # displayed
                # most of the time it is not necessary to add the widget to
                # the Group since it's already there
                Group.add(self, widget)

                # FIXME: this should not be necessary but for some unknown
                # reason some items do not show up when scrolling quickly if
                # this assignment is removed
                widget.visible = True
            else:
                Group.remove(self, widget)

        # unload widgets that were loaded before that now must be unloaded
        for widget in to_unload:
            Group.remove(self, widget)
            self.unload_item(widget)

    def load_item(self, index):
        pass

    def unload_item(self, widget):
        pass

    def is_widget_visible(self, index):
        inf = int(math.floor(self._visible_range_start))
        sup = int(math.ceil(self._visible_range_start+self._visible_range_size))
        return index >= inf and index < sup

    def orientation__set(self, orientation):
        self._orientation = orientation
        self.visible_range_size = self._visible_range_size

    def orientation__get(self):
        return self._orientation

    def __len__(self):
        return len(self.widgets)

    def insert(self, index, widget):
        if index < 0 or index > len(self.widgets):
            raise IndexError

        # do not add the same widget twice
        if widget in self.widgets:
            return

        widget.visible = True

        self.widgets.insert(index, widget)
        if not self._animated.is_animated("visible_range_start"):
            sup = int(self._visible_range_start+self._visible_range_size+1.0)
            if index <= sup:
                self._layout()

    def append(self, widget):
        self.insert(len(self.widgets), widget)

    def remove(self, widget):
        try:
            # if the widget is part of the List
            index = self.widgets.index(widget)
        except ValueError:
            # if the widget is just part of the Group
            Group.remove(self, widget)
        else:
            self.pop(index)

    def pop(self, index):
        if index < 0 or index >= len(self.widgets):
            raise IndexError

        if self.selected_item == index == len(self.widgets)-1:
            self.selected_item -= 1

        widget = self.widgets.pop(index)
        Group.remove(self, widget)
        if not self._animated.is_animated("visible_range_start"):
            sup = int(self._visible_range_start+self._visible_range_size+1.0)
            if index <= sup:
                self._layout()

        return widget

    def empty(self):
        for widget in self.widgets:
            Group.remove(self, widget)
        self.widgets[:] = []
        self._selected_item = 0
        self._layout()
        
    def __getitem__(self, index):
        return self.widgets[index]

    def do_clicked(self, x, y, z, button, time):
        if self._orientation == const.VERTICAL:
            visible_selected = \
                    (y - self.y) / (self.height / self.visible_range_size)
        else:
            visible_selected = \
                    (x - self.x) / (self.width / self.visible_range_size)

        child_clicked = int(self.visible_range_start + visible_selected)

        self.emit('child-clicked', child_clicked)
        return True

    def do_double_clicked(self, x, y, z, button, time):
        return True

    def do_drag_begin(self, x, y, z, button, time):
        self._initial = (x, y, time)
        self._animated.stop_animations()
        self._stop_deceleration()

        self._drag_accum = 0

        self.speed = 0.0

        return True

    def do_drag_motion(self, x, y, z, button, time):
        motion = 0

        if self._orientation == const.VERTICAL:
            motion = y - self._initial[1]
            self.visible_range_start -= motion / self._widget_height
        elif self._orientation == const.HORIZONTAL:
            motion = x - self._initial[0]
            self.visible_range_start -= motion / self._widget_width

        time_delta = time - self._initial[2]
        if time_delta != 0:
            self.speed = motion/time_delta*1000.0

        self._initial = (x, y, time)
        self._drag_accum += abs(motion)

        return True

    def do_drag_end(self, x, y, z, button, time):
        if self._drag_accum > self.drag_threshold:
            self._current_time = mod_time.time()
            self._deceleration_source = gobject.timeout_add(17, self._decelerate)
        return True

    def do_child_clicked(self, index):
        self.selected_item = index
        return True

    deceleration = 8.0
    def _decelerate(self):
        self._previous_time = self._current_time
        self._current_time = mod_time.time()
        delta = self._current_time - self._previous_time

        if self.speed > 0.0:
            self.speed -= self.deceleration*delta
            if self.speed > 0.0:
                self.visible_range_start -= self.speed*delta

                # block the movement if it reaches the first item
                if self.range_start_to_selected() > 0:
                    return True

        elif self.speed < 0.0:
            self.speed += self.deceleration*delta
            if self.speed < 0.0:
                self.visible_range_start -= self.speed*delta

                # block the movement if it reaches the last item
                if self.range_start_to_selected() < len(self.widgets)-1:
                    return True

        self.selected_item = self.range_start_to_selected()
        return False

    def _stop_deceleration(self):
        try:
            gobject.source_remove(self._deceleration_source)
        except AttributeError:
            pass

if __name__ == "__main__":
    import pgm
    import gobject
    import gst
    import glob, sys
    from pgm.graph.text import Text
    from pgm.graph.image import Image

    def create_text(label):
        txt = Text()
        txt.label = label
        txt.font_family = "Bitstream DejaVu"
        txt.font_height = 0.225
        txt.fg_color = (255, 255, 255, 255)
        txt.bg_color = (255, 0, 0, 255)
        txt.ellipsize = pgm.TEXT_ELLIPSIZE_END
        txt.visible = True
        return txt

    def create_img(img_file):
        img = Image()
        img.set_from_file(img_file, 512)
        img.fg_color = (255, 255, 255, 255)
        img.bg_color = (100, 200, 100, 155)
        img.bg_color = (0, 0, 0, 0)
        img.visible = True
        return img

    def create_reflection(master_img):
        img = Image()
        img.set_from_image(master_img)
        img.fg_color = (255, 255, 255, 255)
        img.bg_color = (100, 100, 200, 155)
        img.bg_color = (0, 0, 0, 0)
#        img.width = -master_img.width
        img.height = master_img.height
        img.opacity = 30
#        img.x += master_img.width
        img.layout = pgm.IMAGE_SCALED
        img.y += master_img.height
        img.alignment = pgm.IMAGE_TOP
        img.visible = True
        return img

    def create_video(video_uri):
        img = Image()
        img.fg_color = (255, 255, 255, 255)
        img.bg_color = (0, 0, 0, 0)
        img.alignment = pgm.IMAGE_LEFT
        img.visible = True

        # GStreamer pipeline setup
        pipeline = gst.element_factory_make('playbin')
        sink = gst.element_factory_make('pgmimagesink')
        pipeline.set_property('uri', video_uri)
        pipeline.set_property('video-sink', sink)
        sink.set_property('image', img)
        pipeline.set_state(gst.STATE_PLAYING)

        return img


    def on_key_press(viewport, event, widget):
        if event.type == pgm.KEY_PRESS:
            # quit on q or ESC
            if event.keyval == pgm.keysyms.q or \
               event.keyval == pgm.keysyms.Escape:
                pgm.main_quit()
            
            elif event.keyval == pgm.keysyms.f:
                viewport.fullscreen = not viewport.fullscreen

            elif event.keyval == pgm.keysyms.b:
                widget.selected_item = 0

            elif event.keyval == pgm.keysyms.g:
                widget.selected_item = len(widget) - 1

            elif event.keyval == pgm.keysyms.h:
                #widget.visible_range_size += 2
                widget._animated.visible_range_size += 2

            elif event.keyval == pgm.keysyms.n:
                #widget.visible_range_size -= 2
                widget._animated.visible_range_size -= 2

            elif event.keyval == pgm.keysyms.s:
                if list_widget.orientation == const.VERTICAL:
                    list_widget.orientation = const.HORIZONTAL
                elif list_widget.orientation == const.HORIZONTAL:
                    list_widget.orientation = const.VERTICAL

            elif event.keyval == pgm.keysyms.Down or \
                 event.keyval == pgm.keysyms.Right:
                widget.selected_item += 1

            elif event.keyval == pgm.keysyms.Up or \
                 event.keyval == pgm.keysyms.Left:
                widget.selected_item -= 1

            elif event.keyval == pgm.keysyms.space:
                #widget.insert(0, create_text("T"))
                def test():
#                    img = create_img("/home/kaleo/dev/pigment/examples/pictures/fluendo.png")
                    """
                    widget.insert(0, img)
                    widget.pop(len(widget)-1)
                    """
                    img = widget.pop(0)
                    widget.append(img)
                    return True
                gobject.timeout_add(1000, test)
#                widget.append(img)

            # remove the currently selected item
            elif event.keyval == pgm.keysyms.Return:
                widget.pop(widget.selected_item)

    def on_delete(viewport, event):
        pgm.main_quit()


    # OpenGL viewport creation
    factory = pgm.ViewportFactory('opengl')
    gl = factory.create()
    gl.title = 'List widget'

    # Canvas and image drawable creation
    canvas = pgm.Canvas()

    # Bind the canvas to the OpenGL viewport
    gl.set_canvas(canvas)
    gl.show()

    widget = List()
    widget.position = (0.5, 0.5, 0.0)
    widget.orientation = const.HORIZONTAL
    widget.width = 3.0
    widget.height = 2.0
    widget.visible_range_size = 7
    widget.visible = True
    widget.canvas = canvas

    files = sys.argv[1:]
    for file in files:
        image = create_img(file)
        widget.append(image)
        """
        # reflection code
        image.alignment = pgm.IMAGE_BOTTOM
        reflection = create_reflection(image)
        g = Group(canvas, pgm.DRAWABLE_MIDDLE)
        g.add(image)
        g.add(reflection)
        g.visible = True
        widget.append(g)
        """

    # Let's start a mainloop
    gl.connect('key-press-event',
               on_key_press,
               widget)
    gl.connect('delete-event', on_delete)
    pgm.main()
