
from __future__ import with_statement

# Enthought library imports
from traits.api import Bool, Instance, Int, Any, Float

# Local, relative imports
from base import intersect_bounds, empty_rectangle
from colors import ColorTrait
from component import Component
from container import Container
from viewport import Viewport
from native_scrollbar import NativeScrollBar


class Scrolled(Container):
    """
    A Scrolled acts like a viewport with scrollbars for positioning the view
    position.  Rather than subclassing from viewport, it delegates to one.
    """

    # The component that we are viewing
    component = Instance(Component)

    # The viewport onto our component
    viewport_component = Instance(Viewport)

    # Inside padding is a background drawn area between the edges or scrollbars
    # and the scrolled area/left component.
    inside_padding_width = Int(5)

    # The inside border is a border drawn on the inner edge of the inside
    # padding area to highlight the viewport.
    inside_border_color = ColorTrait("black")
    inside_border_width = Int(0)

    # The background color to use for filling in the padding area.
    bgcolor = ColorTrait("white")

    # Should the horizontal scrollbar be shown?
    horiz_scrollbar = Bool(True)

    # Should the vertical scrollbar be shown?
    vert_scrollbar = Bool(True)

    # Should the scrollbars always be shown?
    always_show_sb = Bool(False)

    # Should the mouse wheel scroll the viewport?
    mousewheel_scroll = Bool(True)

    # Should the viewport update continuously as the scrollbar is dragged,
    # or only when drag terminates (i.e. the user releases the mouse button)
    continuous_drag_update = Bool(True)

    # Override the default value of this inherited trait
    auto_size = False

    #---------------------------------------------------------------------------
    # Traits for support of geophysics plotting
    #---------------------------------------------------------------------------

    # An alternate vertical scroll bar to control this Scrolled, instead of the
    # default one that lives outside the scrolled region.
    alternate_vsb = Instance(Component)

    # The size of the left border space
    leftborder = Float(0)

    # A component to lay out to the left of the viewport area (e.g. a depth
    # scale track)
    leftcomponent = Any

    #---------------------------------------------------------------------------
    # Private traits
    #---------------------------------------------------------------------------

    _vsb = Instance(NativeScrollBar)
    _hsb = Instance(NativeScrollBar)

    # Stores the last horizontal and vertical scroll positions to avoid
    # multiple updates in update_from_viewport()
    _last_hsb_pos = Float(0.0)
    _last_vsb_pos = Float(0.0)

    # Whether or not the viewport region is "locked" from updating via
    # freeze_scroll_bounds()
    _sb_bounds_frozen = Bool(False)

    # Records if the horizontal scroll position has been updated while the
    # Scrolled has been frozen
    _hscroll_position_updated = Bool(False)

    # Records if the vertical scroll position has been updated while the
    # Scrolled has been frozen
    _vscroll_position_updated = Bool(False)

    # Whether or not to the scroll bars should cause an event
    # update to fire on the viewport's view_position.  This is used to
    # prevent redundant events when update_from_viewport() updates the
    # scrollbar position.
    _hsb_generates_events = Bool(True)
    _vsb_generates_events = Bool(True)

    #---------------------------------------------------------------------------
    # Scrolled interface
    #---------------------------------------------------------------------------

    def __init__(self, component, **traits):
        self.component = component
        Container.__init__( self, **traits )
        self._viewport_component_changed()
        return

    def update_bounds(self):
        self._layout_needed = True
        if self._hsb is not None:
            self._hsb._widget_moved = True
        if self._vsb is not None:
            self._vsb._widget_moved = True
        return

    def sb_height(self):
        """ Returns the standard scroll bar height
        """
        # Perhaps a placeholder -- not sure if there's a way to get the standard
        # width or height of a wx scrollbar -- you can set them to whatever you want.
        return 15

    def sb_width(self):
        """ Returns the standard scroll bar width
        """
        return 15

    def freeze_scroll_bounds(self):
        """ Prevents the scroll bounds on the scrollbar from updating until
        unfreeze_scroll_bounds() is called.  This is useful on components with
        view-dependent bounds; when the user is interacting with the scrollbar
        or the viewport, this prevents the scrollbar from resizing underneath
        them.
        """
        if not self.continuous_drag_update:
            self._sb_bounds_frozen = True

    def unfreeze_scroll_bounds(self):
        """ Allows the scroll bounds to be updated by various trait changes.
        See freeze_scroll_bounds().
        """
        self._sb_bounds_frozen = False
        if self._hscroll_position_updated:
            self._handle_horizontal_scroll(self._hsb.scroll_position)
            self._hscroll_position_updated = False
        if self._vscroll_position_updated:
            self._handle_vertical_scroll(self._vsb.scroll_position)
            self._vscroll_position_updated = False
        self.update_from_viewport()
        self.request_redraw()


    #---------------------------------------------------------------------------
    # Trait event handlers
    #---------------------------------------------------------------------------

    def _compute_ranges(self):
        """ Returns the range_x and range_y tuples based on our component
        and our viewport_component's bounds.
        """
        comp = self.component
        viewport = self.viewport_component

        offset = getattr(comp, "bounds_offset", (0,0))

        ranges = []
        for ndx in (0,1):
            scrollrange = float(comp.bounds[ndx] - viewport.view_bounds[ndx])
            if round(scrollrange/20.0) > 0.0:
                ticksize = scrollrange/round(scrollrange/20.0)
            else:
                ticksize = 1
            ranges.append( (offset[ndx], offset[ndx] + comp.bounds[ndx],
                            viewport.view_bounds[ndx], ticksize) )

        return ranges


    def update_from_viewport(self):
        """ Repositions the scrollbars based on the current position/bounds of
            viewport_component.
        """
        if self._sb_bounds_frozen:
            return

        x, y = self.viewport_component.view_position
        range_x, range_y = self._compute_ranges()

        modify_hsb = (self._hsb and x != self._last_hsb_pos)
        modify_vsb = (self._vsb and y != self._last_vsb_pos)

        if modify_hsb and modify_vsb:
            self._hsb_generates_events = False
        else:
            self._hsb_generates_events = True

        if modify_hsb:
            self._hsb.range = range_x
            self._hsb.scroll_position = x
            self._last_hsb_pos = x

        if modify_vsb:
            self._vsb.range = range_y
            self._vsb.scroll_position = y
            self._last_vsb_pos = y

        if not self._hsb_generates_events:
            self._hsb_generates_events = True

        return

    def _layout_and_draw(self):
        self._layout_needed = True
        self.request_redraw()

    def _component_position_changed(self, component):
        self._layout_needed = True
        return

    def _bounds_changed_for_component(self):
        self._layout_needed = True
        self.update_from_viewport()
        self.request_redraw()
        return

    def _bounds_items_changed_for_component(self):
        self.update_from_viewport()
        return

    def _position_changed_for_component(self):
        self.update_from_viewport()
        return

    def _position_items_changed_for_component(self):
        self.update_from_viewport()
        return

    def _view_bounds_changed_for_viewport_component(self):
        self.update_from_viewport()
        return

    def _view_bounds_items_changed_for_viewport_component(self):
        self.update_from_viewport()
        return

    def _view_position_changed_for_viewport_component(self):
        self.update_from_viewport()
        return

    def _view_position_items_changed_for_viewport_component(self):
        self.update_from_viewport()
        return

    def _component_bounds_items_handler(self, object, new):
        if new.added != new.removed:
            self.update_bounds()

    def _component_bounds_handler(self, object, name, old, new):
        if old == None or new == None or old[0] != new[0] or old[1] != new[1]:
            self.update_bounds()
        return

    def _component_changed ( self, old, new ):
        if old is not None:
            old.on_trait_change(self._component_bounds_handler, 'bounds', remove=True)
            old.on_trait_change(self._component_bounds_items_handler,
                                'bounds_items', remove=True)
        if new is None:
            self.component = Container()
        else:
            if self.viewport_component:
                self.viewport_component.component = new
        new.on_trait_change(self._component_bounds_handler, 'bounds')
        new.on_trait_change(self._component_bounds_items_handler, 'bounds_items')
        self._layout_needed = True
        return

    def _bgcolor_changed ( self ):
        self._layout_and_draw()

    def _inside_border_color_changed ( self ):
        self._layout_and_draw()

    def _inside_border_width_changed ( self ):
        self._layout_and_draw()

    def _inside_padding_width_changed(self):
        self._layout_needed = True
        self.request_redraw()

    def _viewport_component_changed(self):
        if self.viewport_component is None:
            self.viewport_component = Viewport()
        self.viewport_component.component = self.component
        self.viewport_component.view_position = [0,0]
        self.viewport_component.view_bounds = self.bounds
        self.add(self.viewport_component)

    def _alternate_vsb_changed(self, old, new):
        self._component_update(old, new)
        return

    def _leftcomponent_changed(self, old, new):
        self._component_update(old, new)
        return

    def _component_update(self, old, new):
        """ Generic function to manage adding and removing components """
        if old is not None:
            self.remove(old)
        if new is not None:
            self.add(new)
        return

    def _bounds_changed ( self, old, new ):
        Component._bounds_changed( self, old, new )
        self.update_bounds()
        return

    def _bounds_items_changed(self, event):
        Component._bounds_items_changed(self, event)
        self.update_bounds()
        return


    #---------------------------------------------------------------------------
    # Protected methods
    #---------------------------------------------------------------------------

    def _do_layout ( self ):
        """ This is explicitly called by _draw().
        """
        self.viewport_component.do_layout()

        # Window is composed of border + scrollbar + canvas in each direction.
        # To compute the overall geometry, first calculate whether component.x
        # + the border fits in the x size of the window.
        # If not, add sb, and decrease the y size of the window by the height of
        # the scrollbar.
        # Now, check whether component.y + the border is greater than the remaining
        # y size of the window.  If it is not, add a scrollbar and decrease the x size
        # of the window by the scrollbar width, and perform the first check again.

        if not self._layout_needed:
            return

        padding = self.inside_padding_width
        scrl_x_size, scrl_y_size = self.bounds
        cont_x_size, cont_y_size = self.component.bounds

        # available_x and available_y are the currently available size for the
        # viewport
        available_x = scrl_x_size - 2*padding - self.leftborder
        available_y = scrl_y_size - 2*padding

        # Figure out which scrollbars we will need

        need_x_scrollbar = self.horiz_scrollbar and \
                    ((available_x < cont_x_size) or self.always_show_sb)
        need_y_scrollbar = (self.vert_scrollbar and \
                    ((available_y < cont_y_size) or self.always_show_sb)) or \
                    self.alternate_vsb

        if need_x_scrollbar:
            available_y -= self.sb_height()
        if need_y_scrollbar:
            available_x -= self.sb_width()
        if (available_x < cont_x_size) and \
                (not need_x_scrollbar) and self.horiz_scrollbar:
            available_y -= self.sb_height()
            need_x_scrollbar = True

        # Put the viewport in the right position
        self.viewport_component.outer_bounds = [available_x, available_y]
        container_y_pos = padding

        if need_x_scrollbar:
            container_y_pos += self.sb_height()
        self.viewport_component.outer_position = [padding + self.leftborder,
                                            container_y_pos]

        range_x, range_y = self._compute_ranges()

        # Create, destroy, or set the attributes of the horizontal scrollbar
        if need_x_scrollbar:
            bounds = [available_x, self.sb_height()]
            hsb_position = [padding + self.leftborder, 0]
            if not self._hsb:
                self._hsb = NativeScrollBar(orientation = 'horizontal',
                                            bounds=bounds,
                                            position=hsb_position,
                                            range=range_x,
                                            enabled=False,
                                            )
                self._hsb.on_trait_change(self._handle_horizontal_scroll,
                                          'scroll_position')
                self._hsb.on_trait_change(self._mouse_thumb_changed,
                                          'mouse_thumb')
                self.add(self._hsb)
            else:
                self._hsb.range = range_x
                self._hsb.bounds = bounds
                self._hsb.position = hsb_position
        elif self._hsb is not None:
            self._hsb = self._release_sb(self._hsb)
            if not hasattr(self.component, "bounds_offset"):
                self.viewport_component.view_position[0] = 0
        else:
            # We don't need to render the horizontal scrollbar, and we don't
            # have one to update, either.
            pass

        # Create, destroy, or set the attributes of the vertical scrollbar
        if self.alternate_vsb:
            self.alternate_vsb.bounds = [self.sb_width(), available_y]
            self.alternate_vsb.position = [2*padding + available_x + self.leftborder,
                                           container_y_pos]

        if need_y_scrollbar and (not self.alternate_vsb):
            bounds = [self.sb_width(), available_y]
            vsb_position = [2*padding + available_x + self.leftborder,
                        container_y_pos]
            if not self._vsb:
                self._vsb = NativeScrollBar(orientation = 'vertical',
                                            bounds=bounds,
                                            position=vsb_position,
                                            range=range_y
                                            )

                self._vsb.on_trait_change(self._handle_vertical_scroll,
                                          'scroll_position')
                self._vsb.on_trait_change(self._mouse_thumb_changed,
                                          'mouse_thumb')
                self.add(self._vsb)
            else:
                self._vsb.bounds = bounds
                self._vsb.position = vsb_position
                self._vsb.range = range_y
        elif self._vsb:
            self._vsb = self._release_sb(self._vsb)
            if not hasattr(self.component, "bounds_offset"):
                self.viewport_component.view_position[1] = 0
        else:
            # We don't need to render the vertical scrollbar, and we don't
            # have one to update, either.
            pass

        self._layout_needed = False
        return

    def _release_sb ( self, sb ):
        if sb is not None:
            if sb == self._vsb:
                sb.on_trait_change( self._handle_vertical_scroll,
                                    'scroll_position', remove = True )
            if sb == self._hsb:
                sb.on_trait_change(self._handle_horizontal_scroll,
                                   'scroll_position', remove=True)
            self.remove(sb)
            # We shouldn't have to do this, but I'm not sure why the object
            # isn't getting garbage collected.
            # It must be held by another object, but which one?
            sb.destroy()
        return None

    def _handle_horizontal_scroll(self, position):
        if self._sb_bounds_frozen:
            self._hscroll_position_updated = True
            return

        c = self.component
        viewport = self.viewport_component
        offsetx = getattr(c, "bounds_offset", [0,0])[0]
        if (position + viewport.view_bounds[0] <= c.bounds[0] + offsetx):
            if self._hsb_generates_events:
                viewport.view_position[0] = position
            else:
                viewport.set(view_position=[position, viewport.view_position[1]],
                             trait_change_notify=False)
        return

    def _handle_vertical_scroll(self, position):
        if self._sb_bounds_frozen:
            self._vscroll_position_updated = True
            return

        c = self.component
        viewport = self.viewport_component
        offsety = getattr(c, "bounds_offset", [0,0])[1]
        if (position + viewport.view_bounds[1] <= c.bounds[1] + offsety):
            if self._vsb_generates_events:
                viewport.view_position[1] = position
            else:
                viewport.set(view_position=[viewport.view_position[0], position],
                             trait_change_notify=False)
        return

    def _mouse_thumb_changed(self, object, attrname, event):
        if event == "down" and not self.continuous_drag_update:
            self.freeze_scroll_bounds()
        else:
            self.unfreeze_scroll_bounds()

    def _draw(self, gc, view_bounds=None, mode="default"):

        if self.layout_needed:
            self._do_layout()
        with gc:
            self._draw_container(gc, mode)

            self._draw_inside_border(gc, view_bounds, mode)

            dx, dy = self.bounds
            x,y = self.position
            if view_bounds:
                tmp = intersect_bounds((x,y,dx,dy), view_bounds)
                if (tmp is empty_rectangle):
                    new_bounds = tmp
                else:
                    new_bounds = (tmp[0]-x, tmp[1]-y, tmp[2], tmp[3])
            else:
                new_bounds = view_bounds

            if new_bounds is not empty_rectangle:
                for component in self.components:
                    if component is not None:
                        with gc:
                            gc.translate_ctm(*self.position)
                            component.draw(gc, new_bounds, mode)

    def _draw_inside_border(self, gc, view_bounds=None, mode="default"):
        width_adjustment = self.inside_border_width/2
        left_edge = self.x+1 + self.inside_padding_width - width_adjustment
        right_edge = self.x + self.viewport_component.x2+2 + width_adjustment
        bottom_edge = self.viewport_component.y+1 - width_adjustment
        top_edge = self.viewport_component.y2 + width_adjustment

        with gc:
            gc.set_stroke_color(self.inside_border_color_)
            gc.set_line_width(self.inside_border_width)
            gc.rect(left_edge, bottom_edge,
                    right_edge-left_edge, top_edge-bottom_edge)
            gc.stroke_path()


    #---------------------------------------------------------------------------
    # Mouse event handlers
    #---------------------------------------------------------------------------

    def _container_handle_mouse_event(self, event, suffix):
        """
        Implement a container-level dispatch hook that intercepts mousewheel
        events.  (Without this, our components would automatically get handed
        the event.)
        """
        if self.mousewheel_scroll and suffix == "mouse_wheel":
            if self.alternate_vsb:
                self.alternate_vsb._mouse_wheel_changed(event)
            elif self._vsb:
                self._vsb._mouse_wheel_changed(event)
            event.handled = True
        return

    #---------------------------------------------------------------------------
    # Persistence
    #---------------------------------------------------------------------------

    def __getstate__(self):
        state = super(Scrolled,self).__getstate__()
        for key in ['alternate_vsb', '_vsb', '_hsb', ]:
            if state.has_key(key):
                del state[key]
        return state

### EOF
