#-------------------------------------------------------------------------------
# Name:        CvStrategyOverlay.py
# Purpose:     Draws the strategy overlay itself.
#              CvOverlayScreen.py does the editing.
#              Contains:
#              -Dot Mapper
#              -Categorized signs
#
# Author:      Del69, EmperorFool
#
# Created:     11/12/2008
#-------------------------------------------------------------------------------

from CvPythonExtensions import *
import BugCore
import BugPath
import BugUtil
import CvOverlayScreenUtils
import PlayerUtil
import SdToolKit

COLOR_KEYS = None
PALETTE_WIDTH = None

gc = CyGlobalContext()
StratLayerOpt = BugCore.game.StrategyOverlay

g_layers = {}

def init(paletteWidth=3, paletteColors=None):
    global COLOR_KEYS, PALETTE_WIDTH
    
    # setup palette width
    if paletteWidth:
        PALETTE_WIDTH = paletteWidth
    else:
        PALETTE_WIDTH = 10
    
    # setup palette colors
    if paletteColors:
        COLOR_KEYS = paletteColors
    else:
        PALETTE_WIDTH = 10  # override because it has 127 colors
        COLOR_KEYS = []
        try:
            for index in range(200):
                info = gc.getColorInfo(index)
                COLOR_KEYS.append(info.getType())
        except:
            pass
    
    # create layers
    DotMapLayer()

def getLayer(id):
    return g_layers[id]

def callEachLayer(func, *args):
    for layer in g_layers.itervalues():
        func(layer, *args)


## Event Handlers

def onGameStart(argsList):
    def callReset(layer):
        layer.reset()
    callEachLayer(callReset)

def onLoad(argsList):
    def callRead(layer):
        layer.read()
    callEachLayer(callRead)
    if StratLayerOpt.isShowDotMap():
        def redraw():
            getDotMap().redrawCities()
        BugUtil.deferCall(redraw)

def onPreSave(argsList):
    def callWrite(layer):
        layer.write()
    callEachLayer(callWrite)

def onBeginActivePlayerTurn(args):
    def callBeginActivePlayerTurn(layer, ePlayer):
        layer.onBeginActivePlayerTurn(ePlayer)
    callEachLayer(callBeginActivePlayerTurn, args[0])

def onSwitchHotSeatPlayer(args):
    def callSwitchHotSeatPlayer(layer, ePlayer):
        layer.onSwitchHotSeatPlayer(ePlayer)
    callEachLayer(callSwitchHotSeatPlayer, args[0])

MSG_ADD_CITY = 500
MSG_REMOVE_CITY = 501
def onModNetMessage(args):
    iData1, iData2, iData3, iData4, iData5 = args
    if iData1 == MSG_ADD_CITY:
        getDotMap().addCityMessage(iData2, iData3, iData4, iData5)
    elif iData1 == MSG_REMOVE_CITY:
        getDotMap().removeCityMessage(iData2, iData3)
    else:
        return 0
    return 1

def onEnabledOptionChanged(option, value):
    pass


## Base Strategy Layer Class

class StrategyLayer(object):
    """
    Provides common functionality for all of the strategy layers.
    """
    def __init__(self, id):
        self.MOD_SAVE_ID = "StrategyOverlay"
        self.INVISIBLE_COLOR = NiColorA(0, 0, 0, 0)
        self.id = id
        self.visible = False
        self.editing = False
        self.dirty = False
        g_layers[id] = self
        self.reset()
    
    def reset(self):
        """
        Resets the data to a blank state and clears the dirty flag.
        """
        self.dirty = False
    
    def read(self):
        """
        Reads the data from the game and clears the dirty flag.
        """
        self.dirty = False
    
    def write(self):
        """
        Writes the data to the game and clears the dirty flag.
        """
        self.dirty = False
    
    def toggleVisibility(self):
        if self.visible:
            self.hide()
        else:
            self.show()
    
    def show(self):
        if not self.visible:
            self.visible = True
            return True
        return False
    
    def hide(self):
        if self.visible:
            self.freeze()
            self.visible = False
            return True
        return False
    
    def toggleEditing(self):
        if not self.editing:
            self.edit()
        else:
            self.freeze()
    
    def edit(self):
        if not self.editing:
            self.show()
            self.editing = True
            return True
        return False
    
    def freeze(self):
        if self.editing:
            self.editing = False
            return True
        return False
    
    def onBeginActivePlayerTurn(self, ePlayer):
        pass
    
    def onSwitchHotSeatPlayer(self, ePlayer):
        pass


## ----------------------------------------------------------------------
## DOT MAP
## ----------------------------------------------------------------------

DOTMAP_LAYER = "DotMap"
X, Y = 0, 1    # used in point tuples instead of creating a new class

g_DotMap = None

class City:
    """
    Holds the data for a single dot-mapped city.
    """
    def __init__(self, point, color, layer):
        self.point = point
        self.color = color
        self.layer = layer
    
    def __eq__(self, other):
        return self.point == other.point and self.color == other.color
    
    def __str__(self):
        return "(%d,%d) on %d" % (self.point[X], self.point[Y], self.layer)
    
    def isAt(self, point):
        return self.point == point
    
    def samePoint(self, other):
        return self.point == other.point
    
    def sameColor(self, other):
        return self.color == other.color
    
    def sameLayer(self, other):
        return self.layer == other.layer

def getDotMap():
    global g_DotMap
    if g_DotMap is None:
        BugUtil.error("CvStrategyOverlay has not been initialized")
    return g_DotMap

def hideDotMap(args=None):
    getDotMap().hide()
    StratLayerOpt.setShowDotMap(False)

def toggleDotMapVisibility(args=None):
    getDotMap().toggleVisibility()
    StratLayerOpt.setShowDotMap(getDotMap().visible)

def toggleDotMapEditMode(args=None):
    getDotMap().toggleEditing()
    if not getDotMap().editing and not StratLayerOpt.isShowDotMap():
        getDotMap().hide()

def onShowDotMapOptionChanged(option, value):
    if value:
        getDotMap().show()
    else:
        getDotMap().hide()

def onDotMapOptionChanged(option, value):
    getDotMap().optionChanged(option, value)

class DotMapLayer(StrategyLayer):
    """
    Draws city crosses of different colors so the user can create a dot-map.
    """
    def __init__(self):
        super(DotMapLayer, self).__init__(DOTMAP_LAYER)
        global g_DotMap
        g_DotMap = self
        # constants
        self.CITY_SAVE_ID = "CityDataDict"
        self.HIGHLIGHT_CROSS_LAYER = 8
        self.FIRST_CROSS_LAYER = 9
        self.NUM_CROSS_LAYERS = 36 #len(COLOR_KEYS)
        self.DOT_LAYER = PlotLandscapeLayers.PLOT_LANDSCAPE_LAYER_NUMPAD_HELP
        self.NO_DOT_STYLE = PlotStyles.PLOT_STYLE_NONE
        self.MAX_DOT_STYLE = PlotStyles.PLOT_STYLE_WAVES
        self.BFC_OFFSETS = []
        for x in range(-2, 3):
            for y in range(-2, 3):
                if abs(x) != 2 or abs(y) != 2:
                    self.BFC_OFFSETS.append((x, y))
        # default options
        self.CROSS_ALPHA = 50.0
        self.DOT_ALPHA = 50.0
        self.HIGHLIGHT_CROSS_ALPHA = 100.0
        self.HIGHLIGHT_DOT_ALPHA = 100.0
        self.DRAW_DOTS = True
        self.DOT_STYLE = PlotStyles.PLOT_STYLE_DOT_TARGET
        self.readOptions()
        # state
        self.highlightedCity = None
    
    def reset(self):
        self.cities = {}
        self.dirty = False
    
    def read(self):
        data = SdToolKit.sdGetGlobal(self.MOD_SAVE_ID, self.CITY_SAVE_ID)
        self.clearCityLayers()
        if data is not None:
            self.cities = self.updateData(data)
            self.dirty = False
        else:
            self.reset()
    
    def updateData(self, data):
        """
        Upgrade previous data formats to latest format.
        """
        if len(data) == 0:
            # empty, don't care
            return data
        for key, value in data.iteritems():
            if isinstance(key, int):
                # data in latest format
                return data
            else:
                # old format, convert below
                break
        # find first living, human player and assign all data to them
        # if none found, assign to player 0
        for player in PlayerUtil.players(alive=True, human=True):
            ePlayer = player.getID()
            break
        else:
            ePlayer = 0
        newData = {}
        cities = {}
        newData[ePlayer] = cities
        for point, (color, layer) in data.iteritems():
            # use new point-based layer scheme
            grid = 6
            layer = (point[X] % grid) * grid + (point[Y] % grid)
            cities[point] = City(point, color, layer)
        return newData
        
    def write(self):
        if self.dirty:
            SdToolKit.sdSetGlobal(self.MOD_SAVE_ID, self.CITY_SAVE_ID, self.cities)
            self.dirty = False
    
    def show(self):
        if super(DotMapLayer, self).show():
            self.redrawCities()
    
    def hide(self):
        if super(DotMapLayer, self).hide():
            self.clearCityLayers()
    
    def edit(self):
        if super(DotMapLayer, self).edit():
            CvOverlayScreenUtils.showOverlayScreen()
    
    def freeze(self):
        if super(DotMapLayer, self).freeze():
            self.unhighlightCity()
            CvOverlayScreenUtils.hideOverlayScreen()
    
    def onBeginActivePlayerTurn(self, ePlayer):
        if StratLayerOpt.isShowDotMap():
            self.show()
    
    def onSwitchHotSeatPlayer(self, ePlayer):
        self.hide()
    
    
    def hasCities(self, ePlayer):
        return ePlayer in self.cities
    
    def hasCity(self, ePlayer, point):
        return self.hasCities(ePlayer) and point in self.cities[ePlayer]
    
    def getCities(self, ePlayer):
        if self.hasCities(ePlayer):
            return self.cities[ePlayer]
        cities = {}
        self.cities[ePlayer] = cities
        return cities
    
    def getCity(self, ePlayer, point):
        if self.hasCities(ePlayer):
            cities = self.cities[ePlayer]
            if point in cities:
                return cities[point]
        return None
    
    def iterCities(self, ePlayer):
        """
        Iterates over the player's cities.
        """
        if self.hasCities(ePlayer):
            for city in self.getCities(ePlayer).itervalues():
                yield city
    
    
    def addCityAt(self, point, color, layer):
        """
        Sends a message to add a city for the active player at the given point.
        """
        
        # check if the plot has been revealed
        pPlayer = gc.getPlayer(PlayerUtil.getActivePlayerID())
        if not pPlayer or pPlayer.isNone():
            BugUtil.warn("CvStrategyOverlay.addCityAt() was passed an invalid player id: %s" % (str(ePlayer)))
            return False
        eTeam = pPlayer.getTeam()
        pPlot = gc.getMap().plot(point[X], point[Y])
        if pPlot.isRevealed(eTeam, False):
            CyMessageControl().sendModNetMessage(MSG_ADD_CITY, PlayerUtil.getActivePlayerID(), point[X] * 1000 + point[Y], color, layer)

    def addCityMessage(self, ePlayer, xy, color, layer):
        """
        Processes a message to add a city.
        """
        x = xy / 1000
        y = xy % 1000
        city = City((x, y), color, layer)
        self.addCity(ePlayer, city)
    
    def addCity(self, ePlayer, city):
        """
        Adds the city to the data set and draws its dot and cross.
        """
        if self.hasCity(ePlayer, city.point):
            oldCity = self.getCity(ePlayer, city.point)
            if city == oldCity:
                return
            BugUtil.debug("DotMap - replacing city at %s", city.point)
            self.removeCity(ePlayer, oldCity)
        BugUtil.debug("DotMap - adding city %s", city)
        self.getCities(ePlayer)[city.point] = city
        self.dirty = True
        if ePlayer == PlayerUtil.getActivePlayerID():
            self.drawCity(city, self.CROSS_ALPHA, self.DOT_ALPHA)
    
    
    def removeCityAt(self, point):
        """
        Sends a message to remove the active player's city at the given point.
        """
        ePlayer = PlayerUtil.getActivePlayerID()
        if self.hasCity(ePlayer, point):
            CyMessageControl().sendModNetMessage(MSG_REMOVE_CITY, ePlayer, point[X] * 1000 + point[Y], -1, -1)
        else:
            self.freeze()
    
    def removeCityMessage(self, ePlayer, xy):
        """
        Processes a message to remove a city.
        """
        x = xy / 1000
        y = xy % 1000
        self.removeCity(ePlayer, self.getCity(ePlayer, (x, y)))
        
    def removeCity(self, ePlayer, city):
        """
        Removes the city from the data set and erases its dot and cross.
        """
        if city:
            BugUtil.debug("DotMap - removing city %s", city)
            del self.getCities(ePlayer)[city.point]
            self.dirty = True
            if ePlayer == PlayerUtil.getActivePlayerID():
                self.redrawCrosses(city.layer)
                self.eraseDot(city, self.DOT_ALPHA)
        else:
            BugUtil.warn("City doesn't exist")
    
    
    def highlightCity(self, point, color):
        """
        Highlights the given city location by drawing it using the given color on the highlight layer.
        Unhighlights the currently highlighted city if there is one.
        
        If there is no city there (N), the new city is drawn (C).
        If the city is on the same layer (S), nothing is done (N). --> WC
        If the city is on a different layer (D), the city's layer is redrawn without it (W) and the new city is drawn (C).
        """
        city = City(point, color, self.HIGHLIGHT_CROSS_LAYER)
        if self.highlightedCity:
            if self.highlightedCity == city:
                return
            else:
                self.unhighlightCity()
        self.highlightedCity = city
        ePlayer = PlayerUtil.getActivePlayerID()
        existingCity = self.getCity(ePlayer, point)
        if existingCity is not None:
            self.redrawCrosses(existingCity.layer, point)
            self.eraseDot(existingCity, self.DOT_ALPHA)
        self.drawCross(city, self.HIGHLIGHT_CROSS_ALPHA)

    def unhighlightCity(self):
        """
        Removes the highlight from the existing city location if there is one.
        
        If there is no city there (N), the current layer is redrawn (L) and the dot is erased (d).
        If the city is on the same layer (S), nothing is done (N). --> LC
        If the city is on a different layer (D), the current layer is redrawn (L) and the city is drawn (C).
        """
        if self.highlightedCity:
            point = self.highlightedCity.point
            self.clearHighlightCrossLayer()
            ePlayer = PlayerUtil.getActivePlayerID()
            city = self.getCity(ePlayer, point)
            if city is not None:
                self.drawCity(city, self.CROSS_ALPHA, self.DOT_ALPHA)
            self.highlightedCity = None
    
    
    def redrawCities(self):
        """
        Erases all city layers and draws all of the cities.
        """
        self.clearCityLayers()
        self.drawCities()
    
    def redrawCrosses(self, layer, skip=None):
        """
        Erases the given layer and draws all city crosses in that layer.
        """
        self.clearCrossLayer(layer)
        self.drawCrosses(layer, skip)
    
    def redrawDots(self):
        """
        Erases and redraws all city dots as they are all in the same layer.
        """
        self.clearDotLayer()
        self.drawDots()
    
    def drawCities(self, skip=None):
        """
        Draws all of the cities except skip, if given.
        """
        crossAlpha = self.CROSS_ALPHA
        dotAlpha = self.DOT_ALPHA
        for city in self.iterCities(PlayerUtil.getActivePlayerID()):
            if not city.isAt(skip):
                self.drawCity(city, crossAlpha, dotAlpha)
    
    def drawCrosses(self, layer=None, skip=None):
        """
        Draws the cross for every city in the given layer.
        """
        crossAlpha = self.CROSS_ALPHA
        for city in self.iterCities(PlayerUtil.getActivePlayerID()):
            if not city.isAt(skip):
                if layer is None or layer == city.layer:
                    self.drawCross(city, crossAlpha)
    
    def drawDots(self, skip=None):
        """
        Draws the dot for every city.
        """
        dotAlpha = self.DOT_ALPHA
        for city in self.iterCities(PlayerUtil.getActivePlayerID()):
            if not city.isAt(skip):
                self.drawDot(city, dotAlpha)
    
    def drawCity(self, city, crossAlpha, dotAlpha):
        """
        Draws the cross and dot for a single city.
        """
        self.drawCross(city, crossAlpha)
        self.drawDot(city, dotAlpha)
    
    def drawCross(self, city, alpha):
        """
        Draws the cross for a single city.
        """
        x, y = city.point
        color = gc.getColorInfo(city.color).getType()
        layer = city.layer
        for dx, dy in self.BFC_OFFSETS:
            CyEngine().fillAreaBorderPlotAlt(x + dx, y + dy, layer, color, alpha)
    
    def drawDot(self, city, alpha):
        """
        Draws the dot for a single city.
        """
        if self.DRAW_DOTS:
            x, y = city.point
            colorInfo = gc.getColorInfo(city.color)
            if BugPath.isMac():
                color = colorInfo.getColor()
                CyEngine().addColoredPlot(x, y, NiColorA(color.r, color.g, color.b, alpha), self.DOT_LAYER)
            else:
                CyEngine().addColoredPlotAlt(x, y, self.DOT_STYLE, self.DOT_LAYER, colorInfo.getType(), alpha)
    
    def eraseDot(self, city, alpha):
        """
        Erases the dot for a single city.
        """
        if self.DRAW_DOTS:
            x, y = city.point
            if BugPath.isMac():
                CyEngine().addColoredPlot(x, y, self.INVISIBLE_COLOR, self.DOT_LAYER)
            else:
                CyEngine().addColoredPlotAlt(x, y, self.NO_DOT_STYLE, self.DOT_LAYER, "COLOR_BLACK", alpha)
    
    def clearCityLayers(self):
        """
        Erases all city crosses and dots.
        """
        self.clearHighlightCrossLayer()
        for index in range(self.NUM_CROSS_LAYERS):
            self.clearCrossLayer(index + self.FIRST_CROSS_LAYER)
        self.clearDotLayer()

    def clearHighlightCrossLayer(self):
        """
        Clears the indexed border layer.
        """
        self.clearCrossLayer(self.HIGHLIGHT_CROSS_LAYER)

    def clearCrossLayer(self, layer):
        """
        Clears the indexed border layer.
        """
        CyEngine().clearAreaBorderPlots(layer)

    def clearDotLayer(self):
        """
        Clears all the dots from screen.
        """
        CyEngine().clearColoredPlots(self.DOT_LAYER)
    
    def percentToAlpha(self, percent):
        return min(100, max(0, percent)) / 100.0
    
    
    def readOptions(self):
        self.CROSS_ALPHA = self.percentToAlpha(StratLayerOpt.getDotMapBrightness())
        self.DOT_ALPHA = self.percentToAlpha(StratLayerOpt.getDotMapBrightness())
        self.HIGHLIGHT_CROSS_ALPHA = self.percentToAlpha(StratLayerOpt.getDotMapHighlightBrightness())
        self.HIGHLIGHT_DOT_ALPHA = self.percentToAlpha(StratLayerOpt.getDotMapHighlightBrightness())
        self.DRAW_DOTS = StratLayerOpt.isDotMapDrawDots()
        self.DOT_STYLE = min(self.MAX_DOT_STYLE, max(0, StratLayerOpt.getDotMapDotIcon()))
    
    def optionChanged(self, option, value):
        """
        Redraws the layer if it is currently visible.
        """
        self.unhighlightCity()
        self.readOptions()
        if self.visible:
            self.redrawCities()
