#
# ImageViewBokeh.py -- classes implementing a ginga viewer backend for Bokeh
#
# Eric Jeschke (eric@naoj.org)
#
# Copyright (c)  Eric R. Jeschke.  All rights reserved.
# This is open-source software licensed under a BSD license.
# Please see the file LICENSE.txt for details.

import sys, re
import numpy
import threading
import math
from io import BytesIO

# Bokeh imports
from bokeh.plotting import figure, show, curdoc
from bokeh.models import BoxSelectTool, TapTool, PanTool
#from bokeh.client import push_session
#from bokeh.io import push_notebook

from ginga import ImageView
from ginga import Mixins, Bindings, colors
from ginga.web.bokehw.CanvasRenderBokeh import CanvasRenderer
from ginga.util.heaptimer import TimerHeap

# TODO: is this the right place for this call?
#session = push_session(curdoc())

try:
    # See if we have aggdraw module--best choice
    from ginga.aggw.ImageViewAgg import ImageViewAgg as ImageViewSS, \
         ImageViewAggError as ImageViewError

except ImportError:
    try:
        # No, hmm..ok, see if we have opencv module...
        from ginga.cvw.ImageViewCv import ImageViewCv as ImageViewSS, \
             ImageViewCvError as ImageViewError

    except ImportError:
        try:
            # No dice. How about the PIL module?
            from ginga.pilw.ImageViewPil import ImageViewPil as ImageViewSS, \
                 ImageViewPilError as ImageViewError

        except ImportError:
            # Fall back to mock--there will be no graphic overlays
            from ginga.mockw.ImageViewMock import ImageViewMock as ImageViewSS, \
                 ImageViewMockError as ImageViewError



## class ImageViewBokehError(ImageView.ImageViewError):
##     pass
class ImageViewBokehError(ImageViewError):
    pass

class ImageViewBokeh2(ImageViewSS):
    """
    This version does all the graphical overlays on the server side.
    """

    def __init__(self, logger=None, rgbmap=None, settings=None):
        ImageViewSS.__init__(self, logger=logger,
                             rgbmap=rgbmap,
                             settings=settings)

        # Our Bokeh plot
        self.figure = None
        # Holds the image on the plot
        self.bkimage = None
        self._push_server = False

        # NOTE: Bokeh manages it's Y coordinates by default with
        # the origin at the bottom (see base class)
        self._originUpper = False

        # override, until we can get access to a timer
        #self.defer_redraw = False

        self.msgtask = None
        # see reschedule_redraw() method
        self._defer_task = None


    def set_figure(self, figure):
        """Call this with the Bokeh figure object."""
        self.figure = figure
        self.bkimage = None

        wd = figure.plot_width
        ht = figure.plot_height

        self.configure_window(wd, ht)

    def get_figure(self):
        return self.figure

    def update_image(self):
        if self.figure is None:
            return

        wd, ht = self.get_window_size()

        # Get surface as a numpy array
        surface = self.get_surface()
        if isinstance(surface, numpy.ndarray):
            arr8 = surface
        else:
            arr8 = numpy.fromstring(surface.tostring(), dtype=numpy.uint8)
            arr8 = arr8.reshape((ht, wd, 4))

        # Bokeh expects a 32-bit uint array type
        view = arr8.view(dtype=numpy.uint32).reshape((ht, wd))

        dst_x = dst_y = 0

        # Create an Image_RGBA object in the plot
        if self.bkimage is None:
            self.bkimage = self.figure.image_rgba(image=[view],
                                                  x=[dst_x], y=[dst_y],
                                                  dw=[wd], dh=[ht])
            d_src = self.bkimage.data_source
            self._setup_handlers(d_src)
            
        else:
            # note: get the data source (a ColumnDataSource) and update
            # the values
            d_src = self.bkimage.data_source
            d_src.data["image"] = [view]
            d_src.data["x"] = [dst_x]
            d_src.data["y"] = [dst_y]
            d_src.data["dw"] = [wd]
            d_src.data["dh"] = [ht]

        if self._push_server:
            try:
                #self.bkimage.data_source.push_notebook()
                #push_notebook()
                cursession().store_objects(d_src)

            except Exception as e:
                self.logger.warn("Can't update bokeh plot: %s" % (str(e)))


    def reschedule_redraw(self, time_sec):
        if self.figure is not None:
            ## try:
            ##     self.figure.after_cancel(self._defer_task)
            ## except:
            ##     pass
            time_ms = int(time_sec * 1000)
            ## self._defer_task = self.figure.after(time_ms,
            ##                                        self.delayed_redraw)

    def configure_window(self, width, height):
        self.configure_surface(width, height)

    def _setup_handlers(self, source):
        pass
        
    def set_cursor(self, cursor):
        if self.figure is None:
            return

    def onscreen_message(self, text, delay=None):
        if self.figure is None:
            return
        ## if self.msgtask:
        ##     try:
        ##         self.figure.after_cancel(self.msgtask)
        ##     except:
        ##         pass
        self.message = text
        self.redraw(whence=3)
        if delay:
            ms = int(delay * 1000.0)
            ## self.msgtask = self.figure.after(ms,
            ##                                   lambda: self.onscreen_message(None))

class ImageViewBokeh(ImageView.ImageViewBase):
    """
    This version does all the graphical overlays on the client side.
    """

    def __init__(self, logger=None, rgbmap=None, settings=None):
        ImageView.ImageViewBase.__init__(self, logger=logger,
                                         rgbmap=rgbmap,
                                         settings=settings)
        # Our Bokeh plot
        self.figure = None
        # Holds the image on the plot
        self.bkimage = None
        self._push_server = False

        self.defer_redraw = False

        # NOTE: Bokeh manages it's Y coordinates by default with
        # the origin at the bottom (see base class)
        self._originUpper = False

        self.img_fg = (1.0, 1.0, 1.0)
        self.img_bg = (0.5, 0.5, 0.5)

        self.message = None

        # Bokeh expects RGBA data for color images
        self._rgb_order = 'RGBA'

        self.renderer = CanvasRenderer(self)

        # cursors
        self.cursor = {}

        # For timing events
        self._msg_timer = None
        self._defer_timer = None

        self.t_.setDefaults(show_pan_position=False,
                            onscreen_ff='Sans Serif')

    def set_figure(self, figure):
        """Call this with the Bokeh figure object."""
        self.figure = figure
        self.bkimage = None

        wd = figure.plot_width
        ht = figure.plot_height

        #figure.responsive = True

        # set background color
        #figure.background_fill = self.img_bg

        self.configure_window(wd, ht)

        #self._defer_timer = curdoc().add_periodic_callback(self.timer_cb, 50)

    def get_figure(self):
        return self.figure

    def get_rgb_order(self):
        return self._rgb_order

    def render_image(self, rgbobj, dst_x, dst_y):
        """Render the image represented by (rgbobj) at dst_x, dst_y
        in the pixel space.

        NOTE: this version uses a Figure.FigImage to render the image.
        """
        self.logger.debug("redraw surface")
        if self.figure is None:
            return

        # Grab the RGB array for the current image and place it in the
        # Bokeh image
        data = self.getwin_array(order=self._rgb_order)
        ht, wd = data.shape[:2]

        # Bokeh expects a 32-bit uint array type
        view = data.view(dtype=numpy.uint32).reshape((ht, wd))

        dst_x = dst_y = 0

        # Create an Image_RGBA object in the plot
        if self.bkimage is None:
            self.bkimage = self.figure.image_rgba(image=[view],
                                                  x=[dst_x], y=[dst_y],
                                                  dw=[wd], dh=[ht])
            d_src = self.bkimage.data_source
            self._setup_handlers(d_src)

        else:
            # note: get the data source (a ColumnDataSource) and update
            # the values
            d_src = self.bkimage.data_source
            d_src.data["image"] = [view]
            d_src.data["x"] = [dst_x]
            d_src.data["y"] = [dst_y]
            d_src.data["dw"] = [wd]
            d_src.data["dh"] = [ht]


        # Draw a cross in the center of the window in debug mode
        if self.t_['show_pan_position']:
            # size of cross is 4% of max window dimensions
            size = int(max(ht, wd) * 0.04)

            ctr_x, ctr_y = self.get_center()
            self.figure.cross(x=[ctr_x], y=[ctr_y],
                              size=size, color='red', line_width=1)

        # render message if there is one currently
        if self.message:
            y = (ht // 3) * 2
            x = (wd // 2)
            self.figure.text(x=[x], y=[y], text=[self.message],
                             text_font_size='24', text_baseline='middle',
                             text_color='white',
                             text_align='center')

        if self._push_server:
            # force an update of the figure
            try:
                cursession().store_objects(d_src)
                # TODO: if and when to use this?
                #self.bkimage.data_source.push_notebook()
                #push_notebook()

            except Exception as e:
                self.logger.warn("Can't update bokeh plot: %s" % (str(e)))


    def configure_window(self, width, height):
        self.configure(width, height)

    def _setup_handlers(self, source):
        pass
        
    def _resize_cb(self, event):
        wd, ht = event.width, event.height
        self.logger.debug("canvas resized %dx%d" % (wd, ht))
        self.configure_window(wd, ht)

    def get_png_image_as_buffer(self, output=None):
        ibuf = output
        if ibuf is None:
            ibuf = BytesIO()
        qimg = self.figure.write_to_png(ibuf)
        return ibuf

    def update_image(self):
        pass

    def set_cursor(self, cursor):
        pass

    def define_cursor(self, ctype, cursor):
        self.cursor[ctype] = cursor

    def get_cursor(self, ctype):
        return self.cursor[ctype]

    def switch_cursor(self, ctype):
        self.set_cursor(self.cursor[ctype])

    def set_fg(self, r, g, b):
        self.img_fg = (r, g, b)
        self.redraw(whence=3)

    def onscreen_message(self, text, delay=None):
        ## try:
        ##     self._msg_timer.stop()
        ## except:
        ##     pass

        self.message = text
        self.redraw(whence=3)

        if delay:
            time_ms = int(delay * 1000.0)
            ## self._msg_timer.interval = time_ms
            ## self._msg_timer.start()

    def onscreen_message_off(self):
        return self.onscreen_message(None)

    def reschedule_redraw(self, time_sec):

        if self._defer_timer is None:
            self.delayed_redraw()
            return

        ## try:
        ##     self._defer_timer.stop()
        ## except:
        ##     pass

        time_ms = int(time_sec * 1000)
        try:
            self._defer_timer.interval = time_ms
            self._defer_timer.start()

        except Exception as e:
            self.logger.warn("Exception starting timer: %s; "
                             "using unoptomized redraw" % (str(e)))
            self.delayed_redraw()

    def show_pan_mark(self, tf):
        self.t_.set(show_pan_position=tf)
        self.redraw(whence=3)

    def timer_cb(self, *args):
        self.logger.info("timer")


class ImageViewEvent(ImageViewBokeh):

    def __init__(self, logger=None, rgbmap=None, settings=None):
        ImageViewBokeh.__init__(self, logger=logger, rgbmap=rgbmap,
                                settings=settings)

        # last known window mouse position
        self.last_win_x = 0
        self.last_win_y = 0
        # last known data mouse position
        self.last_data_x = 0
        self.last_data_y = 0
        # Does widget accept focus when mouse enters window
        self.enter_focus = self.t_.get('enter_focus', True)

        # @$%&^(_)*&^ gnome!!
        self._keytbl = {
            'shift': 'shift_l',
            'control': 'control_l',
            'alt': 'alt_l',
            'win': 'super_l',
            '`': 'backquote',
            '"': 'doublequote',
            "'": 'singlequote',
            '\\': 'backslash',
            ' ': 'space',
            # NOTE: not working
            'escape': 'escape',
            'enter': 'return',
            # NOTE: not working
            'tab': 'tab',
            # NOTE: all Fn keys not working
            'f1': 'f1',
            'f2': 'f2',
            'f3': 'f3',
            'f4': 'f4',
            'f5': 'f5',
            'f6': 'f6',
            'f7': 'f7',
            'f8': 'f8',
            'f9': 'f9',
            'f10': 'f10',
            'f11': 'f11',
            'f12': 'f12',
            }

        # Define cursors for pick and pan
        #hand = openHandCursor()
        hand = 0
        self.define_cursor('pan', hand)
        #cross = thinCrossCursor('aquamarine')
        cross = 1
        self.define_cursor('pick', cross)

        for name in ('motion', 'button-press', 'button-release',
                     'key-press', 'key-release', 'drag-drop',
                     'scroll', 'map', 'focus', 'enter', 'leave',
                     ):
            self.enable_callback(name)

    def set_figure(self, figure):
        super(ImageViewEvent, self).set_figure(figure)

    def _setup_handlers(self, source):
        #source.on_change('selected', self, 'select_event_cb')
        source.on_change('selected', self.select_event_cb)

        ## self._box_select_tool = self.figure.select(dict(type=BoxSelectTool))
        ## self._box_select_tool.select_every_mousemove = True
        self._pan_tool = self.figure.select(dict(type=PanTool))
        #self._pan_tool.select_every_mousemove = True

        ## connect = figure.canvas.mpl_connect
        ## #connect("map_event", self.map_event)
        ## #connect("focus_in_event", self.focus_event, True)
        ## #connect("focus_out_event", self.focus_event, False)
        ## connect("figure_enter_event", self.enter_notify_event)
        ## connect("figure_leave_event", self.leave_notify_event)
        ## #connect("axes_enter_event", self.enter_notify_event)
        ## #connect("axes_leave_event", self.leave_notify_event)
        ## connect("motion_notify_event", self.motion_notify_event)
        ## connect("button_press_event", self.button_press_event)
        ## connect("button_release_event", self.button_release_event)
        ## connect("key_press_event", self.key_press_event)
        ## connect("key_release_event", self.key_release_event)
        ## connect("scroll_event", self.scroll_event)

        # TODO: drag-drop event
        pass

    def transkey(self, keyname):
        self.logger.debug("matplotlib keyname='%s'" % (keyname))
        if keyname is None:
            return keyname
        try:
            return self._keytbl[keyname.lower()]

        except KeyError:
            return keyname

    def get_keyTable(self):
        return self._keytbl

    def set_enter_focus(self, tf):
        self.enter_focus = tf

    def focus_event(self, event, hasFocus):
        return self.make_callback('focus', hasFocus)

    def enter_notify_event(self, event):
        if self.enter_focus:
            self.focus_event(event, True)
        return self.make_callback('enter')

    def leave_notify_event(self, event):
        self.logger.debug("leaving widget...")
        if self.enter_focus:
            self.focus_event(event, False)
        return self.make_callback('leave')

    def key_press_event(self, event):
        keyname = event.key
        keyname = self.transkey(keyname)
        if keyname is not None:
            self.logger.debug("key press event, key=%s" % (keyname))
            return self.make_ui_callback('key-press', keyname)

    def key_release_event(self, event):
        keyname = event.key
        keyname = self.transkey(keyname)
        if keyname is not None:
            self.logger.debug("key release event, key=%s" % (keyname))
            return self.make_ui_callback('key-release', keyname)

    def button_press_event(self, event):
        x, y = event.x, event.y
        button = 0
        if event.button in (1, 2, 3):
            button |= 0x1 << (event.button - 1)
        self.logger.debug("button event at %dx%d, button=%x" % (x, y, button))

        data_x, data_y = self.get_data_xy(x, y)
        return self.make_ui_callback('button-press', button, data_x, data_y)

    def button_release_event(self, event):
        x, y = event.x, event.y
        button = 0
        if event.button in (1, 2, 3):
            button |= 0x1 << (event.button - 1)
        self.logger.debug("button release at %dx%d button=%x" % (x, y, button))

        data_x, data_y = self.get_data_xy(x, y)
        return self.make_ui_callback('button-release', button, data_x, data_y)

    def select_event_cb(self, attrname, old_val, new_val):
        print("select cb: %s <-- %s" % (attrname, str(new_val)))
        self.logger.info("select cb: %s <-- %s" % (attrname, str(new_val)))
        
    def get_last_win_xy(self):
        return (self.last_win_x, self.last_win_y)

    def get_last_data_xy(self):
        return (self.last_data_x, self.last_data_y)

    def motion_notify_event(self, event):
        button = 0
        x, y = event.x, event.y
        self.last_win_x, self.last_win_y = x, y

        if event.button in (1, 2, 3):
            button |= 0x1 << (event.button - 1)
        self.logger.debug("motion event at %dx%d, button=%x" % (x, y, button))

        data_x, data_y = self.get_data_xy(x, y)
        self.last_data_x, self.last_data_y = data_x, data_y
        self.logger.debug("motion event at DATA %dx%d" % (data_x, data_y))

        return self.make_ui_callback('motion', button, data_x, data_y)

    def scroll_event(self, event):
        x, y = event.x, event.y

        # Matplotlib only gives us the number of steps of the scroll,
        # positive for up and negative for down.  No horizontal scrolling.
        direction = None
        if event.step > 0:
            direction = 0.0
        elif event.step < 0:
            direction = 180.0

        amount = abs(event.step) * 15.0

        self.logger.debug("scroll deg=%f direction=%f" % (
            amount, direction))

        data_x, data_y = self.get_data_xy(x, y)
        self.last_data_x, self.last_data_y = data_x, data_y

        return self.make_ui_callback('scroll', direction, amount,
                                  data_x, data_y)

class ImageViewZoom(Mixins.UIMixin, ImageViewEvent):

    # class variables for binding map and bindings can be set
    bindmapClass = Bindings.BindingMapper
    bindingsClass = Bindings.ImageViewBindings

    @classmethod
    def set_bindingsClass(cls, klass):
        cls.bindingsClass = klass

    @classmethod
    def set_bindmapClass(cls, klass):
        cls.bindmapClass = klass

    def __init__(self, logger=None, rgbmap=None, settings=None,
                 bindmap=None, bindings=None):
        ImageViewEvent.__init__(self, logger=logger, rgbmap=rgbmap,
                                settings=settings)
        Mixins.UIMixin.__init__(self)

        self.ui_setActive(True)

        if bindmap is None:
            bindmap = ImageViewZoom.bindmapClass(self.logger)
        self.bindmap = bindmap
        bindmap.register_for_events(self)

        if bindings is None:
            bindings = ImageViewZoom.bindingsClass(self.logger)
        self.set_bindings(bindings)

    def get_bindmap(self):
        return self.bindmap

    def get_bindings(self):
        return self.bindings

    def set_bindings(self, bindings):
        self.bindings = bindings
        bindings.set_bindings(self)


class CanvasView(ImageViewZoom):

    def __init__(self, logger=None, settings=None, rgbmap=None,
                 bindmap=None, bindings=None):
        ImageViewZoom.__init__(self, logger=logger, settings=settings,
                               rgbmap=rgbmap,
                               bindmap=bindmap, bindings=bindings)

        # Needed for UIMixin to propagate events correctly
        self.objects = [self.private_canvas]

    def set_canvas(self, canvas, private_canvas=None):
        super(CanvasView, self).set_canvas(canvas,
                                           private_canvas=private_canvas)

        self.objects[0] = self.private_canvas


        
#END
