###############################################################################
# File:        guShipLayout.py
# Appointment: Ship layout class
# Project:     GalaxyU
# Author:      AR
# History:
# 19.07.2018 Added colony unit
# 31.01.2013 Report was moved in application
# 04.02.2013 Extracted from guObjects
###############################################################################
import math
import pygame

import guObjects
import pgTools

#===============================================================================
# Ship Layout
#===============================================================================
class CShipLayout:    
    #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    # Abstract component layout class
    #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    class CComponent:
        #-------------------------------------------------------------------
        def __init__(self, aShipComponent):
            self.Component = aShipComponent
            self.X = self.Y = None # not initialized coordinates
            
        #-------------------------------------------------------------------
        # Virtual - returns component 'width'
        def CalcWidth(self):
            return math.sqrt(self.Component.CalcMass())

        #-------------------------------------------------------------------
        # Virtual - returns component 'height'
        def CalcHeight(self):
            return self.CalcWidth()
            

        #-------------------------------------------------------------------
        # Virtual - draws the component
        # aMult - multiplier to fit the image
        # aCorrX/aCorrY - coordinates correction to center the picture on the image
        # aLevelWidth - width of current level; aNextLevelWidth - width of the next level or 0 if it is the last level
        # aTotalSize - total size of the ship
        def Draw(self, aImage, aColour, aMult, aCorrX, aCorrY, aLevelWidth, aNextLevelWidth, aTotalSize):
            pass
    
    #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    
    #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    # Derived component classes
    #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    
    #///////////////////////////////////////////////////////////////////////
    class CDrive(CComponent):
        def __init__(self, aShipComponent):
            CShipLayout.CComponent.__init__(self, aShipComponent)
        
        # Redefined virtual
        def Draw(self, aImage, aColour, aMult, aCorrX, aCorrY, aLevelWidth, aNextLevelWidth, aTotalSize):
            r = int(self.CalcWidth()*aMult)
            if r < 2:
                r = 2
            x = self.X*aMult + aCorrX + r
            y = self.Y*aMult + aCorrY + r/2

            cvm = pgTools.CColourVolMaker(aColour, r, 0.3)
            k = r/4 if aNextLevelWidth > 0 else 0
            if k > aNextLevelWidth:
                k = aNextLevelWidth/2
            if self.Component.Quantity > 0:                
                for i in range (-int(r), 0):
                    ir = -i
                    pygame.draw.polygon(aImage, cvm.GetNextColour(), [(x + k, y), (x - r, y + ir/2), (x - r, y - ir/2)], 0)            

            # Draw a hull
            r = int(r/2.7 if aNextLevelWidth == 0 else aTotalSize/(4 if self.Component.Quantity > 0 else 2.5))
            cvm = pgTools.CColourVolMaker(aColour, r, 0.2)
            for i in range(0, r):
                pygame.draw.circle(aImage, cvm.GetNextColour(), (int((x - r) if aNextLevelWidth == 0 else (aTotalSize/2)), int(y)), r - i)



    #///////////////////////////////////////////////////////////////////////
    class CShield(CComponent):
        def __init__(self, aShipComponent):
            CShipLayout.CComponent.__init__(self, aShipComponent)

        # Redefined virtual
        def Draw(self, aImage, aColour, aMult, aCorrX, aCorrY, aLevelWidth, aNextLevelWidth, aTotalSize):
            d = int(self.CalcWidth()*aMult)
            if d < 2:
                d = 2
            x = self.X*aMult + aCorrX
            y = self.Y*aMult + aCorrY
            
            cvm = pgTools.CColourVolMaker(aColour, d/2, 0.2)
            for i in range(0, d/2 - 1):
                pygame.draw.ellipse(aImage, cvm.GetNextColour(), [x + i, y + i, d - 2*i + aNextLevelWidth/4, d - 2*i], 1)
                

    #///////////////////////////////////////////////////////////////////////
    class CCargo(CComponent):
        def __init__(self, aShipComponent, aIsColonyUnit = False):
            CShipLayout.CComponent.__init__(self, aShipComponent)
            self.__isColonyUnit = aIsColonyUnit

        # Redefined virtuals
        def Draw(self, aImage, aColour, aMult, aCorrX, aCorrY, aLevelWidth, aNextLevelWidth, aTotalSize):
            d = self.CalcWidth()*aMult
            if d < 2:
                d = 2
            r = int(d/2)
            x = self.X*aMult + aCorrX
            y = self.Y*aMult + aCorrY
            
            cvm = pgTools.CColourVolMaker(aColour, d, 0.3)
            for i in range(0, r + 1):
                c = cvm.GetNextColour()
                pygame.draw.line(aImage, c, (x, y + d - i), (x + r, y + d - i))
                pygame.draw.line(aImage, c, (x, y + i), (x + r, y + i))

            if not self.__isColonyUnit:
              x += d
              y += r
              cvm = pgTools.CColourVolMaker(aColour, d, 0.3)
              for i in range (-int(d), 0):
                  ir = -i
                  pygame.draw.polygon(aImage, cvm.GetNextColour(), 
                                      [(x + aNextLevelWidth/2, y), (x - r, y + ir/2), (x - r, y - ir/2)], 
                                      0)

    #///////////////////////////////////////////////////////////////////////
    class CColonyUnit(CCargo):
        def __init__(self, aShipComponent):
            CShipLayout.CCargo.__init__(self, aShipComponent, True)


    #///////////////////////////////////////////////////////////////////////
    # 'Rocket' class - drawing launched missile
    class CRocket(CComponent):
        def __init__(self, aShipComponent):
            CShipLayout.CComponent.__init__(self, aShipComponent)

        # Redefined virtuals
        def Draw(self, aImage, aColour, aMult, aCorrX, aCorrY, aLevelWidth, aNextLevelWidth, aTotalSize):
            d = int(self.CalcWidth()*aMult)
            rockedD = float(d)*0.8
            w = float(d)/10;
            dw = w/d
            if d < 2:
                d = 2
            x = self.X*aMult + aCorrX
            y = self.Y*aMult + aCorrY
            
            # Draw the haul
            cvm = pgTools.CColourVolMaker(aColour, d, 0.2)
            for i in range(0, d):
                c = cvm.GetNextColour()
                k = dw*(d - i)
                y1 = int(y + d/2 - k)
                y2 = int(y + d/2 + k)
                pygame.draw.line(aImage, c, (x, y1), (x + rockedD, y1))
                pygame.draw.line(aImage, c, (x, y2), (x + rockedD, y2))

            # Stabilizators
            y1 = y + d/2
            pygame.draw.polygon(aImage, aColour, [(x, y1 - w), (x, y1 - 2*w), (x + int(rockedD/5), y1 - w)], 0);
            pygame.draw.polygon(aImage, aColour, [(x, y1 + w), (x, y1 + 2*w), (x + int(rockedD/5), y1 + w)], 0);
            
            # Draw the cone
            x += rockedD
            y += d/2
            conus = int(d - rockedD)
            if conus < 1:
                conus = 1
            cvm = pgTools.CColourVolMaker(aColour, int(w), 0.2)
            for i in range (-int(w), 0):
                ir = -i
                pygame.draw.polygon(aImage, cvm.GetNextColour(), 
                                    [(x + conus, y), (x, y + ir), (x, y - ir)], 
                                    0)
                

    #/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
    # Base for multiple components
    class CWeaponBase(CComponent):
        #-------------------------------------------------------------------
        def __init__(self, aShipComponent, aDrawFunc):
            CShipLayout.CComponent.__init__(self, aShipComponent)
            self.__DrawFunc = aDrawFunc

        #-------------------------------------------------------------------
        # Redefined virtual
        def Draw(self, aImage, aColour, aMult, aCorrX, aCorrY, aLevelWidth, aNextLevelWidth, aTotalSize):
            d = int(self.CalcWidth()*aMult)
            if d < 2:
                d = 2
            h = int(self.CalcHeight()*aMult)
            x = self.X*aMult + aCorrX
            y = self.Y*aMult + aCorrY
            cvm = pgTools.CColourVolMaker(aColour, d/2, 0.2)
            for i in range(0, d/2):
                ww = d - 2*i + aNextLevelWidth/4
                hh = h - 2*i
                if ww > 0 and hh > 0:
                    self.__DrawFunc(aImage, cvm.GetNextColour(), [x + i, y + i, ww, hh])
                    
            # Draw cannons
            step = float(h)/self.Component.Quantity
            # Cannon width
            w = int(math.sqrt(self.Component.Mass)*aMult/2)
            if w < 2:
                w = 2
            y += step/2
            for i in range(0, self.Component.Quantity):
                cvm = pgTools.CColourVolMaker(aColour, w/2, 0.2)
                yy = y + i*step
                for ii in range(0, w/2 + 1):
                    i = w/2 - ii
                    c = cvm.GetNextColour()
                    pygame.draw.line(aImage, c, (x, yy - i), (x + d + d/4, yy - i), 1)
                    pygame.draw.line(aImage, c, (x, yy + i), (x + d + d/4, yy + i), 1)
            
        #-------------------------------------------------------------------
        def CalcWidth(self):
            return math.sqrt(self.Component.Mass)*(1 + math.pow(self.Component.MaxLaunchCount, 0.3))
            
        #-------------------------------------------------------------------
        def CalcHeight(self):
            return self.CalcWidth()*math.pow(self.Component.Quantity, 0.7)

    #///////////////////////////////////////////////////////////////////////
    class CLaser(CWeaponBase):
        def __init__(self, aShipComponent):
            CShipLayout.CWeaponBase.__init__(self, aShipComponent, pygame.draw.rect)

    #///////////////////////////////////////////////////////////////////////
    class CMissile(CWeaponBase):
        def __init__(self, aShipComponent):
            CShipLayout.CWeaponBase.__init__(self, aShipComponent, pygame.draw.ellipse)
    
    
    #///////////////////////////////////////////////////////////////////////
    class CRepair(CComponent):
        def __init__(self, aShipComponent):
            CShipLayout.CComponent.__init__(self, aShipComponent)

        # Redefined virtual
        def Draw(self, aImage, aColour, aMult, aCorrX, aCorrY, aLevelWidth, aNextLevelWidth, aTotalSize):
            d = int(self.CalcWidth()*aMult)
            if d < 2:
                d = 2
            x = self.X*aMult + aCorrX
            y = self.Y*aMult + aCorrY
            
            cvm = pgTools.CColourVolMaker(aColour, d/2, 0.2)
            pygame.draw.ellipse(aImage, cvm.GetCurrentColour(), [x, y, d, d])
            for i in range(0, d/4 - 1):
                pygame.draw.ellipse(aImage, cvm.GetNextColour(), [x + i, y + i, d - 2*i + aNextLevelWidth/4, d - 2*i], 1)

    #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    

    #-----------------------------------------------------------------------
    # aComponents - list of ship components
    def __init__(self, aComponents):
        #-------------------------------------------------------------------    
        # Local helper: creates ship layout
        # aComponents - a map <height, CComponent)
        # aHeight - max ship height
        # returns map <row, components list>. 
        #-------------------------------------------------------------------    
        def CreateLayout(aComponents, aHeight):
            # Loop by components
            layout = {}
            level = 1 # level 0 reserved for drive
            layout[level] = []    
            keys = reversed(sorted(aComponents.keys()))
            oldType = oldMass = -1
            for h in keys:
                for c in aComponents[h]:
                    if oldType == -1:
                        oldMass = c.Component.CalcMass()
                        oldType = c.Component.Type
                    lh = self.CalcLevelHeight(layout, level)
                    if lh + c.CalcHeight() > aHeight or \
                       c.Component.CalcMass() != oldMass or \
                       c.Component.Type != oldType or \
                       len(layout[level]) > 0 and layout[level][0].Component.IsSingle():
                        level += 1
                        layout[level] = []    
                        oldMass = c.Component.CalcMass()
                        oldType = c.Component.Type
                    layout[level].append(c)
            
            # Sort layout by levels height
            res = {}
            while len(layout) > 0:
                maxH = 0
                maxKey = None
                for key in layout.iterkeys():
                    h = self.CalcLevelHeight(layout, key)
                    if h > maxH:
                        maxKey = key
                        maxH = h
                res[len(res) + 1] = layout[maxKey]
                del layout[maxKey]
            return res
        #-------------------------------------------------------------------    

        # Create a map of components sorted by height
        # Note: we ignore Drive
        mh = {}
        maxHeight = 0
        for c in aComponents:
            # Virtual ctor - can it be better?! TODO...
            cc = None
            if c.Type == guObjects.CShipComponent.SHIP_COMPONENT_DRIVE:
                continue # drive is always at the end
            elif c.Type == guObjects.CShipComponent.SHIP_COMPONENT_SHIELD:
                cc = CShipLayout.CShield(c)
            elif c.Type == guObjects.CShipComponent.SHIP_COMPONENT_CARGO:
                cc = CShipLayout.CCargo(c)
            elif c.Type == guObjects.CShipComponent.SHIP_COMPONENT_COLONY_UNIT:
                cc = CShipLayout.CColonyUnit(c)
            elif c.Type == guObjects.CShipComponent.SHIP_COMPONENT_LASER:
                cc = CShipLayout.CLaser(c)
            elif c.Type == guObjects.CShipComponent.SHIP_COMPONENT_MISSILE:
                # It's a trick if creating a 'rocket' image: only one component and destroyed mass bigger than mass
                cc = CShipLayout.CRocket(c) if len(aComponents) == 1 and c.DestroyedMass > c.Mass else CShipLayout.CMissile(c)
            elif c.Type == guObjects.CShipComponent.SHIP_COMPONENT_REPAIR_UNIT:
                cc = CShipLayout.CRepair(c)
            else:
                cc = CShipLayout.CComponent(c) # unknown
            
            h = cc.CalcHeight()
            maxHeight = max(maxHeight, h)
            if not h in mh:
                mh[h] = [] # add a list - can be several with the same height
            mh[h].append(cc)
         
        self.Layout = None   
        if len(mh) > 0:
            # Should always be here unless the ship has only a drive
            # Adjusting layout
            while True:
                self.Layout = CreateLayout(mh, maxHeight)
                # Calculate total length
                ll = self.CalcLength()
                if ll/maxHeight < 5:
                    break # fine
                # Increase height to add more components in a row
                maxHeight += 1

        # If components have 'shiled' only - makes no sense to draw a hull.
        # Most probably it's a ship, representing an unknown ship type
        if aComponents[0].Type != guObjects.CShipComponent.SHIP_COMPONENT_MISSILE and \
                 (len(aComponents) != 1 or aComponents[0].Type != guObjects.CShipComponent.SHIP_COMPONENT_SHIELD):
            # Add drive (I add 'fake' drive if there is none. This fake drive forces drawing a hule of the ship)              
            cc = guObjects.CShipType.FindComponents(guObjects.CShipComponent.SHIP_COMPONENT_DRIVE, aComponents)
            drive = CShipLayout.CDrive(cc[0] if len(cc) > 0 else guObjects.CShipComponent(0, guObjects.CShipComponent.SHIP_COMPONENT_DRIVE, 1, 0))
            if self.Layout is None:
                self.Layout = {0 : [drive]}
            else:
                self.Layout[0] = [drive]
        
        # Set coordinates to each component in layout
        xStart = yStart = 0
        ph = 0 # height of the module before
        corrY = 0 # correction
        for level in self.Layout.iterkeys():
            # Calc level height
            lh = self.CalcLevelHeight(self.Layout, level)
            # Center level position by the previous level
            k = (0 if ph == 0 else ph - lh)/2
            yStart += k
            y = yStart
            if y < 0:
                corrY = min(y, corrY)
            # Set coordinates for each component on the level
            for c in self.Layout[level]:
                c.X = xStart
                c.Y = y
                y += c.CalcHeight()
            xStart += self.CalcLevelWidth(self.Layout, level)
            ph = lh

        # Correct Y because it can go in minus
        for level in self.Layout.iterkeys():
            for c in self.Layout[level]:
                c.Y -= corrY


    #-----------------------------------------------------------------------
    # Calculates level height
    def CalcLevelHeight(self, aLayout, aLevel):
        h = 0
        if aLevel in aLayout:
            for c in aLayout[aLevel]:
                h += c.CalcHeight()
        return h

    #-----------------------------------------------------------------------
    # Calculates level width
    def CalcLevelWidth(self, aLayout, aLevel):
        w = 0
        if aLevel in aLayout:
            for c in aLayout[aLevel]:
                w = max(w, c.CalcWidth())
        return w

    #-----------------------------------------------------------------------
    # Calculates layout height
    def CalcHeight(self):
        lh = 0 # total height
        for level in self.Layout.iterkeys():
            lh = max(lh, self.CalcLevelHeight(self.Layout, level))
        return lh

    #-----------------------------------------------------------------------
    # Calculates layout length
    def CalcLength(self):
        ll = 0 # total length
        for level in self.Layout.iterkeys():
            ll += self.CalcLevelWidth(self.Layout, level)
        return ll

    #-----------------------------------------------------------------------
    # Draws layout in a given image
    def Draw(self, aImage, aColour):
        # Calc start coordinates depending on image size
        ll = self.CalcLength()
        lh = self.CalcHeight()
        k = min(aImage.get_height()/lh, aImage.get_width()/ll)
        kx = aImage.get_width()/ll
        ky = aImage.get_height()/lh
        corrX = (aImage.get_width() - ll*k)/2
        corrY = (aImage.get_height() - lh*k)/2
        
        # Draw levels
        maxLevel = max(self.Layout.iteritems(), key=lambda x:x[0])[0]
        for level in self.Layout.iterkeys():
            levelWidth = self.CalcLevelWidth(self.Layout, level) * k
            nextLevelWidth = 0 if level == maxLevel else self.CalcLevelWidth(self.Layout, level + 1) * k
            for c in self.Layout[level]:
                c.Draw(aImage, aColour, k, corrX, corrY, levelWidth, nextLevelWidth, min(aImage.get_height(), aImage.get_width()))

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

