###############################################################################
# File:        pgWxDisplay.py
# Appointment: PyGame display in wx frame
# Project:     pg
# Author:      AR
# History:
# 28.10.2013 Added OnMouseLeftDoubleClickEvent
# 25.09.2012 Created
###############################################################################
import zx
import wxTools

import wx
import pygame
import platform

#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Base class for pygame based frames
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
class CPygameDisplay(wx.ScrolledWindow):

    #---------------------------------------------------------------------------
    # Ctor parameters:
    #     aParent - parent window class
    #     aID - window ID
    #     aPygameDrawFunc - function making actual drawing on pygame screen
    #     aTotalArea - total virtual area
    #     aDrawArea - window draw area
    #     minScale/maxScale - scale change range
    #     scaleDelta - delta to change the scale
    def __init__(self, aParent, aID, aPygameDrawFunc, aTotalArea = zx.CArea(), aDrawArea = zx.CArea(),
                 minScale = 0.1, maxScale = 1, scaleDelta = 0.1):
        wx.ScrolledWindow.__init__(self, aParent, id = aID)
        
        self._pygameDrawFunc = aPygameDrawFunc
        self._size = self.GetSizeTuple()
        self._size_dirty = True
        
        self._totalArea = aTotalArea
        
        # Events to be redefined by user
        self.OnKeyEvent = None
        self.OnMouseMoveEvent = None
        self.OnMouseLeftDownEvent = None
        self.OnMouseLeftUpEvent = None
        self.OnMouseWheelEvent = None
        self.OnMouseLeftDoubleClickEvent = None
        self.OnScaleChanged = None
        
        # This 'func pointer' can be redefined to check if any picture change allowed
        self.FuncCanChangeDisplay = None
        
        # Initial scale
        self._minScale = 0.01 if minScale <= 0 else minScale
        self._maxScale = self._minScale if maxScale <= self._minScale else maxScale
        self._scale = self._maxScale
        self._scaleDelta = scaleDelta
        
        # Draw area coordinates
        self.SetDrawArea(aDrawArea)
       
        # Bind events
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        if platform.system() == "Windows":
            self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) # needed for windows only!?
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.Bind(wx.EVT_KEY_UP, self.OnKeyUp)

        self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
        self.Bind(wx.EVT_MOTION, self.OnMouseMove)
        self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
        self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseLeftDoubleClick)

        # Disable scrolling (very negative effect on Windows)
        self.EnableScrolling(False, False)
        
        self._flexFrame = None
        self._keyMonitor = zx.CKeysMonitor()
        self._mouseScroller = wxTools.CMouseScroller(self)
        
        # Paint locker to reduce blinking
        self.__LockPaint = False

        # Flags allowing rescale        
        self.CanRescale = True


    #---------------------------------------------------------------------------
    # Helper: returns true if display can be changed
    def __CanChangeDisplay(self):
        return self.FuncCanChangeDisplay() if self.FuncCanChangeDisplay is not None else True
        
    #---------------------------------------------------------------------------
    # Converts virtual coordinates to screen
    def VirtualToScreenX(self, x):
        l, t = self.GetViewStart()
        ls, ts = self.GetScrollPixelsPerUnit()
        return int(x/self._scale - l*ls)
    def VirtualToScreenY(self, y):
        l, t = self.GetViewStart()
        ls, ts = self.GetScrollPixelsPerUnit()
        return int(y/self._scale - t*ts)

    # Converts screen coordinates to virtual
    def ScreenToVirtualX(self, x):
        l, t = self.GetViewStart()
        ls, ts = self.GetScrollPixelsPerUnit()
        return (x + l*ls)*self._scale
    #---------------------------------------------------------------------------
    def ScreenToVirtualY(self, y):
        l, t = self.GetViewStart()
        ls, ts = self.GetScrollPixelsPerUnit()
        return (y + t*ts)*self._scale

    #---------------------------------------------------------------------------
    # Returns True if an object is visible with gived screen coordinates and radius
    def IsVisibleOnScreen(self, aScreenX, aScreenY, aRadius):
        return aScreenX + aRadius >= 0 and aScreenX - aRadius <= self._screen.get_width() and \
               aScreenY + aRadius >= 0 and aScreenY - aRadius <= self._screen.get_height()

    #---------------------------------------------------------------------------
    # Returns True if both points are off screen
    def AreOffScreen(self, aScreenX1, aScreenY1, aScreenX2, aScreenY2):
        return aScreenX1 < 0 and aScreenX2 < 0 or\
               aScreenY1 < 0 and aScreenY2 < 0 or\
               aScreenX1 > self._screen.get_width() and aScreenX2 > self._screen.get_width() or\
               aScreenY1 > self._screen.get_height() and aScreenY2 > self._screen.get_height()

    #---------------------------------------------------------------------------
    # Explicitly sets scale
    def SetScale(self, scale):
        if not self.CanRescale:
            return
            
        scale = self.CorrectScale(scale)
        if scale > 0 and scale != self._scale and self.__CanChangeDisplay():
            self.__LockPaint = True
            # This must be done here to avoid unwanted scrolling on mouse wheel
            if self.OnScaleChanged is not None:
                self.OnScaleChanged(scale)

            # Calculate centre coordinates before rescaling
            xcentre = self.GetParent().GetClientSize().GetWidth()/2
            ycentre = self.GetParent().GetClientSize().GetHeight()/2
            x = self.ScreenToVirtualX(xcentre)
            y = self.ScreenToVirtualY(ycentre)
            
            # Rescale
            self._scale = scale
            self.RescaleWindow()
            
            # Calculate new screen coordinates
            xscr = self.VirtualToScreenX(x)
            yscr = self.VirtualToScreenY(y)

            # Restore the picture by scrolling
            self.Scroll(xscr - xcentre, yscr - ycentre)

            # Repaint the picture (has to be done explicitly here to avoid obscure image)
            self.__LockPaint = False
            self.Paint()
            

    #---------------------------------------------------------------------------
    def GetScale(self):
        return self._scale

    #---------------------------------------------------------------------------
    # Corrects scale if it goes out of limits
    def CorrectScale(self, scale):   
        if scale < self._minScale:
            scale = self._minScale
        if scale > self._maxScale:
            scale = self._maxScale
 
        return scale
        
    #---------------------------------------------------------------------------
    # Calculates drawing area depending on current scale
    def RescaleWindow(self):
        # Recalculate area depending on the scale
        a = zx.CArea(self._totalArea.GetLeft()/self._scale, 
                     self._totalArea.GetTop()/self._scale, 
                     self._totalArea.GetWidth()/self._scale,
                     self._totalArea.GetHeight()/self._scale)
        
        # set the area
        self.SetDrawArea(a)

    #---------------------------------------------------------------------------

    # Sets draw area
    def SetDrawArea(self, aDrawArea):    
        self.SetScrollbars(1, 1, aDrawArea.Width, aDrawArea.Height)
        self.SetVirtualSize(wx.Size(aDrawArea.Width, aDrawArea.Height))


    #---------------------------------------------------------------------------
    # Increasing scale on 1 step
    def IncScale(self, step = 0.1):
        scale = self._scale + step
        if scale > self._maxScale:
            scale = self._maxScale
        if scale != self._scale:
            self.SetScale(scale)

    #---------------------------------------------------------------------------
    # Decreasing scale on 1 step
    def DecScale(self, step = 0.1):
        scale = self._scale - step
        if scale < self._minScale:
            scale = self._minScale
        if scale != self._scale:
            self.SetScale(scale)

        
    #---------------------------------------------------------------------------
    def Paint(self, aDC = None):
        if not self.__LockPaint:
            # Set pygame screen
            if self._size_dirty:
                self._screen = pygame.Surface(self._size, 0, 32)
                self._size_dirty = False
              
            # Draw on pygame screen
            self._pygameDrawFunc(self._screen)

            # Now display picture in wx frame
            s = pygame.image.tostring(self._screen, 'RGB')  # Convert the surface to an RGB string
            img = wx.ImageFromData(self._size[0], self._size[1], s)  # Load this string into a wx image
            bmp = wx.BitmapFromImage(img)  # Get the image in bitmap form
            dc = aDC if aDC is not None else wx.ClientDC(self)  # Device context for drawing the bitmap
            dc.DrawBitmap(bmp, 0, 0, False)  # Blit the bitmap image to the display

    #---------------------------------------------------------------------------
    def RePaint(self):
        self._size_dirty = True
        self.Paint()
 
    #---------------------------------------------------------------------------
    def OnPaint(self, event):
        if not self.__LockPaint:
            self.Paint(wx.PaintDC(self))
            event.Skip()  # Make sure the parent frame gets told to redraw as well

 
    #---------------------------------------------------------------------------
    # For insane windows only to avoid flicker
    def OnEraseBackground(self, event):
        pass
        
    #---------------------------------------------------------------------------
    def OnSize(self, event):
        self._size = self.GetSizeTuple()
        self._size_dirty = True
        event.Skip()


    #---------------------------------------------------------------------------
    def OnKeyDown(self, event):
        c = event.GetKeyCode()
        self._keyMonitor.AddKey(c)
        # Process user event
        if self.OnKeyEvent is not None:
            if self.OnKeyEvent(event, c):
                return

        if self.__CanChangeDisplay():
            if c == 45 or c == 390:
                self.IncScale(self._scaleDelta)
                return
            if c == 61 or c == 388:
                self.DecScale(self._scaleDelta)
                return

            x, y = self.GetViewStart()
            if c == wx.WXK_LEFT:
                self.Scroll(x - 10, y)
            elif c == wx.WXK_RIGHT:
                self.Scroll(x + 10, y)
            elif c == wx.WXK_UP:
                self.Scroll(x, y - 10)
            elif c == wx.WXK_DOWN:
                self.Scroll(x, y + 10)


    #---------------------------------------------------------------------------
    def OnKeyUp(self, event):
        self._keyMonitor.RemoveKey(event.GetKeyCode())        


    #---------------------------------------------------------------------------
    def OnMouseLeftDown(self, event):
        # Process user event
        if self.OnMouseLeftDownEvent is not None:
            if self.OnMouseLeftDownEvent(event):
                return

        if self._flexFrame is None and self._keyMonitor.FindKey(wx.WXK_CONTROL):
            self._flexFrame = wxTools.CFlexFrame(self, event.GetX(), event.GetY())


    #---------------------------------------------------------------------------
    def OnMouseLeftUp(self, event):
        # Process user event
        if self.OnMouseLeftUpEvent is not None:
            if self.OnMouseLeftUpEvent(event):
                return

        # Process and reset flex painter
        if self._flexFrame is not None:
            # Do rescale only if right ctrl is pressed
            if self._keyMonitor.FindKey(wx.WXK_CONTROL):
                # New width/height
                w = abs(self._flexFrame.StartX - event.GetX())
                h = abs(self._flexFrame.StartY - event.GetY())
                scale = -1.0
                if w != 0 and h != 0:
                    # New left/top coordinates
                    l = self._flexFrame.StartX if self._flexFrame.StartX < event.GetX() else event.GetX()
                    t = self._flexFrame.StartY if self._flexFrame.StartY < event.GetY() else event.GetY()
                    
                    # Calc new scale
                    scale1 = w/float(self._size[0])
                    scale2 = h/float(self._size[1])
                    scale = self.CorrectScale(self._scale*(scale1 if scale1 > scale2 else scale2))

                # Reset the frame first (before redrawing)                 
                self._flexFrame = None
                # Rescale
                if scale > 0 and scale != self._scale:
                    # Coordinates before rescaling
                    x = self.ScreenToVirtualX(l)
                    y = self.ScreenToVirtualY(t)

                    self._scale = scale
                    self.RescaleWindow()
            
                    # Scroll ton the top left corner
                    xscr = self.VirtualToScreenX(x)
                    yscr = self.VirtualToScreenX(y)
                    self.Scroll(xscr, yscr)

                    self.RePaint()
            else:
                # Right ctrl not pressed - just remove the frame
                self._flexFrame = None
                
        # Reset mouse scroller
        self._mouseScroller.Reset()
        
    #---------------------------------------------------------------------------
    def OnMouseMove(self, event):
        # Process user event
        if self.OnMouseMoveEvent is not None:
            if self.OnMouseMoveEvent(event):
                return

        if event.LeftIsDown():
            if self._flexFrame is not None:
                # Avoid drawing if coordinates are negative - bad visual effect
                if event.GetX() >= 0 and event.GetY() >= 0:
                    self._flexFrame.Draw(event.GetX(), event.GetY())
            else:
                self._mouseScroller.Scroll(event.GetX(), event.GetY())
                
    #---------------------------------------------------------------------------
    def OnMouseWheel(self, event):
        # Process user event
        if self.OnMouseWheelEvent is not None:
            if self.OnMouseWheelEvent(event):
                return

        if event.GetWheelRotation() > 0:
            self.IncScale(self._scaleDelta)
        else:
            self.DecScale(self._scaleDelta)
            
    #---------------------------------------------------------------------------
    def OnMouseLeftDoubleClick(self, event):
        # Process user event
        if self.OnMouseLeftDoubleClickEvent is not None:
            self.OnMouseLeftDoubleClickEvent(event)
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

