###############################################################################
# File:        pgViewer.py
# Appointment: General class for pg game
# Project:     pg
# Author:      AR
# History:
# 24.02.2013 Created from guBattle
###############################################################################
import pygame, os

import zx
import pgTools


#==============================================================================
# Configurable pygame display viewer
#==============================================================================
class CViewer(zx.CCfgHolder):
    DEFAULT_WIDTH = 640
    DEFAULT_HEIGHT = 480
    MIN_SIZE = 3
    
    #--------------------------------------------------------------------------
    # Handled viewer object
    class CObject:
        #----------------------------------------------------------------------
        # aPos - object position on a virtual screen
        # aSize - object size
        def __init__(self, aPos, aImage, aSize):
            self.Pos = aPos
            self.OrigImage = aImage # original image
            self.Size = aSize
            self.Rect = aImage.get_rect()
            self.ScaledImage = self.OrigImage # scaled image used to draw
            self.IsVisible = True
    
    #--------------------------------------------------------------------------
    # aNaturalScale - the scale one to show 1:1 case
    # a*InfoSpace specifies not used for viewer areas from left/top/right/bottom. 
    # These areas can be used to display additional information and controls
    def __init__(self, aCfg, aCfgPath, aCfgName, aNaturalScale = 1, 
                 aLeftInfoSpace = 0,
                 aRightInfoSpace = 0,
                 aTopInfoSpace = 0,
                 aBottomInfoSpace = 0,
                 aMaxWidth = 0,
                 aMaxHeight = 0):
        # Set dysplay size from config
        self.Cfg = zx.CCfgHolder(aCfg, aCfgPath, aCfgName)
        width = int(self.Cfg.GetValue("width", str(self.DEFAULT_WIDTH)))
        height = int(self.Cfg.GetValue("height", str(self.DEFAULT_HEIGHT)))
        os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (100, 100)
        pygame.init()
        
        self.MaxWidth = aMaxWidth
        self.MaxHeight = aMaxHeight
        if self.MaxWidth != 0 and self.MaxWidth < self.DEFAULT_WIDTH:
            self.MaxWidth = self.DEFAULT_WIDTH
        if self.MaxHeight != 0 and self.MaxHeight < self.DEFAULT_HEIGHT:
            self.MaxHeight = self.DEFAULT_HEIGHT
        self.__SetScreenMode(width, height)
        
        self.NaturalScale = float(aNaturalScale) # natural scale
        self.ScalingEnabled = True 
        
        self.ScrollEnabled = True
        
        # Background image (for quick repaint)
        self.__BackGround = None

        # Prepare containers and scale
        self.ResetObjects()
        
        # Info space
        def AdjustInfoSpace(aMax, aValue):
            return aValue if aValue <= aMax/4 else aMax/4
        self.__LeftInfoSpace = AdjustInfoSpace(self.Screen.get_width(), aLeftInfoSpace)
        self.__TopInfoSpace = AdjustInfoSpace(self.Screen.get_height(), aTopInfoSpace)
        self.__RightInfoSpace = AdjustInfoSpace(self.Screen.get_width(), aRightInfoSpace)
        self.__BottomInfoSpace = AdjustInfoSpace(self.Screen.get_height(), aBottomInfoSpace)


    #--------------------------------------------------------------------------
    def __del__(self):
        # Save config
        self.Cfg.SetValue("width", str(self.Screen.get_width()))
        self.Cfg.SetValue("height", str(self.Screen.get_height()))
        
        # User event
        self.OnClose()
        
        # Close display
        pygame.display.quit()    
        

    #--------------------------------------------------------------------------
    # Helper - sets screen to desired size according to limitations
    def __SetScreenMode(self, aWidth, aHeight):
        if aWidth < self.DEFAULT_WIDTH:
            aWidth = self.DEFAULT_WIDTH
        if aHeight < self.DEFAULT_HEIGHT:
            aHeight = self.DEFAULT_HEIGHT
        if self.MaxWidth > 0 and aWidth > self.MaxWidth:
            aWidth = self.MaxWidth
        if self.MaxHeight > 0 and aHeight > self.MaxHeight:
            aHeight = self.MaxHeight
        if pygame.display.mode_ok((aWidth, aHeight)) == 0:
            aWidth = self.DEFAULT_WIDTH
            aHeight = self.DEFAULT_HEIGHT
        self.Screen = pygame.display.set_mode((aWidth, aHeight), pygame.RESIZABLE)


    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # This event called from dtor
    def OnClose(self):
        pass
    
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # This event called after scale changed
    def AfterScaleChanged(self, aScale):
        pass

    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # This event called before user changes scale
    def BeforeUserChangeScale(self, aNewScale):
        pass

    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # This virtual returns an image to be drawn on the screen
    def GetObjScaledImage(self, aKey, aObject):
        return aObject.ScaledImage
    
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # This virtual is called after object was drawn on the screen
    def AfterDrawObject(self, aId, aObject):
        pass
    
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Events for redefining: should return true to avoid default processing
    def OnKeyDown(self, aEvent):
        return False


    #--------------------------------------------------------------------------
    # Returns current background
    def GetBackground(self):
        if self.__BackGround is None:
            self.__BackGround = self.Screen.copy()
        return self.__BackGround
        
    #--------------------------------------------------------------------------
    # Resets background
    def ResetBackground(self):
        self.__BackGround = None
        
    #--------------------------------------------------------------------------
    # Resets all objects
    def ResetObjects(self, aRescale = True):
        # Dict of handled objects <id, object>
        self.Objects = {}
        
        if aRescale:
            # Scale
            self.Scale = self.NaturalScale # current scale
            
            # Current shift in virtual coordinates
            self.ShiftX = self.ShiftY = 0
            
            # Total virtual area, occupied by all objects
            self.VirtualArea = zx.CArea()

    
    #--------------------------------------------------------------------------
    # Return working screen width/height (excluding info space)
    def GetWorkingWidth(self):
        return self.Screen.get_width() - self.__LeftInfoSpace - self.__RightInfoSpace
    def GetWorkingHeight(self):
        return self.Screen.get_height() - self.__TopInfoSpace - self.__BottomInfoSpace
        
    #--------------------------------------------------------------------------
    # Recalc coordinates from virtual to screen
    def VirtualToScreenX(self, x, aUseShift = True):
        return (self.__LeftInfoSpace if aUseShift else 0) + (x - (self.ShiftX if aUseShift else 0)) * self.Scale
    def VirtualToScreenY(self, y, aUseShift = True):
        return (self.__TopInfoSpace if aUseShift else 0) + (y - (self.ShiftY if aUseShift else 0)) * self.Scale
    # Returns screen position
    def VirtualToScreen(self, aPosVirtual, aUseShift = True):
        return (self.VirtualToScreenX(aPosVirtual[0], aUseShift), self.VirtualToScreenY(aPosVirtual[1], aUseShift))


    #--------------------------------------------------------------------------
    # Recalc coordinates from screen to virtual
    def ScreenToVirtualX(self, x, aUseShift = True):
        return float(x - (self.__LeftInfoSpace if aUseShift else 0))/self.Scale + (self.ShiftX if aUseShift else 0)
    def ScreenToVirtualY(self, y, aUseShift = True):
        return float(y - (self.__TopInfoSpace if aUseShift else 0))/self.Scale + (self.ShiftY if aUseShift else 0)
    # Returns screen position
    def ScreenToVirtual(self, aPosScreen, aUseShift = True):
        return (self.ScreenToVirtualX(aPosScreen[0], aUseShift), self.ScreenToVirtualY(aPosScreen[1], aUseShift))
        

    #--------------------------------------------------------------------------
    # Registeres an object
    def AddObject(self, aId, aImage, aPos, aSize):
        self.Objects[aId] = CViewer.CObject(aPos, aImage, aSize)
        
        # Set virtual area
        k = aSize/self.NaturalScale
        self.VirtualArea.SetByPoints(min(self.VirtualArea.GetLeft(), aPos[0] - k),
                                     min(self.VirtualArea.GetTop(), aPos[1] - k),
                                     max(self.VirtualArea.GetRight(), aPos[0] + k),
                                     max(self.VirtualArea.GetBottom(), aPos[1] + k))

    #--------------------------------------------------------------------------
    # Makes suitable min size
    def ArrangeMinSize(self, aSize):
        return aSize if aSize >= self.MIN_SIZE else self.MIN_SIZE 
        
        
    #--------------------------------------------------------------------------
    # Explicitly rescales object o using scale koef k
    def ReScaleObject(self, o, k):
        transScale = self.ArrangeMinSize(int(o.Size * k))
        o.ScaledImage = pygame.transform.scale(o.OrigImage, (transScale, transScale))
        o.Rect = o.ScaledImage.get_rect()
    
    #--------------------------------------------------------------------------
    # Rescales all images. Returns True if re-scale has happen
    # Redraws the screen if aRedraw is True; 
    # aForce: if true - forcing rescale even if scale does not change
    def ReScale(self, aNewScale, aRedraw = True, aForce = False):
        if self.ScalingEnabled and (self.Scale != aNewScale or aForce):
            # Get virtual coordinates of the screen center before rescaling
            screenCenterX = float(self.GetWorkingWidth())/2
            screenCenterY = float(self.GetWorkingHeight())/2
            x = self.ScreenToVirtualX(screenCenterX)
            y = self.ScreenToVirtualY(screenCenterY)
            
            # Rescale images
            k = float(aNewScale/self.NaturalScale)
            for o in self.Objects.itervalues():
                self.ReScaleObject(o, k)
                
            self.Scale = aNewScale
            self.AfterScaleChanged(self.Scale) # call event

            # Make sure the virtual x,y are in the screen centre
            self.ShiftX -= self.ScreenToVirtualX(screenCenterX) - x
            self.ShiftY -= self.ScreenToVirtualY(screenCenterY) - y
            
            if aRedraw:
                self.ReDraw()
            return True
        return False
            
            
    #--------------------------------------------------------------------------
    # Draws background
    def DrawBackground(self):
        self.Screen.fill((0,0,0)) # just black by default


    #--------------------------------------------------------------------------
    # Helper: draws info rects
    def DrawInfoRects(self, aRects, aColour):
        for rect in aRects:
            if rect.width > 0 and rect.height > 0:
                pygame.draw.rect(self.Screen, aColour, rect)
        
    #--------------------------------------------------------------------------
    # Draws info areas
    def DrawInfoArea(self):
        if self.__LeftInfoSpace > 0:
            rects = []
            rects.append(pygame.Rect(0, 
                                     0, 
                                     self.__LeftInfoSpace, 
                                     self.__TopInfoSpace))
            rects.append(pygame.Rect(rects[0].left, 
                                     rects[0].height, 
                                     rects[0].width, 
                                     self.Screen.get_height() - self.__BottomInfoSpace - self.__TopInfoSpace))
            rects.append(pygame.Rect(rects[0].left, 
                                     rects[0].height + rects[1].height, 
                                     rects[0].width, 
                                     self.__BottomInfoSpace))
            self.DrawLeftInfo(rects)    

        if self.__RightInfoSpace > 0:
            rects = []
            rects.append(pygame.Rect(self.Screen.get_width() - self.__RightInfoSpace, 
                                     0, 
                                     self.__RightInfoSpace, 
                                     self.__TopInfoSpace))
            rects.append(pygame.Rect(rects[0].left, 
                                     rects[0].height, 
                                     rects[0].width, 
                                     self.Screen.get_height() - self.__BottomInfoSpace - self.__TopInfoSpace))
            rects.append(pygame.Rect(rects[0].left, 
                                     rects[0].height + rects[1].height, 
                                     rects[0].width, 
                                     self.__BottomInfoSpace))
            self.DrawRightInfo(rects)    

        if self.__TopInfoSpace > 0:
            rects = []
            rects.append(pygame.Rect(0, 
                                     0, 
                                     self.__LeftInfoSpace, 
                                     self.__TopInfoSpace))
            rects.append(pygame.Rect(rects[0].width, 
                                     0, 
                                     self.Screen.get_width() - self.__RightInfoSpace - self.__LeftInfoSpace, 
                                     rects[0].height))
            rects.append(pygame.Rect(rects[0].width + rects[1].width, 
                                     0, 
                                     self.__RightInfoSpace, 
                                     rects[0].height))
            self.DrawTopInfo(rects)    

        if self.__BottomInfoSpace > 0:
            rects = []
            rects.append(pygame.Rect(0, 
                                     self.Screen.get_height() - self.__BottomInfoSpace, 
                                     self.__LeftInfoSpace, 
                                     self.__BottomInfoSpace))
            rects.append(pygame.Rect(rects[0].width, 
                                     rects[0].top, 
                                     self.Screen.get_width() - self.__RightInfoSpace - self.__LeftInfoSpace, 
                                     rects[0].height))
            rects.append(pygame.Rect(rects[0].width + rects[1].width, 
                                     rects[0].top, 
                                     self.__RightInfoSpace, 
                                     rects[0].height))
            self.DrawBottomInfo(rects)    

    #--------------------------------------------------------------------------
    # This virtuals supposed to be redefined if necessary
    # aRects is an array of 3 rects: 1 and 3 are intersections with other info areas, 2 - the middle
    def DrawLeftInfo(self, aRects):
        self.DrawInfoRects(aRects, (0,0,100))
    def DrawRightInfo(self, aRects):
        self.DrawInfoRects(aRects, (0,0,100))
    def DrawTopInfo(self, aRects):
        self.DrawInfoRects(aRects, (100,0,0))
    def DrawBottomInfo(self, aRects):
        self.DrawInfoRects(aRects, (100,0,0))


    #--------------------------------------------------------------------------
    # Returns true if rect (any part of it) is visible
    def IsRectVisible(self, aRect):
        return aRect.right >= self.__LeftInfoSpace and \
                 aRect.left <= self.Screen.get_width() - self.__RightInfoSpace and \
                   aRect.top <= self.Screen.get_height() - self.__BottomInfoSpace and \
                     aRect.bottom >= self.__TopInfoSpace
    

    #--------------------------------------------------------------------------
    # Returns true if any part of a rect is visible
    def IsRectOnScreen(self, aRect):
        return aRect.left >= self.__LeftInfoSpace and \
                 aRect.right <= self.Screen.get_width() - self.__RightInfoSpace and \
                   aRect.top >= self.__TopInfoSpace and \
                     aRect.bottom <= self.Screen.get_height() - self.__BottomInfoSpace
    
    #--------------------------------------------------------------------------
    # Returns true if object is entierly visible (no hidden parts)
    def IsObjectOnScreen(self, o):
        return self.IsRectOnScreen(o.Rect)


    #--------------------------------------------------------------------------
    # This function ensures that objects in the list are all on the screen
    # aDesiredScale - a scale to be kept, if possible
    def MakeObjectsOnScreen(self, aObjects, aDesiredScale = -1):
        # List to avoid unlimited loop
        ctrl = []
        justScaledDown = False # flag to avoid unlimited rescaling loop
        
        # Make sure area occupied by objects fits in the screen    
        while True:
            l = t = r = b = None
            onScreen = True
            for o in aObjects:
                if onScreen and not self.IsObjectOnScreen(o):
                    onScreen = False
                l = o.Rect.left if l is None else min(l, o.Rect.left)
                t = o.Rect.top if t is None else min(t, o.Rect.top)
                r = o.Rect.right if r is None else max(r, o.Rect.right)
                b = o.Rect.bottom if b is None else max(b, o.Rect.bottom)
            area = pygame.Rect(l, t, r - l + 1, b - t + 1)
            
            if area.width > self.GetWorkingWidth() or area.height > self.GetWorkingHeight():
                self.ReScale(self.Scale - self.Scale/100, True)
                justScaledDown = True
                continue
            elif aDesiredScale > 0 and self.Scale < aDesiredScale and \
                        area.width < self.GetWorkingWidth() and \
                            area.height < self.GetWorkingHeight() and \
                                not justScaledDown:
                self.ReScale(self.Scale + self.Scale/100, True)
                continue
                 
            if not onScreen:
                d = 1
                if area.left < self.__LeftInfoSpace:
                    self.ShiftX -= d
                elif area.right > self.Screen.get_width() - self.__RightInfoSpace:
                    self.ShiftX += d
                elif area.top < self.__TopInfoSpace:
                    self.ShiftY -= d
                elif area.bottom > self.Screen.get_height() - self.__BottomInfoSpace:
                    self.ShiftY += d
                self.ReDraw() # this should use current shifting
                if zx.Find(ctrl, lambda pos: pos[0] == self.ShiftX and pos[1] == self.ShiftY) != None:
                    break # to avoid unlimited loop
                ctrl.append((self.ShiftX, self.ShiftY))
                if len(ctrl) > 5:
                    ctrl.pop(0)
            else:
                break

    
    #--------------------------------------------------------------------------
    # Redraws all objects according to current scale
    def ReDraw(self):
        self.DrawBackground()
        # Draw in the order: biggest objects first (to avoid hiding smaller ones behind)
        for k, o in reversed(sorted(self.Objects.items(), key = lambda(k, v) : v.Rect.width)):
            pos = self.VirtualToScreen(o.Pos)
            if abs(pos[0]) > 999999 or abs(pos[1]) > 999999: # ignore possible 'escape' coordinates
                continue
            o.Rect.centerx = pos[0]
            o.Rect.centery = pos[1]
            # Draw visible objects only
            if o.IsVisible and self.IsRectVisible(o.Rect):
                image = self.GetObjScaledImage(k, o)
                # Recalc rect - it may changed
                o.Rect = image.get_rect()
                o.Rect.centerx = pos[0]
                o.Rect.centery = pos[1]
                self.Screen.blit(image, o.Rect)
                self.AfterDrawObject(k, o)
        self.DrawInfoArea()
        pygame.display.flip()
        self.ResetBackground()

    #--------------------------------------------------------------------------
    # Scroll screen on aDeltaX, aDeltaY
    def Scroll(self, aDeltaX, aDeltaY):
        if not self.ScrollEnabled:
            return
            
        oldX, oldY = self.ShiftX, self.ShiftY
        nx = self.ScreenToVirtualX(self.GetWorkingWidth()/2) + aDeltaX
        if nx >= self.VirtualArea.GetLeft() and nx <= self.VirtualArea.GetRight():
            self.ShiftX += aDeltaX
        ny = self.ScreenToVirtualY(self.GetWorkingHeight()/2) + aDeltaY
        if ny >= self.VirtualArea.GetTop() and ny <= self.VirtualArea.GetBottom():
            self.ShiftY += aDeltaY
        if oldX != self.ShiftX or oldY != self.ShiftY:
            self.ReDraw()
            
     
    #--------------------------------------------------------------------------
    # Changing scale
    def ChangeScale(self, aNewScale):
        if not self.ScalingEnabled:
            return
    
        if aNewScale > self.Scale:
            if aNewScale > self.NaturalScale:
                aNewScale = self.NaturalScale
        if aNewScale < self.Scale:
            if self.VirtualToScreenX(self.VirtualArea.GetWidth(), False)*2 > self.GetWorkingWidth() or \
                    self.VirtualToScreenY(self.VirtualArea.GetHeight(), False)*2 > self.GetWorkingHeight():
                if aNewScale < 0.5:
                    aNewScale = 0.5
            else:
                return
        
        self.BeforeUserChangeScale(aNewScale)
        self.ReScale(aNewScale)

    #--------------------------------------------------------------------------
    # Adjusting screen to feet all objects in visible area
    def AdjustScreen(self):
        if not self.ScrollEnabled:
            return
            
        # Adjust scale to fit the window
        scale = self.Scale
        if self.VirtualToScreenX(self.VirtualArea.GetWidth(), False) > self.GetWorkingWidth():
            scale = float(self.GetWorkingWidth())/self.VirtualArea.GetWidth()
        if self.VirtualToScreenY(self.VirtualArea.GetHeight(), False) > self.GetWorkingHeight():
            scaleH = float(self.GetWorkingHeight())/self.VirtualArea.GetHeight()
            scale = min(scale, scaleH)
        self.ReScale(scale, False) # rescale withowt redrawing

        # Center the visible area
        k = self.ScreenToVirtualX(self.GetWorkingWidth(), False) - self.VirtualArea.GetWidth()
        self.ShiftX = self.VirtualArea.GetLeft() - (k/2 if k > 0 else 0)
        k = self.ScreenToVirtualY(self.GetWorkingHeight(), False) - self.VirtualArea.GetHeight()
        self.ShiftY = self.VirtualArea.GetTop() - (k/2 if k > 0 else 0)


    #---------------------------------------------------------------------------
    # Playing
    def Go(self):
        # Adjust scale to fit the window
        self.AdjustScreen()
        
        # Main loop
        self.ReDraw()
        mouseKeyMonitor = zx.CKeysMonitor() # monitor to catch mouse clicks
        while True:  
            event = pygame.event.wait()  
            if event.type == pygame.QUIT:
                break
            
            # Key events
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    break
                # Call user event first
                if self.OnKeyDown(event):
                    continue # processed
                    
                # Default processing
                if event.key == pygame.K_F2:
                    if self.ScalingEnabled:
                        self.Scale = self.NaturalScale
                        self.AdjustScreen()
                        self.ReDraw()
                
                # Keydown loop
                while not pygame.event.peek(pygame.KEYUP):
                    if self.ScalingEnabled:
                        # Rescale
                        if event.key == pygame.K_PLUS or event.key == pygame.K_KP_PLUS:
                            self.ChangeScale(self.Scale + 0.1)
                        elif event.key == pygame.K_MINUS or event.key == pygame.K_KP_MINUS:
                            self.ChangeScale(self.Scale - 0.1)
                    
                    # Scroll
                    if event.key == pygame.K_LEFT:
                        self.Scroll(-1, 0)
                    elif event.key == pygame.K_RIGHT:
                        self.Scroll(1, 0)
                    elif event.key == pygame.K_UP:
                        self.Scroll(0, -1)
                    elif event.key == pygame.K_DOWN:
                        self.Scroll(0, 1)
                    
                    pygame.time.delay(10)

            # Mouse events
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 4:
                    # Mouse wheel down
                    self.ChangeScale(self.Scale - 0.3)
                elif event.button == 5:
                    # Mouse wheel up
                    self.ChangeScale(self.Scale + 0.3)
                else:
                    mouseKeyMonitor.AddKey(event.button)
            elif event.type == pygame.MOUSEBUTTONUP:
                mouseKeyMonitor.RemoveKey(event.button)

            elif event.type == pygame.MOUSEMOTION:
                if mouseKeyMonitor.FindKey(1):
                    # Left mouse button is down: scroll area
                    self.Scroll(-self.ScreenToVirtualX(event.rel[0], False), -self.ScreenToVirtualY(event.rel[1], False))
            
            # Video events     
            elif event.type == pygame.VIDEORESIZE:
                sz = event.dict['size']
                self.__SetScreenMode(sz[0], sz[1])
                self.ReDraw()
#------------------------------------------------------------------------------

