## AttitudeUtil
##
## Utility functions for dealing with AI Attitudes.
##
## Notes
##   - Must be initialized externally by calling init()
##
## Copyright (c) 2008 The BUG Mod.
##
## Author: Ruff_hi, EmperorFool

from CvPythonExtensions import *
import BugUtil
import FontUtil
import PlayerUtil
import re

NUM_ATTITUDES = 5

DEFAULT_COLORS = (
    "COLOR_RED", 
    "COLOR_CYAN", 
    "COLOR_CLEAR", 
    "COLOR_GREEN", 
    "COLOR_YELLOW",
)

ATTITUDE_KEYS = (
    "furious", 
    "annoyed", 
    "cautious", 
    "pleased", 
    "friendly", 
)
ATTITUDE_COLORS = None
ATTITUDE_ICONS = None

# These are also defaults; XML will override
ATTITUDE_MODIFIERS = (
    "TXT_KEY_MISC_ATTITUDE_LAND_TARGET",
    "TXT_KEY_MISC_ATTITUDE_WAR",
    "TXT_KEY_MISC_ATTITUDE_PEACE",
    "TXT_KEY_MISC_ATTITUDE_SAME_RELIGION",
    "TXT_KEY_MISC_ATTITUDE_DIFFERENT_RELIGION",
    "TXT_KEY_MISC_ATTITUDE_BONUS_TRADE",
    "TXT_KEY_MISC_ATTITUDE_OPEN_BORDERS",
    "TXT_KEY_MISC_ATTITUDE_DEFENSIVE_PACT",
    "TXT_KEY_MISC_ATTITUDE_RIVAL_DEFENSIVE_PACT",
    "TXT_KEY_MISC_ATTITUDE_RIVAL_VASSAL",
    "TXT_KEY_MISC_ATTITUDE_SHARE_WAR",
    "TXT_KEY_MISC_ATTITUDE_FAVORITE_CIVIC",
    "TXT_KEY_MISC_ATTITUDE_TRADE",
    "TXT_KEY_MISC_ATTITUDE_RIVAL_TRADE",
    "TXT_KEY_MISC_ATTITUDE_FREEDOM",
    "TXT_KEY_MISC_ATTITUDE_EXTRA_GOOD",
# Show Hidden Attitudes in BULL
    "TXT_KEY_MISC_ATTITUDE_FIRST_IMPRESSION",
    "TXT_KEY_MISC_ATTITUDE_TEAM_SIZE",
    "TXT_KEY_MISC_ATTITUDE_BETTER_RANK",
    "TXT_KEY_MISC_ATTITUDE_WORSE_RANK",
    "TXT_KEY_MISC_ATTITUDE_LOW_RANK",
    "TXT_KEY_MISC_ATTITUDE_LOST_WAR",
)
MODIFIER_STRING_TO_KEY = None

gc = CyGlobalContext()
ArtFileMgr = CyArtFileMgr()
localText = CyTranslator()


## Initialization

def init (colors=DEFAULT_COLORS, modifiers=None):
    """Initializes this module, raising ConfigError if any problems occur."""
    # create font icons for each attitude level
    global ATTITUDE_ICONS
    ATTITUDE_ICONS = [FontUtil.getChar(key) for key in ATTITUDE_KEYS]
    if len(ATTITUDE_ICONS) != NUM_ATTITUDES:
        raise BugUtil.ConfigError("Failed to create attitude icons")
    
    # convert colors to type IDs
    if len(colors) != NUM_ATTITUDES:
        raise BugUtil.ConfigError("Expected %d colors" % NUM_ATTITUDES)
    global ATTITUDE_COLORS
    ATTITUDE_COLORS = map(gc.getInfoTypeForString, colors)
    invalidCount = ATTITUDE_COLORS.count(-1)
    if invalidCount > 0:
        invalid = []
        for id, color in zip(ATTITUDE_COLORS, colors):
            if id == -1:
                invalid.append(color)
        raise BugUtil.ConfigError("Given %d invalid colors: %s" % (invalidCount, str(invalid)))

    # init modifiers, overriding default list as necessary
    if modifiers is not None and (isinstance(modifiers, list) or isinstance(modifiers, tuple)):
        global ATTITUDE_MODIFIERS
        ATTITUDE_MODIFIERS = tuple(modifiers)
    initModifiers()


## Attitude

def hasAttitude (nPlayer, nTarget):
    """Returns True if nTarget can see nPlayer's attitude toward them."""
    return (nPlayer != -1 and nTarget != -1 and nPlayer != nTarget
            and gc.getTeam(gc.getPlayer(nPlayer).getTeam()).isHasMet(gc.getPlayer(nTarget).getTeam()))

def getAttitudeString (nPlayer, nTarget):
    """Returns the full hover text with attitude modifiers nPlayer has toward nTarget."""
    if hasAttitude(nPlayer, nTarget):
        return CyGameTextMgr().getAttitudeString(nPlayer, nTarget)
    return None

def getAttitudeCategory (nPlayer, nTarget):
    """Returns the attitude level nPlayer has toward nTarget [0,4]."""
    if hasAttitude(nPlayer, nTarget):
        return gc.getPlayer(nPlayer).AI_getAttitude(nTarget)
    return None

def getAttitudeColor (nPlayer, nTarget):
    """Returns the color of the attitude nPlayer has toward nTarget."""
    iCategory = getAttitudeCategory(nPlayer, nTarget)
    if iCategory is not None:
        return ATTITUDE_COLORS[iCategory]
    return -1

def getAttitudeIcon (nPlayer, nTarget):
    """Returns the font icon of the attitude nPlayer has toward nTarget."""
    iCategory = getAttitudeCategory(nPlayer, nTarget)
    if iCategory is not None:
        return ATTITUDE_ICONS[iCategory]
    return ""

def getAttitudeCount (nPlayer, nTarget):
    """Returns the total attitude modifiers nPlayer has toward nTarget."""
    sAttStr = getAttitudeString(nPlayer, nTarget)
    if sAttStr == None:
        return
    nAtt = 0
    # TODO: Replace with simple line-by-line handling
    #       so it doesn't get tricked by leader names (": " fixes issue)
    ltPlusAndMinuses = re.findall ("[-+][0-9]+\s?: ", sAttStr)
    for i in range (len (ltPlusAndMinuses)):
        nAtt += int (ltPlusAndMinuses[i][:-2])
    return nAtt


def getAttitudeText (nPlayer, nTarget, bNumber, bSmily, bWorstEnemy, bWarPeace):
    """Returns a string describing the attitude nPlayer has toward nTarget."""
    nAttitude = getAttitudeCount (nPlayer, nTarget)
    if nAttitude == None:
        return None
    
    szText = []
    if bSmily:
        szText.append(getAttitudeIcon(nPlayer, nTarget))
    if bNumber:
        szText.append(BugUtil.colorText(u"%+d" % nAttitude, getAttitudeColor(nPlayer, nTarget)))
    
    szIcons = u""
    pPlayer = gc.getPlayer(nPlayer)
    pTarget = gc.getPlayer(nTarget)
    if bWorstEnemy and isWorstEnemy(pPlayer, pTarget):
        szIcons += FontUtil.getChar("angrypop")
    
    if bWarPeace:
        nTeam = pPlayer.getTeam()
        pTeam = gc.getTeam(nTeam)
        nTargetTeam = pTarget.getTeam()
        pTargetTeam = gc.getTeam(nTargetTeam)
        if pTeam.isAtWar(nTargetTeam):
            szIcons += FontUtil.getChar("war")
        elif gc.getGame().getActiveTeam() in (nTeam, nTargetTeam):
            bPeace = False
            if pTeam.isForcePeace(nTargetTeam):
                bPeace = True
            elif pTargetTeam.isAVassal():
                for nOwnerTeam in range(gc.getMAX_TEAMS()):
                    if pTargetTeam.isVassal(nOwnerTeam) and pTeam.isForcePeace(nOwnerTeam):
                        bPeace = True
                        break
            if bPeace:
                szIcons += FontUtil.getChar("peace")
    if szIcons:
        szText.append(szIcons)
    
    return u" ".join(szText)

def initModifiers (argsList=None):
    """ Creates the dictionary that maps strings to modifier keys. """
    global MODIFIER_STRING_TO_KEY
    MODIFIER_STRING_TO_KEY = {}
    for sKey in ATTITUDE_MODIFIERS:
        sStr = BugUtil.getPlainText(sKey, "NONE")
        if (sStr != "NONE"):
            # These modifier strings should contain the '%d: ' prefix
            # so we need to extract just the portion in quotes.
            pMatch = re.match(u"^.*(\".+\")", sStr, re.UNICODE)
            if (pMatch):
                MODIFIER_STRING_TO_KEY[unicode(pMatch.group(1))] = sKey
    for iMemType in range(MemoryTypes.NUM_MEMORY_TYPES):
        sKey = str(gc.getMemoryInfo(iMemType).getTextKey())
        sStr = BugUtil.getPlainText(sKey, "NONE")
        if (sStr != "NONE"):
            # These modifier strings have no extra text and so
            # we can use them directly
            MODIFIER_STRING_TO_KEY[unicode(sStr)] = sKey
    #BugUtil.debug(u"initModifiers() MODIFIER_STRING_TO_KEY = %s" % str(MODIFIER_STRING_TO_KEY))

class Attitude:
    """ Holds summary of attitude that this player has toward target player.

    If the two IDs are the same or the two players have not met, the
    class will be filled with generic default values. We also check to see
    that the active player has met both players, and the accessor functions
    will react differently depending on if this is the case. 
    """
    def __init__ (self, iThisPlayer, iTargetPlayer):
        pActiveTeam = gc.getTeam(gc.getActivePlayer().getTeam())
        iThisTeam = gc.getPlayer(iThisPlayer).getTeam()
        pThisTeam = gc.getTeam(iThisTeam)
        iTargetTeam = gc.getPlayer(iTargetPlayer).getTeam()
        self.iThisPlayer = iThisPlayer
        self.iTargetPlayer = iTargetPlayer
        self.iAttitudeSum = 0
        self.iAttitudeModifiers = {}
        self.bHasActiveMetBoth = CyGame().isDebugMode() or (pActiveTeam.isHasMet(iThisTeam) and pActiveTeam.isHasMet(iTargetTeam)) 
        self.eAttitudeType = AttitudeTypes.NO_ATTITUDE
        # This might be better off being something descriptive such as
        # "players have not met" or "players are the same"
        self.sAttitudeString = ""
        if (iThisPlayer != iTargetPlayer and pThisTeam.isHasMet(iTargetTeam)):
            self.eAttitudeType = gc.getPlayer(iThisPlayer).AI_getAttitude(iTargetPlayer)
            self.sAttitudeString = CyGameTextMgr().getAttitudeString(iThisPlayer, iTargetPlayer)
            for sLine in self.sAttitudeString.split("\n"):
                #BugUtil.debug(u"LINE: %s" % (sLine))
                pMatch = re.match("^.*>([-\+]\d+)\s?:\s+(\".+\")<.*$", sLine, re.UNICODE)
                if (pMatch):
                    #BugUtil.debug(u"MATCH: (%s) (%s)" %(pMatch.group(1), pMatch.group(2)))
                    iValue = int(pMatch.group(1))
                    sString = unicode(pMatch.group(2))
                    self.iAttitudeSum += iValue
                    if sString in MODIFIER_STRING_TO_KEY:
                        self.iAttitudeModifiers[MODIFIER_STRING_TO_KEY[sString]] = iValue
                        #BugUtil.debug(u"Attitude::init() - Added to dict: %s" % (sString))
                    else:
                        BugUtil.debug(u"Attitude::init() - Attitude string contains unknown modifier text: %s" % (sString))
        #BugUtil.debug(u"Attitude::init() complete.")
        BugUtil.debug(u"%s"  % (self))

    def __str__ (self):
        """ String representation of class instance. """
        return (u"Attitude { 'iThisPlayer': %d, 'iTargetPlayer': %d, 'iAttitudeSum': %d, 'eAttitudeType': %d, 'bHasActiveMetBoth': %s,\n 'iAttitudeModifiers': %s,\n 'sAttitudeString': %s }" 
                % (self.iThisPlayer, self.iTargetPlayer, self.iAttitudeSum, self.eAttitudeType,
                   self.bHasActiveMetBoth, str(self.iAttitudeModifiers), self.sAttitudeString))

    def hasModifier (self, sKey):
        """ Does the attitude contain given modifier? """
        if self.bHasActiveMetBoth:
            return (sKey in self.iAttitudeModifiers)
        return False

    def getModifier (self, sKey):
        """ Returns integer value of given attitude modifer. """
        if self.bHasActiveMetBoth:
            if sKey in self.iAttitudeModifiers:
                return self.iAttitudeModifiers[sKey]
        return 0

    def hasAttitude (self):
        """ Does this player have any attitude toward the target? """
        if self.bHasActiveMetBoth:
            return (not self.eAttitudeType == AttitudeTypes.NO_ATTITUDE)
        return False

    def getCount (self):
        """ Returns total (visible) count of attitude modifiers. """
        if self.bHasActiveMetBoth:
            return self.iAttitudeSum
        return 0

    def getCategory (self):
        """ Returns attitude category as an AttitudeTypes enum. """
        if self.bHasActiveMetBoth:
            return self.eAttitudeType
        return AttitudeTypes.NO_ATTITUDE

    def getString (self):
        """ Returns full diplomacy text string. """
        if self.bHasActiveMetBoth:
            return self.sAttitudeString
        return ""

    def getIcon (self):
        """ Returns smilie icon string based on attitude type. """
        if self.bHasActiveMetBoth:
            eCategory = self.getCategory()
            if eCategory != AttitudeTypes.NO_ATTITUDE:
                return ATTITUDE_ICONS[int(eCategory)]
        return ""

    def getColor (self):
        """ Returns the color of the attitude this player has toward target. """
        if self.bHasActiveMetBoth:
            eCategory = self.getCategory()
            if eCategory != AttitudeTypes.NO_ATTITUDE:
                return ATTITUDE_COLORS[int(eCategory)]
        return -1
    
    def getText (self, bNumber, bSmily, bWorstEnemy, bWarPeace):
        """ Returns a string describing the attitude this player has toward target. """
        if self.bHasActiveMetBoth:
            nAttitude = self.getCount()

            if bNumber:
                szText = str (nAttitude)
                if nAttitude > 0:
                    szText = "+" + szText
                if bSmily:
                    szText = "[" + szText + "] "
                else:
                    szText = "<font=3>   " + szText + "</font> "
            else:
                szText = ""

            iColor = self.getColor()
            szText = BugUtil.colorText(szText, iColor)
            if bSmily:
                szText = self.getIcon() + " " + szText

            pThisPlayer = gc.getPlayer(self.iThisPlayer)
            pTargetPlayer = gc.getPlayer(self.iTargetPlayer)
            if bWorstEnemy and isWorstEnemy(pThisPlayer, pTargetPlayer):
                szText +=  u"%c" %(CyGame().getSymbolID(FontSymbols.ANGRY_POP_CHAR))

            if bWarPeace:
                iThisTeam = pThisPlayer.getTeam()
                pThisTeam = gc.getTeam(iThisTeam)
                iTargetTeam = pTargetPlayer.getTeam()
                pTargetTeam = gc.getTeam(iTargetTeam)
                if pThisTeam.isAtWar(iTargetTeam):
                    szText += u"%c" % (gc.getCommerceInfo(CommerceTypes.COMMERCE_GOLD).getChar() + 25)
                elif gc.getGame().getActiveTeam() in (iThisTeam, iTargetTeam):
                    bPeace = False
                    if pThisTeam.isForcePeace(iTargetTeam):
                        bPeace = True
                    elif pTargetTeam.isAVassal():
                        for iOwnerTeam in range(gc.getMAX_TEAMS()):
                            if pTargetTeam.isVassal(iOwnerTeam) and pThisTeam.isForcePeace(iOwnerTeam):
                                bPeace = True
                                break
                    if bPeace:
                        szText += u"%c" % (gc.getCommerceInfo(CommerceTypes.COMMERCE_GOLD).getChar() + 26)
        
            return szText
        return ""
    

## Worst Enemy
##
## Each non-human team has a worst enemy team.
## CyPlayer.getWorstEnemyName() returns the names of everyone on their hated team separated by slashes (/).

def isWorstEnemy(playerOrID, enemyOrID):
    """
    Returns True if <enemy> is one of the worst enemies of <player>'s team.
    """
    player, team = PlayerUtil.getPlayerAndTeam(playerOrID)
    enemy, enemyTeam = PlayerUtil.getPlayerAndTeam(enemyOrID)
    return not team.isHuman() and team.getID() != enemyTeam.getID() and getWorstEnemyTeam(player) == enemyTeam.getID()

def getWorstEnemies(playerOrID):
    """
    Returns a list containing the player IDs that are worst enemies of <player>'s team.
    """
    eTeam = getWorstEnemyTeam(playerOrID)
    enemies = []
    if eTeam != -1:
        for player in PlayerUtil.teamPlayers(eTeam, alive=True, barbarian=False):
            enemies.append(player.getID())
    return enemies

def getWorstEnemyTeam(playerOrID):
    """
    Returns the team ID that is the worst enemy of <player>'s team.
    
    If <player>'s team has no worst enemy, returns -1.
    """
    player, team = PlayerUtil.getPlayerAndTeam(playerOrID)
    if not team.isHuman():
        worstEnemyName = player.getWorstEnemyName()
        if worstEnemyName:
            for team in PlayerUtil.teams(alive=True, barbarian=False):
                if team.getName() == worstEnemyName:
                    return team.getID()
    return -1

def getWorstEnemyTeams():
    """
    Returns a dictionary of the team IDs that are each team's worst enemy.
    
    The key is team ID; the value is the worst enemy team ID.
    If a team has no worst enemy, -1 is stored as its value.
    Ignores dead, human, barbarian, and minor teams.
    
    Loops over players because CyTeam does not have getWorstEnemyName().
    """
    namesToID = {}
    for team in PlayerUtil.teams(alive=True, barbarian=False, minor=False):
        namesToID[team.getName()] = team.getID()
    enemies = {}
    for team in PlayerUtil.teams(alive=True, human=False, barbarian=False, minor=False):
        eTeam = team.getID()
        eLeader = team.getLeaderID()
        if eLeader != -1:
            player = PlayerUtil.getPlayer(eLeader)
            worstEnemyName = player.getWorstEnemyName()
            if worstEnemyName:
                try:
                    enemies[eTeam] = namesToID[worstEnemyName]
                except KeyError:
                    BugUtil.debug("Cannot find team \"%s\"", worstEnemyName)
                    enemies[eTeam] = -1
            else:
                enemies[eTeam] = -1
    return enemies
