###############################################################################
# File:        guReport.py
# Appointment: Reports support
# Project:     GalaxyU
# Author:      AR
# History:
# 26.22.2018 Added AdditionalData support
# 02.05.2018 Adopted to additional parameter in battle units
# 11.10.2013 Propagationg ships FleetName and Efficiency to reports
# 10.10.2013 Added bombing support
# 03.08.2013 gu: (XIII) adding action sequences to battle records
# 31.07.2013 gu: (XI) delegated group number of units from API
# 25.09.2012 Created
###############################################################################
import guShipLayout
import guObjects
import guBattle
import guGroundBattle
import guApp
import guShipsView
import zx
import zxVersion
import zxLog

import os 
import math
import wx
import wxTools
from ctypes import *
import pygame.gfxdraw

# Coorinates normalization multiplier - to make the picture to be better on the screen
COORDINATES_MULT = 6

#==============================================================================
# Specifies star ownership related to the report
#==============================================================================
class CStarOwnership:
    def __init__(self, aStar):
        self.IsOwn = aStar.IsOwn()
        self.IsKnown = aStar.IsKnown()
        self.IsActualKnown = aStar.IsActualKnown()
#------------------------------------------------------------------------------    


#===============================================================================
# Report description
#===============================================================================
class CReport:
    # Commonly used colours
    NATION_COLOUR = (255,255,255)
    ENEMY_COLOUR  = (255, 0, 0)
    NEUTRAL_COLOUR = (0,150,255)
    
    #---------------------------------------------------------------------------
    def __init__(self, api):
        self.API = api
        self.__CleanUp()
        self.NationName = ""
        self.Turn = 0
        self.FlagTemplateHeight = self.API.guGetFlagHeight()
        self.FlagTemplateWidth = self.API.guGetFlagWidth()

        # Colour attributes for diplomatic state (used in GetDiplomaticStateColourAttr)
        self.__attrPeace = wx.ListItemAttr()
        self.__attrPeace.SetTextColour((0,0,200))
        self.__attrWar = wx.ListItemAttr()
        self.__attrWar.SetTextColour((200,0,0))

    #---------------------------------------------------------------------------
    # Helper: cleans up containers
    def __CleanUp(self):
        self.FileName = ""          # Report filename
        self.VisibleStars = []      # all stars
        self.__StarsData = {}       # hash map of star data used in FindStar: <star id: CStar>
        self.NationFlagsImages = {} # map of nations flag images
        self.SpaceGroups = []       # list of space groups
        self.ShipTypes = []         # list of ship types
        self.Ships = []             # list of ships
        self.__ShipGroups = []      # list of ship groups
        self.Fleets = []            # list of fleets
        self.TransportOrders = []   # list of transport orders
        self.ProdOrders = []        # list of production orders
        self.NationsRating = []     # list of nations rating
        self.KnownNations = []      # list of known nations
        self.ReportMessages = []    # list of report messages
        self.Sciences = []          # list of sciences
        self.MoneyTransactions = [] # list of money transactions
        self.AutoManagers = []      # list of auto managers
        self.Rulers = []            # list of rulers
        self.AdditionalData = []    # additional data
        
        # Dictionaries
        self.__ShipLayouts = {}
        self.__ShipImages = {}
        self.__ShipsLocations = {} # {X, [ships]}
        # Unique id sequence (made negative to distingush from stars ids)
        self.__IdGenerator = zx.CSequence(-1, -1)


    #---------------------------------------------------------------------------
    # Helper: loads space groups in the list
    # Returns False if invalid report format
    def __LoadSpaceGroups(self):
        self.SpaceGroups = []

        # Callback
        def cb(aX, aY, aMass, aDestination, aIsMyGroup, aETA, aSpeed):
            self.SpaceGroups.append(\
                guObjects.CSpaceGroup(\
                    self.__IdGenerator.Next(),
                    aX, aY, aMass, aDestination, aIsMyGroup, aETA, aSpeed))

        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(\
            None, c_double, c_double, c_double, c_long, c_bool, c_long, c_double)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGroupsInSpace(self.NationName, cb_func)
        return True


    #---------------------------------------------------------------------------
    # Returns battle sequences for given star
    def GetBattleSequences(self, aStarId):
        # Result sequences
        bs = []
        
        def cbUnits(\
                aUnitId, aShipTypeName, aShipTypeCode,
                aMass, aEfficiency, aX, aY,
                aIsMissile, aLaunchedFromUnitId, aNumber):
            newSeq.Units.append(guBattle.CBattleSequence.CUnit(\
                aUnitId, aShipTypeName,
                aMass, aEfficiency, aX, aY,
                aIsMissile, aLaunchedFromUnitId, aNumber))
        CB_FUNC_TYPE_UNITS = CFUNCTYPE(\
            None, c_char_p, c_char_p, c_char_p,
            c_double, c_double, c_double, c_double,
            c_bool, c_char_p, c_long)
        cb_func_Units = CB_FUNC_TYPE_UNITS(cbUnits)

        def cbFireActions(\
                aSeqNumber, aUnitId, aTargetId,
                aFireType, aDamageType, aShotSize, aShotPower,
                aTargetDamageAmount, aTargetRepulsedByShield, aTargetEfficiency,
                aIsFireAction):
            actionsContainer = newSeq.FireActions if aIsFireAction else newSeq.MissileHits
            actionsContainer.append(guBattle.CBattleSequence.CFireAction(\
                aSeqNumber, aUnitId, aTargetId,
                aFireType, aDamageType, aShotSize, aShotPower,
                aTargetDamageAmount, aTargetRepulsedByShield, aTargetEfficiency,
                self.__IdGenerator.Next()))
        CB_FUNC_TYPE_FIRE_ACTIONS = CFUNCTYPE(\
            None, c_long, c_char_p, c_char_p,
            c_long, c_long, c_double, c_double,
            c_double, c_double, c_double,
            c_bool)
        cb_func_FireActions = CB_FUNC_TYPE_FIRE_ACTIONS(cbFireActions)

        def cbShipsMoves(aSeqNumber, aUnitId, aX, aY, aCount, aIsShipMove):
            actionsContainer = newSeq.ShipMoves if aIsShipMove else newSeq.MissileMoves
            actionsContainer.append(guBattle.CBattleSequence.CMoveAction(\
                aSeqNumber, aUnitId, aX, aY, aCount))
        CB_FUNC_TYPE_SHIPS_MOVES = CFUNCTYPE(\
            None, c_long, c_char_p, c_double, c_double, c_long, c_bool)
        cb_func_ShipsMoves = CB_FUNC_TYPE_SHIPS_MOVES(cbShipsMoves)

        # Loading sicle
        seq = 0
        while True:
            newSeq = guBattle.CBattleSequence()
            if self.API.guGetBattleSequence(\
                    aStarId, seq,
                    cb_func_Units,
                    cb_func_FireActions,
                    cb_func_ShipsMoves,
                    cb_func_FireActions,
                    cb_func_ShipsMoves):
                bs.append(newSeq)
                seq += 1
            else:
                break # no more battle cycles found
        
        return bs
    

    #---------------------------------------------------------------------------
    # Returns bombing for given star
    def GetBombRecords(self, aStarId):
        # Result sequences
        br = []

        def cb(aStarId, aOwner, aAttacker, aDestroyedCol, aDestroyedPop, aDestroyedInd):
            br.append(guBattle.CBombRecord(\
                aStarId, aOwner, aAttacker, aDestroyedCol, aDestroyedPop, aDestroyedInd))

        CB_FUNC_TYPE = CFUNCTYPE(None, c_long, c_char_p, c_char_p, c_double, c_double, c_double)
        cb_func = CB_FUNC_TYPE(cb)

        self.API.guGetBombings(aStarId, cb_func)
        return br


    #---------------------------------------------------------------------------
    # Returns ground battles for given star
    def GetGroundBattleRecords(self, aStarId):
        # Result sequences
        br = []

        self.__dbIndexes = {}
        def cb(\
                aStarId, aInvaderNationName, aOwnerNationName,
                aInvaderInitialCol, aOwnerInitialCol,
                aInvaderResultCol, aOwnerResultCol):

            if aStarId in self.__dbIndexes:
                self.__dbIndexes[aStarId] += 1
            else:
                self.__dbIndexes[aStarId] = 0

            gb = guGroundBattle.CGroundBattleRecord(\
                aStarId, aInvaderNationName, aOwnerNationName,
                aInvaderInitialCol, aOwnerInitialCol,
                aInvaderResultCol, aOwnerResultCol)
            br.append(gb)

            # Load minibattles
            mbIndex = 0
            def cbMini(aInvaderUnitId, aOwnerUnitId, aId1, aId2, aIsDestroyed):
                if aInvaderUnitId != -1:
                    mb.InvaderUnits.append(aInvaderUnitId)
                if aOwnerUnitId != -1:
                    mb.OwnerUnits.append(aOwnerUnitId)
                if aId1 != -1:
                    mb.ShotResults.append(guGroundBattle.CGroundBattleRecord.CShotResult(\
                        aId1, aId2, aIsDestroyed))

            CB_MB_FUNC_TYPE = CFUNCTYPE(None, c_long, c_long, c_long, c_long, c_bool)
            cb_mb_func = CB_MB_FUNC_TYPE(cbMini)
            while True:
                mb = guGroundBattle.CGroundBattleRecord.CMiniBattle()
                if not self.API.guGetGroundMiniBattle(\
                        aStarId, self.__dbIndexes[aStarId], mbIndex, cb_mb_func):
                    break
                gb.MiniBattles.append(mb)
                mbIndex += 1
                
        CB_FUNC_TYPE = CFUNCTYPE(\
            None, c_long, c_char_p, c_char_p,
            c_double, c_double,
            c_double, c_double)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetGroundBattles(aStarId, cb_func)
        return br

    
    #---------------------------------------------------------------------------
    # Helper: loads stars and battles in the list
    # mode: 0 - loading stars, 1 - adding sector mark stars
    # Returns False if invalid report format
    def __LoadStars(self, aMode):
        if aMode == 0:
            self.VisibleStars = []

        def cb(\
                aStarId, aStarName, aX, aY,
                aSize, aPopulation, aMaterials, aIndustry,
                aResourceFactor, aLiveConditions,
                aOwner, aLastKnownAt, aGalaxyName,
                aIsBannedToVisit, aIsBannedToBomb):
            star = guObjects.CStar(\
                aStarId, aStarName, aX, aY,
                aSize, aResourceFactor, aLiveConditions,
                aPopulation, aMaterials, aIndustry,
                aOwner, aLastKnownAt, aGalaxyName,
                aIsBannedToVisit, aIsBannedToBomb)
            self.VisibleStars.append(star)
            # Load possible battle sequences
            star.BattleSequences = self.GetBattleSequences(star.Id)
            # Load possible bombing records
            star.BombRecords = self.GetBombRecords(star.Id)
            # Load possible ground battles
            star.GroundBattles = self.GetGroundBattleRecords(star.Id);

        CB_FUNC_TYPE = CFUNCTYPE(\
            None,
            c_long, c_char_p, c_double, c_double,
            c_long, c_double, c_double, c_double,
            c_double, c_double,
            c_char_p, c_long, c_char_p,
            c_bool, c_bool)
        cb_func = CB_FUNC_TYPE(cb)

        n = self.API.guGetStarsData(self.NationName, aMode, cb_func)
        return aMode == 1 or n > 0
        

    #---------------------------------------------------------------------------
    # Helper: loads ship components using callback
    def LoadShipComponentsByCB(self, aGetShipComponentsFunc):
        # Prepare callback populating components
        components = []
        def cb(\
                aId,
                aType, aElemMass, aQuantity, aDestroyedMass, aTech,
                aTargetType, aCargoStr,
                aMissileMaxLaunchCount, aMissileWarHeadPercentage, aEfficiency,
                aShotPower):
            components.append(guObjects.CShipComponent(\
                aId,
                aType, aElemMass, aQuantity, aDestroyedMass, aTech,
                aTargetType, aCargoStr,
                aMissileMaxLaunchCount, aMissileWarHeadPercentage, aEfficiency,
                aShotPower))

        CB_FUNC_TYPE = CFUNCTYPE(\
            None,
            c_long,
            c_long, c_double, c_long, c_double, c_double,
            c_long, c_char_p,
            c_long, c_double, c_double, c_double)
        cb_func = CB_FUNC_TYPE(cb)

        # Call lambda with callback
        if aGetShipComponentsFunc(cb_func) <= 0:
            guApp.Application.Log(zxLog.LOG_ERROR, "Loaded no ship components")

        # components should be populated at this point
        return components


    #---------------------------------------------------------------------------
    # Helper: loads ships types
    # Returns False if invalid report format
    def __LoadShipTypes(self):
        self.ShipTypes = []

        def cb(aNationName, aShipTypeName, aShipClass):
            shipType = guObjects.CShipType(aShipTypeName, aNationName, aShipClass)

            # Load components for the ship type
            shipType.Components = self.LoadShipComponentsByCB(\
                lambda cb: self.API.guGetShipTypeComponents(shipType.NationName, shipType.Name, cb))
                
            # Add ship type                                               
            self.ShipTypes.append(shipType)

        CB_FUNC_TYPE = CFUNCTYPE(None, c_char_p, c_char_p, c_long)
        cb_func = CB_FUNC_TYPE(cb)

        self.API.guGetShipTypes(self.NationName, cb_func)
        return True

    
    #---------------------------------------------------------------------------
    # Helper: loads ships
    # Returns False if invalid report format
    def __LoadShips(self):
        self.Ships = []

        # Callback
        def cb(\
                aId, aNationName, aTypeName, aShipName,
                aBuildYear, aTravelledDistance, aBattleCounter, aDamagedEnemyShipsCounter,
                aDestroyedEnemyShipsCounter, aDestroyedEnemyShipsMass, aDamagedCounter,
                aCombatFactor, aX, aY, aInSpace, aFleetName, aDefaultFleetName,
                aEfficiency, aLoadedMass, aAssignment, aUpgradeStatus):
            ship = guObjects.CShip(\
                aId,
                aNationName,
                aTypeName,
                aShipName,
                aBuildYear,
                aTravelledDistance,
                aBattleCounter,
                aDamagedEnemyShipsCounter,
                aDestroyedEnemyShipsCounter,
                aDestroyedEnemyShipsMass,
                aDamagedCounter,
                aCombatFactor,
                aX,
                aY,
                aInSpace,
                aFleetName,
                aDefaultFleetName,
                aEfficiency,
                aLoadedMass,
                aAssignment,
                aUpgradeStatus)

            ship.Components = self.LoadShipComponentsByCB(\
                lambda cb: self.API.guGetShipComponents(ship.NationName, ship.Id, cb))

            # Add ship to container 
            self.Ships.append(ship)

        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(\
            None, c_long, c_char_p, c_char_p, c_char_p,
            c_long, c_double, c_long, c_long,
            c_long, c_double, c_long,
            c_double, c_double, c_double, c_bool, c_char_p, c_char_p,
            c_double, c_double, c_char_p, c_char_p)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetShips(self.NationName, cb_func)
        return True


    #---------------------------------------------------------------------------
    # Helper: loads fleets
    # Returns False if invalid report format
    def __LoadFleets(self):
        self.Fleets = []
        
        # Callback
        def cb(\
                aNationName, aName, aIsSystem,
                aNumberOfShips, aTotalMass, aAssignment, aSpeed, aStarName):
            self.Fleets.append(guObjects.CFleet(\
                aNationName, aName, aIsSystem,
                aNumberOfShips, aTotalMass, aAssignment, aSpeed, aStarName))

        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(\
            None, c_char_p, c_char_p, c_bool,
            c_long, c_double, c_char_p, c_double, c_char_p)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetFleets(self.NationName, cb_func)

        return True


    #---------------------------------------------------------------------------
    # Helper: loads transport orders
    # Returns False if invalid report format
    def __LoadTransportOrders(self):
        self.TransportOrders = []
        
        # Callback
        def cb(\
                aId, aNationName, aCargo, aStartStarId, aDestinationStarId,
                aOrderedAmount, aProducedAmount, aStatus):            
            self.TransportOrders.append(\
                guObjects.CTransportOrder(\
                    aNationName, aId, aCargo, aStartStarId,
                    aDestinationStarId, aOrderedAmount, aProducedAmount, aStatus))

        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(\
            None, c_long, c_char_p, c_char_p, c_long, c_long,
            c_double, c_double, c_char_p)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetTransportOrders(self.NationName, cb_func)
        return True


    #---------------------------------------------------------------------------
    # Helper: loads production orders
    # Returns False if invalid report format
    def __LoadProdOrders(self):
        self.ProdOrders = []

        # Callback
        def cb(\
                aNationName, aStarId, aOrderId,
                aOrderType, aPriority, aOrderedAmount, aProducedAmount,
                aStatus, aInfo):            
            self.ProdOrders.append(\
                guObjects.CProdOrder(\
                    aNationName, aStarId, aOrderId,
                    aOrderType, aPriority, aOrderedAmount, aProducedAmount,
                    aStatus, aInfo))

        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(\
            None, c_char_p, c_long, c_long,
            c_char_p, c_long, c_double, c_double,
            c_char_p, c_char_p)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetProdOrders(self.NationName, cb_func)
        return True



    #---------------------------------------------------------------------------
    # Helper: loads nations rating
    # Returns False if invalid report format
    def __LoadNationsRating(self):
        self.NationsRating = []

        # Callback
        def cb(\
                aNationName, aRatingIndex, aRating, aCreationTurn,
                aPopulation, aIndustry, aShipsNumber, aShipsMass,
                aTechDrive, aTechWeapon, aTechShield, aTechCargo, aTechAvg,
                aColoniesNumber, aColoniesTotalSize, aMoney, aDebt,
                aPopulationDelta, aIndustryDelta,
                aShipsNumberDelta, aShipsMassDelta,
                aTechDriveDelta, aTechWeaponDelta, aTechShieldDelta,
                aTechCargoDelta, aTechAvgDelta,
                aColoniesNumberDelta, aColoniesTotalSizeDelta, aMoneyDelta, aDebtDelta,
                aRatingDelta):            
            self.NationsRating.append(guObjects.CRatingData(\
                aNationName, aRatingIndex, aRating, aCreationTurn,
                aPopulation, aIndustry, aShipsNumber, aShipsMass,
                aTechDrive, aTechWeapon, aTechShield, aTechCargo, aTechAvg,
                aColoniesNumber, aColoniesTotalSize, aMoney, aDebt,
                aPopulationDelta, aIndustryDelta,
                aShipsNumberDelta, aShipsMassDelta,
                aTechDriveDelta, aTechWeaponDelta, aTechShieldDelta,
                aTechCargoDelta, aTechAvgDelta,
                aColoniesNumberDelta, aColoniesTotalSizeDelta, aMoneyDelta, aDebtDelta,
                aRatingDelta))
        
        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(\
            None, c_char_p, c_long, c_double, c_long,
            c_double, c_double, c_long, c_double,
            c_double, c_double, c_double, c_double, c_double,
            c_long, c_long, c_double, c_double,
            c_double, c_double,
            c_long, c_double,
            c_double, c_double, c_double,
            c_double, c_double,
            c_long, c_long, c_double, c_double,
            c_double)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetNationsRating(self.NationName, cb_func)
        return True


    #---------------------------------------------------------------------------
    # Helper: loads known nations
    # Returns False if invalid report format
    def __LoadKnownNations(self):
        self.KnownNations = []

        # Callback
        def cb(\
                aNationName, aKnownNationName, aLastTimeContact,
                aTechDrive, aTechWeapon, aTechShield, aTechCargo, aLastTechUpdated,
                aPopulation, aIndustry, aLastPopulationUpdated):            
            self.KnownNations.append(guObjects.CKnownNation(
                aNationName, aKnownNationName, aLastTimeContact,
                aTechDrive, aTechWeapon, aTechShield, aTechCargo, aLastTechUpdated,
                aPopulation, aIndustry, aLastPopulationUpdated))
        
        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(\
            None, c_char_p, c_char_p, c_long,
            c_double, c_double, c_double, c_double, c_long,
            c_double, c_double, c_long)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetKnownNations(self.NationName, cb_func)
        return True


    #---------------------------------------------------------------------------
    # Helper: loads report messages
    # Returns False if invalid report format
    def __LoadReportMessages(self):
        self.ReportMessages = []
        
        # Callback
        def cb(aNationName, aType, aText, aAdditionalData):            
            self.ReportMessages.append(\
                guObjects.CReportMessage(aNationName, aType, aText, aAdditionalData))
        
        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(None, c_char_p, c_char_p, c_char_p, c_char_p)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetReportMessages(self.NationName, cb_func)
        return True
    

    #---------------------------------------------------------------------------
    # Helper: loads rulers
    # Returns False if invalid report format
    def __LoadRulers(self):
        self.Rulers = []

        # Callback
        def cb(aNationName, aRuleName, aTitle, aGalaxyRulerTitle):   
            self.Rulers.append(guObjects.CRuler(aNationName, aRuleName, aTitle, aGalaxyRulerTitle))
        
        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(None, c_char_p, c_char_p, c_char_p, c_char_p)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetRulers(self.NationName, cb_func)
        return True


    #---------------------------------------------------------------------------
    # Helper: loads additional data
    # Returns False if invalid report format
    def __LoadAdditionalData(self):
        self.AdditionalData = []

        # Callback
        def cb(aNationName, aId, aName, aType, aData):   
            self.AdditionalData.append(guObjects.CDataItem(aNationName, aId, aName, aType, aData))
        
        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(None, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetAdditionalData(self.NationName, cb_func)
        return True

    #---------------------------------------------------------------------------
    # Helper: loads sciences
    # Returns False if invalid report format
    def __LoadSciences(self):
        self.Sciences = []

        # Callback
        def cb(aNationName, aScienceName, aDrive, aWeapon, aShield, aCargo):   
            self.Sciences.append(\
                guObjects.CScience(aNationName, aScienceName, aDrive, aWeapon, aShield, aCargo))
        
        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(None, c_char_p, c_char_p, c_double, c_double, c_double, c_double)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetSciences(self.NationName, cb_func)
        return True


    #---------------------------------------------------------------------------
    # Helper: loads money transactions
    # Returns False if invalid report format
    def __LoadMoneyTransactions(self):
        self.MoneyTransactions = []
        
        # Callback
        def cb(aNationName, aDelta, aResult, aText):            
            self.MoneyTransactions.append(\
                guObjects.CMoneyTransaction(aNationName, aDelta, aResult, aText))
        
        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(None, c_char_p, c_double, c_double, c_char_p)
        cb_func = CB_FUNC_TYPE(cb)
        self.API.guGetMoneyTransactions(self.NationName, cb_func)
        return True


    #---------------------------------------------------------------------------
    # Helper: loads auto managers
    # Returns False if invalid report format
    def __LoadAutoManagers(self):
        self.AutoManagers = []

        # Callback
        def cb(aNationName, aManagerName, aIsActive, aDefaultPriority, aStars, aInfo):            
            self.AutoManagers.append(guObjects.CAutoManager(\
                aNationName, aManagerName, aIsActive, aDefaultPriority, aStars, aInfo))
        
        # Call API function
        CB_FUNC_TYPE = CFUNCTYPE(None, c_char_p, c_char_p, c_bool, c_long, c_char_p, c_char_p)
        cb_func = CB_FUNC_TYPE(cb)
        return self.API.guGetManagers(self.NationName, cb_func)


    #---------------------------------------------------------------------------
    def GetShipGroups(self):
        if len(self.__ShipGroups) == 0:
            # Not ready yet - prepare them
            # It is important to evaluate the ship groups by 'lazy' method
            # to be requested *after* the coordinates normalization has finished
            self.__PrepareShipGroups()
        
        return self.__ShipGroups

    #---------------------------------------------------------------------------
    # Helper: prepares ship groups
    # Returns False if invalid report format
    def __PrepareShipGroups(self):
        print "---PrepareShipGroups started..."
        # Loop by star systems
        for star in self.VisibleStars:
            # Find all ships in the star system
            allShips = self.FindShipsForLocation(star.X, star.Y)
            if len(allShips) > 0:
                # Group ships by nations and types
                nationsGroups = {} # <nation name : ships groups>
                for ship in allShips:
                    ship.Star = star # remember ship location
                    if not ship.NationName in nationsGroups:
                        nationsGroups[ship.NationName] = {} # <type name + destination: ships>
                    key = ship.TypeName + "/" + ship.FleetName + \
                        str(ship.GetDestination()) + "/" + ship.Assignment                      
                    if not key in nationsGroups[ship.NationName]:
                        nationsGroups[ship.NationName][key] = []
                    nationsGroups[ship.NationName][key].append(ship)
                    
                # Add ship groups for this star system
                # Note: here I remove coordinate adjustment

                # Find how many ships on each 'side'
                leftSideCount = 0
                rightSideCount = 0
                for nationName, shipsByType in nationsGroups.iteritems():
                    # Sorted by ship mass
                    for shipType, ships in shipsByType.iteritems():
                        if self.IsInPeace(nationName):
                            leftSideCount += 1
                        else:
                            rightSideCount += 1
                
                # Find a number of ships in column per side
                maxShipsInCol = 10
                suppressLeftSideLabels = leftSideCount > maxShipsInCol
                suppressRightSideLabels = rightSideCount > maxShipsInCol

                def colSideAdjusterFunc(counter):
                    if counter > maxShipsInCol:
                        while True:
                            counter /= 2
                            if counter <= maxShipsInCol:
                                break
                    return counter
                leftSideCount = colSideAdjusterFunc(leftSideCount)    
                rightSideCount = colSideAdjusterFunc(rightSideCount)    

                # Create groups
                dist = pow(star.Size, 1/2)/1.2
                starXmult = star.X/COORDINATES_MULT
                starYmult = star.Y/COORDINATES_MULT
                friendsX = starXmult - dist
                friendsY = starYmult - dist
                currentColsFriends = 0
                colsFriends = 0
                enemiesX = starXmult + dist
                enemiesY = starYmult - dist
                currentColsEnemies = 0
                colsEnemies = 0
                for nationName, shipsByType in nationsGroups.iteritems():
                    # Sorted by ship mass
                    for shipType, ships in sorted(\
                            shipsByType.iteritems(), key = lambda item: item[1][0].ShipTypeCalcMass()):
                        step = 0.1*pow(ships[0].ShipTypeCalcMass(), 1/3) + 0.3
                        isInPeace = self.IsInPeace(nationName)

                        if isInPeace:
                            x = friendsX
                            y = friendsY
                            friendsY += step
                            currentColsFriends += 1
                            if currentColsFriends == leftSideCount:
                                currentColsFriends = 0
                                colsFriends += 1
                                friendsX = (starXmult - dist) - 0.1*colsFriends
                                friendsY = (starYmult - dist) + (0 if colsFriends%2 == 0 else 0.1)
                        else:
                            x = enemiesX
                            y = enemiesY
                            enemiesY += step
                            currentColsEnemies += 1
                            if currentColsEnemies == rightSideCount:
                                currentColsEnemies = 0
                                colsEnemies += 1
                                enemiesX = (starXmult + dist) + 0.1*colsEnemies
                                enemiesY = (starYmult - dist) + (0 if colsEnemies%2 == 0 else 0.1)

                        sg = guObjects.CShipGroup(\
                            self.__IdGenerator.Next(), x, y, isInPeace, star.Id, ships)
                        if isInPeace:
                            sg.SuppressLabel = suppressLeftSideLabels
                        else:
                            sg.SuppressLabel = suppressRightSideLabels
                        self.__ShipGroups.append(sg)

        print "---PrepareShipGroups finished..."


    #---------------------------------------------------------------------------
    # Returns star by id. If not found - returns None
    def FindStar(self, aId):
        # Try to find in hash first
        if not aId in self.__StarsData:
            star = zx.Find(self.VisibleStars, lambda star: star.Id == aId)
            if star is not None:
                # Found: save in hash
                self.__StarsData[aId] = star
            else:
                return None
        
        return self.__StarsData[aId]
    

    #---------------------------------------------------------------------------
    # Returns star prefix from AutoManagers
    # (if a star is added to the manager, its first 3 letters are added to the prefix)
    def FindStarAMPrefix(self, aId):
        prefix = ""
        for am in self.AutoManagers:
            if zx.Find(am.Stars, lambda star: star == aId):
                prefix += "[" + am.Name[:3] + "]" #first 3 letters letter only
        return prefix

    #---------------------------------------------------------------------------
    # Returns star name by id. If not found - returns #aId
    def GetStarName(self, aId):
        star = self.FindStar(aId)
        return star.GetFullName() if star is not None else '#' + str(aId)

    
    #---------------------------------------------------------------------------
    # Returns list of ships in location
    def FindShipsForLocation(self, aX, aY):
        if len(self.__ShipsLocations) == 0:
            # Not created yet
            for ship in self.Ships:
                if not ship.X in self.__ShipsLocations:
                    # Coordinate is not registered yet
                    self.__ShipsLocations[ship.X] = []
                self.__ShipsLocations[ship.X].append(ship)

        if aX in self.__ShipsLocations:
            return zx.GetSubListFor(self.__ShipsLocations[aX], lambda ship: ship.Y == aY)

        return []

    #---------------------------------------------------------------------------
    # Extract a joining fleet name from ship assignment
    @staticmethod
    def GetJoiningFleetNameFromAssignment(aStr):
        if len(aStr) > 0:
            key = "joining fleet '"
            i1 = aStr.find(key)
            if i1 != -1:
                i1 += len(key)
                s = aStr[i1:]
                i2 = s.find("'")
                if i2 != -1:
                    return s[:i2]
        return ""


    #---------------------------------------------------------------------------
    # Returns a list of ships joining a fleet for a nation
    def GetShipsJoiningFleet(self, aFleetName, aNationName):
        return zx.GetSubListFor(\
            self.Ships,
            lambda ship: self.GetJoiningFleetNameFromAssignment(ship.Assignment) == aFleetName and \
                ship.NationName == aNationName)

    #---------------------------------------------------------------------------
    # Adds a menu item for list of ships joining a fleet for a nation
    # Returns True if item has been added and False if not
    def AddShipsJoiningFleetMenuItem(self, aParent, aMenu, aFleetName, aNationName):
        if aFleetName != "?" and \
                len(self.GetShipsJoiningFleet(aFleetName, aNationName)): 
            mi = wx.MenuItem(aMenu, wx.NewId(), "Ships joining the fleet '" + aFleetName + "'")
            aParent.Bind(\
                wx.EVT_MENU,
                lambda event: guShipsView.ShipsList(\
                    lambda: self.GetShipsJoiningFleet(aFleetName, aNationName),
                    "All ships joing fleet '" + aFleetName + "' for '" + aNationName + "'"), mi)
            aMenu.AppendItem(mi)
            return True
        return False


    #---------------------------------------------------------------------------
    # Returns a nation's rating object or None if not found
    # If aNationName is None, returns 'own' rating
    def FindNationRating(self, aNationName = None):
        name = aNationName if aNationName is not None else self.NationName
        return zx.Find(self.NationsRating, lambda item: item.Nation == name)
        
        
    #---------------------------------------------------------------------------
    # Returns nation techs set from rating. If not found, returns all 1        
    def GetNationTechs(self, aNationName = None):
        rating_info = self.FindNationRating(aNationName)
        if rating_info is None:
            rating_info = guObjects.CRatingData()
            rating_info.TechDrive = 1
            rating_info.TechWeapon = 1
            rating_info.TechShield = 1
            rating_info.TechCargo = 1
        return rating_info    

        
    #---------------------------------------------------------------------------
    # Loading report from a file
    # Returns True in success
    def Load(self, aFileName, aSuppressErrorDialog = False):
        res = False
        isArchive = False
        filePath = aFileName.value
        fileBody, fileExt = os.path.splitext(filePath)
        if fileExt == ".rar":
            guApp.Application.Log(zxLog.LOG_INFO, "Extracting report from archive " + filePath + "...")
            # It's an rar archive - extract to temporary file
            # remove old file in a case it's still there
            try:
                os.remove(fileBody + ".rep")
            except:
                pass

            # Extract file
            # (Note: using quotes to wrap potential spaces in names)
            def Extract(cmd):
                return os.system(cmd + "\"" + filePath + "\"" + " " + "\"" + os.path.dirname(filePath) + "\"") == 0
                
            if not Extract("rar e "):
                Extract("./rar e ") # this may happen on linux when rar not installed, but present in current dir

            filePath = fileBody + ".rep"
            if not os.path.isfile(filePath):
                guApp.Application.Log(zxLog.LOG_ERROR, "Failed to extract " + filePath)
            else:
                isArchive = True # loaded successfully - set the flag

        err = c_char_p("")
        if not self.API.guLoadReport(filePath, byref(err)):
            if not aSuppressErrorDialog:
                wxTools.ErrorReport(None, err.value, 'Error loading report')
        else:
            # Loaded successfully
            # Check report version
            serverVersionStr = guApp.Application.API.guGetServerVersion()
            apiVersionStr = guApp.Application.API.guGetAPIVersion()
            guApp.Application.Log(zxLog.LOG_SUCCESS, "Report '" + filePath + "' has been loaded successfully, server Version=" + serverVersionStr)
            if serverVersionStr != apiVersionStr:
                serverVersion = zxVersion.CVersion(serverVersionStr) # Server version
                apiVersion = zxVersion.CVersion(apiVersionStr) # API version
                if serverVersion.ProductVersion != apiVersion.ProductVersion:
                    guApp.Application.Log(zxLog.LOG_WARNING,
                                          'Your Client API Product Version <' + \
                                              str(apiVersion.ProductVersion) + 
                                          '> is different to the Server Product Version <' + \
                                              str(serverVersion.ProductVersion) + \
                                          '>. You are strongly advised to download the latest client.')
                elif serverVersion.FeatureVersion != apiVersion.FeatureVersion or \
                     serverVersion.BugFixVersion != apiVersion.BugFixVersion:
                    guApp.Application.Log(zxLog.LOG_WARNING,
                                          'Your Client API Version <' + \
                                              apiVersionStr + 
                                          '> misses some new features/bug fixes now available in the Server Version <' + \
                                              serverVersionStr + \
                                          '>. It is advised to download the latest client.')
            
            if self.Update():
                # Store successfully loaded file name
                self.FileName = aFileName.value
                res = True

            if isArchive:
                os.remove(filePath)

            return res


    #---------------------------------------------------------------------------
    # Reloads report
    def Reload(self):
        self.Load(c_char_p(self.FileName))


    #---------------------------------------------------------------------------
    # This method can be called to update data from API after user commands were executed
    # They may change some data
    def Update(self):
        self.NationName = self.API.guGetReportNation()
        self.Turn = self.API.guGetReportTurn()

        # Populate lists
        oldFileName = self.FileName
        self.__CleanUp()
        if not self.__LoadStars(0) \
           or not self.__LoadStars(1) \
           or not self.__LoadTransportOrders() \
           or not self.__LoadProdOrders() \
           or not self.__LoadNationsRating() \
           or not self.__LoadKnownNations() \
           or not self.__LoadReportMessages() \
           or not self.__LoadRulers() \
           or not self.__LoadSciences() \
           or not self.__LoadMoneyTransactions() \
           or not self.__LoadAutoManagers() \
           or not self.__LoadSpaceGroups() \
           or not self.__LoadShipTypes() \
           or not self.__LoadShips() \
           or not self.__LoadFleets() \
           or not self.__LoadAdditionalData():
            dial = wx.MessageDialog(None, 'Invalid report format', 'Error', wx.OK | wx.ICON_ERROR)
            dial.ShowModal()
            return False
        
        self.FileName = oldFileName
        
        return True

    #---------------------------------------------------------------------------
    # 'Normalizing' star coordinates to (0,0) basis to simplify calculations between virtual area and the screen
    def __NormalizeXXXStarCoordinates(self, aStars, aXDelta, aYDelta):
        for star in aStars:
            star.X -= aXDelta
            star.Y -= aYDelta
    def NormalizeCoordinates(self, aXDelta, aYDelta):
        # Star coordinates
        self.__NormalizeXXXStarCoordinates(self.VisibleStars, aXDelta, aYDelta)
        # Space groups
        self.__NormalizeXXXStarCoordinates(self.SpaceGroups, aXDelta, aYDelta)
        # Ships
        self.__NormalizeXXXStarCoordinates(self.Ships, aXDelta, aYDelta)
        
    
    #---------------------------------------------------------------------------
    # Returns true if report nation is GM
    def IsGM(self):
        return self.NationName == "___GM___"
        
    
    #---------------------------------------------------------------------------
    # Returns diplomatic state to aNationName. It can be
    # '' if aNationName is a report nation or GM or 'peace' if report nation is in peace with aNationName else 'war'
    def GetDiplomaticState(self, aNationName):
        return '' if self.IsGM() or self.NationName == aNationName else ('peace' if self.IsInPeace(aNationName) else 'war')

    #---------------------------------------------------------------------------
    # Helper: returns colour text atribute for diplomatic state
    def GetDiplomaticStateColourAttr(self, aNationName):
        ds = self.GetDiplomaticState(aNationName)
        if ds == 'peace':
            return self.__attrPeace
        elif ds == 'war':
            return self.__attrWar
        return None

    #---------------------------------------------------------------------------
    # Calculates distance in parcecs between 2 objects
    def CalcDistance(self, aX1, aY1, aX2, aY2):
        if aX1 == aX2 and aY1 == aY2:
            return 0
        dx = (aX1 - aX2)/COORDINATES_MULT
        dy = (aY1 - aY2)/COORDINATES_MULT
        return math.sqrt(dx*dx + dy*dy)


    #---------------------------------------------------------------------------
    # Calculates distance in parcecs between 2 stars
    def CalcDistanceBetweenSpaceObjects(self, a1, a2):
        return self.CalcDistance(a1.X, a1.Y, a2.X, a2.Y) if a1 is not None and a2 is not None else 0
        

    #---------------------------------------------------------------------------
    # Returns a flag image for nation aNationName
    def GetNationFlagImage(self, aNationName):
        if aNationName == "":
            aNationName = self.NationName
        if not aNationName in self.NationFlagsImages:
            # Flag image has not been created yet - load it from API in a form of coded hex string
            imageCode = self.API.guGetNationFlag(aNationName)
            flag = pygame.Surface((self.FlagTemplateWidth, self.FlagTemplateHeight))
            if imageCode != "" and len(imageCode) == self.FlagTemplateHeight * self.FlagTemplateWidth * 6: # matrix each coded 6 bytes
                # We've got a valid format: code colours
                for row in range(self.FlagTemplateHeight):
                    for col in range(self.FlagTemplateWidth):
                        start = (row * self.FlagTemplateWidth + col)*6
                        colourStr = imageCode[start:start + 6]
                        pygame.gfxdraw.pixel(flag, col, row, (int(colourStr[0:2], 16), int(colourStr[2:4], 16), int(colourStr[4:6], 16)))
            self.NationFlagsImages[aNationName] = flag

        return self.NationFlagsImages[aNationName]

    #---------------------------------------------------------------------------
    # Returns true if nation1 is in peace with nation2
    def IsStatePeace(self, aNationName1, aNationName2):
        return self.API.guIsPeaceState(aNationName1, aNationName2)
        
    #---------------------------------------------------------------------------
    # Returns True if nation is in peace with report's nation
    def IsInPeace(self, aNationName):
        return self.IsGM() or self.NationName == aNationName or self.IsStatePeace(self.NationName, aNationName)
        
    #---------------------------------------------------------------------------
    # Returns relative colour to other nation
    # If aFaded is True the colours returned half bright
    def GetNationColour(self, aNationName, aFaded = False):
        c = self.NATION_COLOUR if self.NationName == aNationName else \
                                  (self.NEUTRAL_COLOUR if self.IsInPeace(aNationName) else self.ENEMY_COLOUR)
        return FadeColour(c, 2) if aFaded else c
                                          

    #---------------------------------------------------------------------------
    # Returns a ship type by nation name and type name or None if not found
    def FindShipType(self, aNationName, aTypeName):
        for st in self.ShipTypes:
            if st.NationName == aNationName and st.Name == aTypeName:
                return st
        return None
    
    #---------------------------------------------------------------------------
    # Returns layout for given ship
    def GetShipLayout(self, aShipComponents, aNationName, aTypeName):
        k = guObjects.CShipType.GetTypeKey(aNationName, aTypeName)
        if not k in self.__ShipLayouts:
            # has not been created yet
            self.__ShipLayouts[k] = guShipLayout.CShipLayout(aShipComponents)
        return self.__ShipLayouts[k]

    #---------------------------------------------------------------------------
    # Returns image for given ship type
    # If aTypeName has "_missile_" is true - special missile image is returned
    def GetShipTypeImage(self, aNationName, aTypeName, aColour):
        k = guObjects.CShipType.GetTypeKey(aNationName, aTypeName)
        if not k in self.__ShipLayouts:
            # Try to find the ship type
            st = self.FindShipType(aNationName, aTypeName)
            if st is not None:
                return self.GetShipImage(st.Components, aNationName, aTypeName, aColour)
            # Unknown type
            # Create a dummy ship
            components = []
            if aTypeName.find("_missile_") != -1:
                # It's a missile - special missile-like looking ship image
                # This will create a 'rocket' picture: must be only one component and destroyed mass bigger than mass
                components.append(guObjects.CShipComponent(0, guObjects.CShipComponent.SHIP_COMPONENT_MISSILE, 1, 1, 5))
            else:
                # Dummy image - sphere
                components.append(guObjects.CShipComponent(0, guObjects.CShipComponent.SHIP_COMPONENT_SHIELD, 1))
            return self.GetShipImage(components, aNationName, aTypeName, aColour)
        # Known image - get from collection
        return self.__ShipImages[k]
        
    #---------------------------------------------------------------------------
    # Returns image for given ship
    def GetShipImage(self, aShipComponents, aNationName, aTypeName, aColour):
        k = guObjects.CShipType.GetTypeKey(aNationName, aTypeName)
        if not k in self.__ShipImages:
            # has not been created yet
            self.__ShipImages[k] = guObjects.CShipType.CreateImage(aColour, self.GetShipLayout(aShipComponents, aNationName, aTypeName))
        return self.__ShipImages[k]


    #---------------------------------------------------------------------------
    # Returns state colour for the star system related to the report
    def GetStateColour(self, aStar):
        o = CStarOwnership(aStar)
        if o.IsOwn:
            return CReport.NATION_COLOUR
        elif o.IsKnown:
            starOwner = aStar.GetOwner()
            k = GetGrayingKoef(o.IsActualKnown)
            if starOwner == "":
                return FadeColour((0, 255, 0), k)
            else: 
                if self.IsStatePeace(self.NationName, starOwner):
                    c = CReport.NEUTRAL_COLOUR
                else:
                    c = CReport.ENEMY_COLOUR
                return FadeColour(c, k)
        return (80, 80, 80)

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

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

#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Returns faded colour with graying koef
def FadeColour(aColour, k):
    return (aColour[0]/k, aColour[1]/k, aColour[2]/k)

#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Returns graying koef koef depending on aIsActualKnown
def GetGrayingKoef(aIsActualKnown):
    return 1.5 if aIsActualKnown else 2

