from core.db import Spells

class SpellStatus:
	none		= 0
	casting		= 1
	fire		= 2
	cooling 	= 3
	done		= 4
	cancelled	= 5

class CasterCheckStatus:
	clear		= 0 ## Ready to cast
	busy		= 1 ## Already casting
	lowEnergy	= 2 ## Not enough energy
	dead		= 3 ## Caster is dead
	cooling		= 4 ## Spell is cooling

class TargetCheckStatus:
	clear		= 0
	notFacing	= 1
	notInRange	= 2
	dead		= 3
	maxHealth	= 4
	selfAttack	= 5
	casterDead	= 6

class BasicCast:
	def __init__(self, caster, spell, targetSpawnId):
		self._caster = caster
		self._spell = spell
		self._targetSpawnId = targetSpawnId
		self._targets = []

		self._targetCheckTask = None
		self._casterCheckTask = None
		self._castingTask = None

		self.startCast()

	@property
	def caster(self): return self._caster
	@property
	def spell(self): return self._spell
	@property
	def targetSpawnId(self): return self._targetSpawnId
	@property
	def target(self):
		if self._targets: return self._targets[0]
	@property
	def targets(self): return self._targets

	@targets.setter
	def targets(self, targets):
		"""
		@type target: core.character.Character
		"""
		self._targets = targets

	def casterCheck(self):
		""" Checks the following:
		- If the caster isn't already casting.
		- If the caster has enough energy.
		- If the caster is dead.
		- If the spell isn't in cooldown.

		@rtype: int
		@return: See CasterCheckStatus
		"""
		if self.caster.characterData.spells.isCasting:
			if self.caster == base.world.playerController.character:
				base.uiManager.actionMessenger.print("Already busy!", 2)
			return CasterCheckStatus.busy

		if self.spell.isCooling():
			if self.caster == base.world.playerController.character:
				base.uiManager.actionMessenger.print("Spell is cooling..", 2)
			return CasterCheckStatus.cooling

		if self.caster.characterData.stats.energy.value < self.spell.data.energyCost:
			if self.caster == base.world.playerController.character:
				base.uiManager.actionMessenger.print("Not enough energy", 2)
			return CasterCheckStatus.lowEnergy

		if self.caster.isDead(): return CasterCheckStatus.dead

		return CasterCheckStatus.clear

	def targetCheck(self, target):
		"""
		- target is not dead
		- caster is not dead
		- caster is facing target
		- caster is in range of target
		- if impactPoints are positive
		- - check if target doesn't have max health
		- if impactPoints are negative
		- - check if target is not caster (cannot attack self)
		"""

		if target.isDead(): return TargetCheckStatus.dead
		if self.caster.isDead(): return TargetCheckStatus.casterDead

		if self.spell.data.impactPoints < 0: # Attacking
			if (target == self.caster):
				if self.caster == base.world.playerController.character:
					base.uiManager.actionMessenger.print("Cannot attack self.", 2)
				return TargetCheckStatus.selfAttack
		elif self.spell.data.impactPoints > 0: # healing
			if (target.characterData.stats.health.value ==
					target.characterData.stats.health.max
				):
				if self.caster == base.world.playerController.character:
					base.uiManager.actionMessenger.print("Max health reached!", 2)
				return TargetCheckStatus.maxHealth

		if (self.spell.data.facingAngle and not
				self.caster.canSee(
					target,
					self.spell.data.facingAngle
				)
			):
			if self.caster == base.world.playerController.character:
				base.uiManager.actionMessenger.print("You are not facing the NPC.", 2)
			return TargetCheckStatus.notFacing

		if self.spell.data.rangeEnd and not self.inRange(target):
			if self.caster == base.world.playerController.character:
				base.uiManager.actionMessenger.print("Not in range.", 2)
			return TargetCheckStatus.notInRange

		return TargetCheckStatus.clear

	def targetIntervalCheck(self, task):
		if self.targetCheck(self.target) != TargetCheckStatus.clear:
			self.cancelCast()
			return task.done
		return task.again

	def inRange(self, target):
		diffVec = self.caster.getGlobalPos() - target.getGlobalPos()
		if (diffVec.getXy().length() >= self.spell.data.rangeStart and
			diffVec.getXy().length() <= self.spell.data.rangeEnd):
			return True
		return False

	def stopAllTasks(self):
		if self._targetCheckTask:
			taskMgr.remove(self._targetCheckTask)
			self._targetCheckTask = None

		if self._casterCheckTask:
			taskMgr.remove(self._casterCheckTask)
			self._casterCheckTask = None

		if self._castingTask:
			taskMgr.remove(self._castingTask)
			self._castingTask = None

	def cancelCast(self, spawnId=None):
		self.caster.hasDied.disconnect(self.cancelCast)
		if self.target and self.spell.data.targetType == Spells.targetTypes.selected:
			self.target.hasDied.disconnect(self.cancelCast)
		self.spell.setStatus(SpellStatus.cancelled)
		self.stopAllTasks()
		self.caster.characterData.spells.isCasting = False
		self.caster.actorNP.loop("idle")
		self.finalizeCast()

	def getTarget(self):
		target = base.world.player
		if self.targetSpawnId > 0: # None or player selected
			target = base.world.npcsManager.getSpawn(self.targetSpawnId)
		return target

	def getTargets(self):
		"""
		Get multiple targets in range of the spell, excluding the caster.
		"""
		targets = []
		if not self.caster.isDead():
			for character in base.world.npcsManager.spawns + [base.world.player]:
				if (character != self.caster and
					self.caster.characterData.fraction in character.characterData.enemies and
					not character.isDead() and
					self.inRange(character)
					):
					targets.append(character)
		return targets

	def startCast(self):
		if self.casterCheck() != CasterCheckStatus.clear: return

		if self.spell.data.targetType in [Spells.targetTypes.selected, Spells.targetTypes.self]:
			if self.spell.data.targetType == Spells.targetTypes.selected:
				self.targets = [self.getTarget()]
			else:
				self.targets = [self.caster]

			if self.targetCheck(self.target) != TargetCheckStatus.clear:
				return

			if self.spell.data.targetType == Spells.targetTypes.selected:
				self.target.hasDied.connect(self.cancelCast)

			self._targetCheckTask = taskMgr.doMethodLater(
				.1,
				self.targetIntervalCheck,
				'{}_castTargetCheck'.format(self.caster.characterData.spawnData.id),
			)

		self.caster.hasDied.connect(self.cancelCast)
		self.doCastingTime()

	def doCastingTime(self):
		self.caster.characterData.spells.isCasting = True # TODO spells should know this by watching it's spell's

		if self.spell.data.castTime:
			self._castingTask = taskMgr.doMethodLater(
				self.spell.data.castTime,
				self.applySpell,
				'{}_cast'.format(self.caster.characterData.spawnData.id),
				extraArgs=[],
			)
			self.spell.setStatus(SpellStatus.casting)

			## Start casting animation.
			self.caster.actorNP.loop("casting")
		else: self.applySpell()

	def applySpell(self):
		""" Apply spell (after casting-time)
		"""
		self.caster.hasDied.disconnect(self.cancelCast)
		self.stopAllTasks()

		## Reset animation.
		self.caster.actorNP.loop("idle")

		if self.caster.isDead():
			self.cancelCast()
			return

		## Subtract used energy from caster.
		if self.spell.data.energyCost:
			self.caster.characterData.stats.energy.value -= self.spell.data.energyCost

		if self.spell.data.targetType == Spells.targetTypes.range:
			# Get all targets in range
			self.targets = self.getTargets()
		elif self.target and self.spell.data.targetType == Spells.targetTypes.selected:
			self.target.hasDied.disconnect(self.cancelCast)

		if self.spell.data.impactPoints:
			for target in self.targets:
				target.characterData.stats.health.value += self.spell.data.impactPoints
				if self.spell.data.impactPoints < 0 and not target.isDead():
					target.underAttackBy(int(self.caster.characterData.spawnData.id))
				#else: # healing

		self.spell.setStatus(SpellStatus.fire)
		self.caster.characterData.spells.isCasting = False # TODO spells should know this by watching it's spell's

		self.targets.clear()

		self.doCooldown()

	def doCooldown(self):
		""" Wait for cooldown time before calling self.finalizeCast()
		"""

		if self.spell.data.coolDown:
			self.spell.setStatus(SpellStatus.cooling)
			taskMgr.doMethodLater(
				self.spell.data.coolDown,
				self.finalizeCast,
				'{}_finalizeCast'.format(self.caster.characterData.spawnData.id),
				extraArgs=[]
			)
		else: self.finalizeCast()

	def finalizeCast(self):
		""" Finalize the cast.
		"""
		self.spell.setStatus(SpellStatus.done)

		## Note: make sure made connections are disconnected, else
		## they leave a reference and this instance won't get destructed.
		# ..
