## TechPrefs
##
## Builds ordered lists of techs as preferred by the various Great People.
##
## Copyright (c) 2007-2008 The BUG Mod.
##
## Author: EmperorFool

from CvPythonExtensions import *

# BUG - Mac Support - start
import BugUtil
BugUtil.fixSets(globals())
# BUG - Mac Support - end

gc = CyGlobalContext()

# see gc.getNumFlavorTypes() and gc.getFlavorTypes()
# not available via gc.getInfoTypeForString(), thus the hard-coding here :(
NUM_FLAVORS = 8
FLAVORS = [ "Military", "Religion", "Production",
			"Gold", "Science", "Culture",
			"Growth", "Espionage" ]
(
	FLAVOR_MILITARY,
	FLAVOR_RELIGION,
	FLAVOR_PRODUCTION,
	FLAVOR_GOLD,
	FLAVOR_SCIENCE,
	FLAVOR_CULTURE,
	FLAVOR_GROWTH,
	FLAVOR_ESPIONAGE,
) = range(NUM_FLAVORS)

class TechPrefs:

	def __init__(self):
		self.NUM_TECHS = gc.getNumTechInfos()
		self.NUM_AND_PREREQS = gc.getDefineINT("NUM_AND_TECH_PREREQS")
		self.NUM_OR_PREREQS = gc.getDefineINT("NUM_OR_TECH_PREREQS")
		
		self.mTechs = {}
		self.lTechsByFlavor = []
		for iFlavor in range(NUM_FLAVORS):
			self.lTechsByFlavor.append([])
		
		# build a list of all techs and a list of techs for each flavor
		for iTech in range(self.NUM_TECHS):
			pTechInfo = gc.getTechInfo(iTech)
			pTech = self.getTech(iTech)
			for iFlavor in range(NUM_FLAVORS):
				iFlavorValue = pTechInfo.getFlavorValue(iFlavor)
				if (iFlavorValue > 0):
					pTech.setFlavorValue(iFlavor, iFlavorValue)
					self.lTechsByFlavor[iFlavor].append((-iFlavorValue, iTech, pTech))
					bHasFlavor = True
			
			# hook up prereq techs
			for i in range(self.NUM_AND_PREREQS):
				pPrereqTech = pTechInfo.getPrereqAndTechs(i)
				if (pPrereqTech != -1):
					pTech.addAndPrereq(self.getTech(pPrereqTech))
			for i in range(self.NUM_OR_PREREQS):
				pPrereqTech = pTechInfo.getPrereqOrTechs(i)
				if (pPrereqTech != -1):
					pTech.addOrPrereq(self.getTech(pPrereqTech))
		
		# sort each flavor's list of techs by decreasing preference: reverse flavor value, tech number
		# and create a copy that doesn't get trimmed as techs are researched
		self.lAllTechsByFlavor = {}
		for iFlavor in range(NUM_FLAVORS):
			lTechs = self.lTechsByFlavor[iFlavor]
##			print "%s has %d techs" % (FLAVORS[iFlavor], len(lTechs))
			lTechs.sort()
			self.lTechsByFlavor[iFlavor] = [ pTech for _, _, pTech in lTechs ]
			self.lAllTechsByFlavor[iFlavor] = tuple(self.lTechsByFlavor[iFlavor])
		
##		print "---- Techs with Flavor ----"
##		for pTech in self.mTechs.values():
##			print "%2d: %s" % (pTech.iTech, pTech.getName())
##		for iFlavor in range(NUM_FLAVORS):
##			print "---- %d Techs with Flavor %s ----" % (len(self.lTechsByFlavor[iFlavor]), FLAVORS[iFlavor])
##			for pTech in self.lTechsByFlavor[iFlavor]:
##				print "%2d-%2d: %s" % (pTech.getFlavorValue(iFlavor), pTech.iTech, pTech.getName())
		
##		pOptics = self.getTech(gc.getInfoTypeForString("TECH_OPTICS"))
##		print pOptics
##		pAstronomy = self.getTech(gc.getInfoTypeForString("TECH_ASTRONOMY"))
##		print pAstronomy
##		pPhysics = self.getTech(gc.getInfoTypeForString("TECH_PHYSICS"))
##		print pPhysics
##		pAstronomy.removeFromTree()
##		print pOptics
##		print pAstronomy
##		print pPhysics
##		
##		self.removeKnownTechs()
##		print self.getNextResearchableFlavorTech(FLAVOR_RELIGION)
##		print self.getNextResearchableFlavorTech(FLAVOR_SCIENCE)
##		print self.getNextResearchableFlavorTech(FLAVOR_GOLD)
##		techs = set()
##		techs.add(self.getTechStr("TECH_THE_WHEEL"))
##		techs.add(self.getTechStr("TECH_MYSTICISM"))
##		print self.getNextResearchableWithFlavorTech(FLAVOR_RELIGION, techs)
##		print self.getNextResearchableWithFlavorTech(FLAVOR_SCIENCE, techs)
##		print self.getNextResearchableWithFlavorTech(FLAVOR_GOLD, techs)
##		techs.add(self.getTechStr("TECH_IRON_WORKING"))
##		techs.add(self.getTechStr("TECH_IRON_POTTERY"))
##		techs.add(self.getTechStr("TECH_MEDITATION"))
##		print self.getNextResearchableWithFlavorTech(FLAVOR_RELIGION, techs)
##		print self.getNextResearchableWithFlavorTech(FLAVOR_SCIENCE, techs)
##		print self.getNextResearchableWithFlavorTech(FLAVOR_GOLD, techs)#


	def getTech(self, iTech):
		if iTech not in self.mTechs:
			self.mTechs[iTech] = Tech(iTech)
		return self.mTechs[iTech]

	def getTechStr(self, sTech):
		iTech = gc.getInfoTypeForString(sTech)
		if iTech in self.mTechs:
			return self.mTechs[iTech]
		return None

	def removeTech(self, iTech):
		"""Removes the given tech, usually because it has been researched."""
		if (iTech in self.mTechs):
			pTech = self.mTechs[iTech]
			del self.mTechs[iTech]
			for iFlavor in range(NUM_FLAVORS):
				if pTech in self.lTechsByFlavor[iFlavor]:
					self.lTechsByFlavor[iFlavor].remove(pTech)
			pTech.removeFromTree()

	def removeKnownTechs(self):
		"""Removes the techs known to the current team."""
		pTeam = gc.getTeam(gc.getActivePlayer().getTeam())
		for iTech in range(self.NUM_TECHS):
			if (pTeam.isHasTech(iTech)):
				self.removeTech(iTech)


	def getResearchableTechs(self):
		"""Returns a set of all techs that can be researched now."""
		sCan = set()
		for pTech in self.mTechs.values():
			if (pTech.canResearch()):
				sCan.add(pTech)
		return sCan

	def getResearchableWithTechs(self, sTechs):
		"""Returns a set of all techs that can be researched once the given techs have been researched."""
		sCan = set()
		for pTech in self.mTechs.values():
			if (pTech not in sTechs and pTech.canResearchWith(sTechs)):
				sCan.add(pTech)
		return sCan


	def getNextFlavorTech(self, iFlavor):
		"""Returns the next tech in the flavor's list or None."""
		if (len(self.lTechsByFlavor[iFlavor]) > 0):
			return self.lTechsByFlavor[iFlavor][0]
		else:
			return None

	def getNextResearchableFlavorTech(self, iFlavor):
		"""Returns the next tech in the flavor's list that is researchable now or None."""
		for pTech in self.lTechsByFlavor[iFlavor]:
			if (pTech.canResearch()):
				return pTech
		return None

	def getNextResearchableWithFlavorTech(self, iFlavor, sTechs):
		"""Returns the next tech in the flavor's list that is researchable once the given techs are researched or None."""
		for pTech in self.lTechsByFlavor[iFlavor]:
			if (pTech not in sTechs and pTech.canResearchWith(sTechs)):
				return pTech
		return None

	def getAllFlavorTechs(self, iFlavor):
		"""Returns a list of all techs in the flavor's list."""
		lTechs = []
		for pTech in self.lAllTechsByFlavor[iFlavor]:
			lTechs.append(pTech)
		return lTechs

	def getRemainingFlavorTechs(self, iFlavor):
		"""Returns a list of techs in the flavor's list that haven't been researched yet."""
		lTechs = []
		for pTech in self.lTechsByFlavor[iFlavor]:
			BugUtil.debug("checking %s", pTech.getName())
			if (pTech.getID() in self.mTechs):
				BugUtil.debug("bingo")
				lTechs.append(pTech)
		return lTechs

	def getFutureFlavorTechs(self, iFlavor, sTechs):
		"""Returns a list of techs in the flavor's list that haven't been researched yet and aren't in <sTechs>."""
		lTechs = []
		for pTech in self.lTechsByFlavor[iFlavor]:
			if (pTech.getID() in self.mTechs and pTech not in sTechs):
				lTechs.append(pTech)
		return lTechs


	def printFlavorTechs(self, iFlavor):
		"""Prints the techs in the flavor's list."""
		for pTech in self.lTechsByFlavor[iFlavor]:
			print pTech

	def printResearchableFlavorTechs(self, iFlavor):
		"""Prints the techs in the flavor's list."""
		for pTech in self.lTechsByFlavor[iFlavor]:
			if pTech.canResearch():
				print pTech

	def printResearchableWithFlavorTechs(self, iFlavor, sTechs):
		"""Prints the techs in the flavor's list."""
		for pTech in self.lTechsByFlavor[iFlavor]:
			if pTech.canResearchWith(sTechs):
				print pTech


class Tech:
	
	def __init__(self, iTech):
		self.iTech = iTech
		self.lFlavorValues = [0] * NUM_FLAVORS
		self.lFlavorPref = [0] * NUM_FLAVORS
		self.sAndPrereqs = set()
		self.sOrPrereqs = set()
		self.iNumAndPrereqs = 0
		self.iNumOrPrereqs = 0
		self.sLeadsTo = set()

	def getID(self):
		return self.iTech

	def getInfo(self):
		return gc.getTechInfo(self.iTech)

	def getName(self):
		return self.getInfo().getDescription()
	
	def __hash__(self):
		return hash(self.iTech)
	
	def __eq__(self, other):
		return self.iTech == other.iTech
	
	def __cmp__(self, other):
		return self.iTech - other.iTech


	def setFlavorValue(self, iFlavor, iValue):
		self.lFlavorValues[iFlavor] = iValue

	def getFlavorValue(self, iFlavor):
		return self.lFlavorValues[iFlavor]

	def setFlavorPref(self, iFlavor, iPref):
		self.lFlavorPref[iFlavor] = iPref

	def getFlavorPref(self, iFlavor):
		return self.lFlavorPref[iFlavor]


	def addAndPrereq(self, pTech):
		if pTech not in self.sAndPrereqs:
			self.iNumAndPrereqs += 1
			self.sAndPrereqs.add(pTech)
			pTech.addLeadsTo(self)

	def addOrPrereq(self, pTech):
		if pTech not in self.sOrPrereqs:
			self.iNumOrPrereqs += 1
			self.sOrPrereqs.add(pTech)
			pTech.addLeadsTo(self)

	def removePrereq(self, pTech):
		self.sAndPrereqs.discard(pTech)
		self.sOrPrereqs.discard(pTech)


	def getNumTechsNeeded(self):
		"""Returns the minimum number of techs that must be researched to be able to research this tech."""
		return self.getNumAndTechsNeeded() + self.getNumOrTechsNeeded()

	def getNumAndTechsNeeded(self):
		return len(self.sAndPrereqs)

	def getNumOrTechsNeeded(self):
		if (len(self.sOrPrereqs) == 0 or len(self.sOrPrereqs) < self.iNumOrPrereqs):
			return 0
		return 1

	def getTechsNeeded(self):
		"""
		Returns two sets of techs that are needed to make this tech researchable.
		
		The first set are all missing And prereqs.
		The second set is all Or prereqs or an empty set if at least one has already been researched.
		"""
		andSet = self.sAndPrereqs.copy()
		if (len(self.sOrPrereqs) == 0 or len(self.sOrPrereqs) < self.iNumOrPrereqs):
			orSet = set()
		else:
			orSet = self.sOrPrereqs.copy()
		return andSet, orSet

	def canResearch(self):
		"""Returns True if this tech has met all And prereqs and at least one Or prereq."""
		return self.getNumTechsNeeded() == 0

	def canResearchWith(self, sTechs):
		"""Returns True if this tech can be researched once the given tech(s) have been researched."""
		if (len(sTechs) == 0):
			return self.canResearch()
		sAnds = self.sAndPrereqs.difference(sTechs)
		sOrs = self.sOrPrereqs.difference(sTechs)
		return (len(sOrs) == 0 or len(sOrs) < self.iNumOrPrereqs) and len(sAnds) == 0


	def addLeadsTo(self, pTech):
		self.sLeadsTo.add(pTech)

	def removeLeadsTo(self, pTech):
		self.sLeadsTo.discard(pTech)

	def removeFromTree(self):
		"""Removes this tech from the prereq lists of the techs it leads to and the leads to lists of its prereqs."""
		for pTech in self.sAndPrereqs:
			pTech.removeLeadsTo(self)
		for pTech in self.sOrPrereqs:
			pTech.removeLeadsTo(self)
		for pTech in self.sLeadsTo:
			pTech.removePrereq(self)


	def __str__(self):
		str = self.getName()
		bFirst = True
		if (len(self.sAndPrereqs) > 0 or len(self.sOrPrereqs) > 0):
			str += " requires "
		if (len(self.sAndPrereqs) > 0):
			for pTech in self.sAndPrereqs:
				if bFirst:
					bFirst = False
				else:
					str += " and "
				str += pTech.getName()
		if (len(self.sOrPrereqs) > 0):
			if not bFirst:
				str += " and "
				bFirst = True
			for pTech in self.sOrPrereqs:
				if bFirst:
					bFirst = False
				else:
					str += " or "
				str += pTech.getName()
		if (len(self.sLeadsTo) > 0):
			str += ", leads to "
			bFirst = True
			for pTech in self.sLeadsTo:
				if bFirst:
					bFirst = False
				else:
					str += ", "
				str += pTech.getName()
		return str
