########################################################################
#	Hello Worlds - Libre 3D RPG game.
#	Copyright (C) 2020  CYBERDEViL
#
#	This file is part of Hello Worlds.
#
#	Hello Worlds is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	Hello Worlds is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################


from panda3d.core import Vec3
from panda3d.bullet import BulletRigidBodyNode
from panda3d.ai import AIWorld, AICharacter

from core.db import NPCs
from core.worldMouse import NPCMouse
from core.models import SelectedNpcModel
from core.character import Character, CharacterModel

import random
class AI:
	def __init__(self, npc, aiWorld):
		"""
		@param npc:
		@type npc: core.npcsManager.NPC

		@param aiWorld:
		@type aiWorld: panda3d.ai.AIWorld
		"""
		self._npc = npc
		self._aiWorld = aiWorld

		self._activeSteering = None
		self._selectedSpell = None
		self._selectedAttacker = None
		self._beenUnderAttack = False
		self._onWayHome = False

		self.AIchar = AICharacter(
			"spawnId_{0}".format(npc.characterData.spawnData.id),
			npc.characterNP, 100, 6, 10)
		self.AIbehaviors = self.AIchar.getAiBehaviors()
		self._aiWorld.addAiChar(self.AIchar)

		self.AIchar.setMaxForce(6)

	def destroy(self):
		self._aiWorld.removeAiChar("spawnId_{0}".format(self._npc.characterData.spawnData.id))
		#del self.AIchar
		self.AIchar = None

		self._activeSteering = None
		self._selectedSpell = None
		self._selectedAttacker = None
		self._beenUnderAttack = False
		self._onWayHome = False

		self._npc = None
		self._aiWorld = None

	def lookAt(self, attacker):
		"""
		@param attacker:
		@type attacker: core.character.Character
		"""
		self._npc.characterNP.lookAt(attacker.characterNP)

	def getFirstAttacker(self):
		""" Note: do check if it has any attackers before calling this.

		@rtype: core.character.Character
		@return: 
		"""
		return self.getAttacker(self._npc._underAttackBy[0])

	def getRandomAttacker(self):
		""" Note: do check if it has any attackers before calling this.

		@rtype: core.character.Character
		@return: 
		"""
		return self.getAttacker(
			random.choice(self._npc._underAttackBy)
		)

	def getAttacker(self, spawnId):
		""" Get attacker it's core.character.Character object
		@param spawnId: Attacker it's spawnId
		@type spawnId: int

		@rtype: core.character.Character
		@return: 
		"""
		if spawnId > 0: return base.world.npcsManager.getSpawn(spawnId)
		return base.world.player

	def getRandomSpell(self):
		""" Returns a random spell to cast.

		@rtype: core.models.SpellModel
		@return: 
		"""
		return random.choice(list(self._npc.characterData.spells))

	def distanceBetween(self, pos, pos2):
		# TODO make this a util function or something ..
		# This doesn't consider height or Z axis.
		diffVec = pos - pos2
		diffVecXY = diffVec.getXy()
		return diffVecXY.length()

	def removeSteering(self):
		if self._activeSteering:
			self.AIbehaviors.removeAi('all')
			self._activeSteering = None

	def update(self):
		if self._npc._underAttackBy:
			if not list(self._npc.characterData.spells): return
			if self._onWayHome: self.AIbehaviors.removeAi('pathfollow')

			if not self._selectedAttacker:
				self._selectedAttacker = self.getFirstAttacker()
				if not self._selectedAttacker: return

			if self._selectedAttacker.isDead():
				self._selectedAttacker = None
				return

			if (self._npc.characterData.spells.isCasting or
				self._npc.isDead()): return

			self._beenUnderAttack = True

			if not self._selectedSpell:
				self._selectedSpell = self.getRandomSpell()

			if self._selectedSpell.isCooling():
				return

			if self._npc.characterData.stats.energy.value < self._selectedSpell.data.energyCost:
				return

			self.lookAt(self._selectedAttacker)

			if self._selectedSpell.data.rangeEnd:
				distance = self.distanceBetween(
					self._npc.getGlobalPos(),
					self._selectedAttacker.getGlobalPos()
				)

				## Get closer
				if distance >= self._selectedSpell.data.rangeEnd - 1:
					if self._activeSteering != 'persue':
						self.removeSteering()
						self.AIbehaviors.pursue(self._selectedAttacker.characterNP, 10)
						self._activeSteering = 'persue'
					return

				## Take more distance
				if distance <= self._selectedSpell.data.rangeStart + 0.5:
					if self._activeSteering != 'flee':
						self.removeSteering()
						self.AIbehaviors.flee(self._selectedAttacker.characterNP, 10)
						self._activeSteering = 'flee'
					return

				self.removeSteering()

			self._npc.startCast(self._selectedSpell.data.id, targetSpawnId=int(self._selectedAttacker.characterData.spawnData.id))
			self._selectedSpell = None
			self._selectedAttacker = None

		elif self._beenUnderAttack:
			# Return to spawn point
			self.AIbehaviors.pathFollow(1)
			self.AIbehaviors.addToPath(Vec3(*self._npc.characterData.spawnData.pos))
			self.AIbehaviors.startFollow()
			self._beenUnderAttack = False
			self._onWayHome = True

		elif self._onWayHome:
			# Check if NPC is at spawn point
			distanceFromHome = self.distanceBetween(
				self._npc.getGlobalPos(),
				Vec3(*self._npc.characterData.spawnData.pos)
			)
			if distanceFromHome < 0.5:
				self._onWayHome = False
				self.AIbehaviors.removeAi('pathfollow')

class NPC(Character):
	def __init__(self, world, worldNP, spawnData, aiWorld):
		"""
		@param world:
		@type world: BulletWorld

		@param worldNP:
		@type worldNP: NodePath

		@param spawnData:
		@type spawnData: core.db.NPCSpawnData
		"""
		self._aiWorld = aiWorld
		model = CharacterModel(NPCs[spawnData.characterId] , spawnData)
		Character.__init__(self, world, worldNP, model)

	def __hash__(self): return self.characterData.spawnData.id

	def __lt__(self, other):
		return not other < self.characterData.spawnData.id

	def setup(self):
		Character.setup(self)
		self.ai = AI(self, self._aiWorld)

	def destroy(self):
		## First check if characterNP is not None (already destroyed)
		## this can happen when a npc goes out of range and gets
		## killed at the same time. TODO fix this when implementing
		## proper die/death system.
		if self.characterNP:
			Character.destroy(self)
			self.ai.destroy()
			self.ai = None

	def die(self):
		Character.die(self)
		if self.characterData.spawnData.respawnTime:
			# set respawn time to 0 to not respawn.
			taskMgr.doMethodLater(self.characterData.spawnData.respawnTime, self.respawn, 'respawn')

	def addSelectPlane(self):
		self.groundPlane = loader.loadModel('assets/other/select_plane.egg') #TODO use AssetsPath
		self.groundPlane.reparentTo(self.actorNP)
		self.groundPlane.setPos(0, 0, 0)
		self.groundPlane.setScale(2)

	def removeSelectPlane(self):
		self.groundPlane.removeNode()
		self.groundPlane = None

class NPCsManager:
	def __init__(self, world, worldNP):
		"""
		@param world:
		@type world: BulletWorld

		@param worldNP:
		@type worldNP: NodePath
		"""
		self._world = world
		self._worldNP = worldNP
		self._player = None

		self._spawnData = None # List with db.SpawnData's from assets/maps/{map_name}/spawns.json

		self._np = self._worldNP.attachNewNode(BulletRigidBodyNode('NPCs'))
		self._AIworld = AIWorld(self._worldNP)
		self._spawns = {} # spawnId : spawned npc

		self._selectModel = SelectedNpcModel()
		self.mouseHandler = NPCMouse(self._np, self._selectModel)

		taskMgr.add(self.update, 'updateNPCs')

	@property
	def selectedNpcModel(self):
		""" 
		@rtype: core.models.SelectedNpcModel
		@return: Return the selected npc model.
		"""
		return self._selectModel

	@property
	def node(self): return self._np

	@property
	def spawns(self): return list(self._spawns.values())

	def clear(self):
		# Remove all npcs from the world.
		# TODO Remove objects
		# ..
		self._spawnData = None

		for spawnId in self._spawns:
			# TODO make sure everything is proper deleted. (same happends in self.update)
			self._spawns[spawnId].destroy()
		self._spawns.clear()

	def setPlayer(self, player):
		self._player = player

	def setSpawnData(self, spawns):
		self.clear()
		self._spawnData = spawns

		for spawn in self._spawnData: self.spawn(spawn)

	def spawn(self, spawnData): # spawn
		self._spawns.update({spawnData.id : NPC(self._world, self._np, spawnData, self._AIworld)})

	def getSpawn(self, spawnId):
		""" Returns NPC

		@param spawnId: 
		@type spawnId: int
		"""
		return self._spawns.get(str(spawnId))

	def distanceBetween(self, pos, pos2):
		# This doesn't consider height or Z axis.
		diffVec = pos - pos2
		diffVecXY = diffVec.getXy()
		return diffVecXY.length()

	def update(self, task):
		if self._player:
			if base.world.player.isMoving:
				# Unspawn out of range, spawn in range.
				for spawn in self._spawnData:
					distance = self.distanceBetween(
						Vec3(*spawn.pos),
						base.world.player.characterNP.getPos()
					)

					if distance > 100: # Out of range
						if spawn.id in self._spawns:
							# TODO make sure everything is proper deleted.
							self._spawns.pop(spawn.id).destroy()

					elif distance < 80: # In range
						if spawn.id not in self._spawns:
							self.spawn(spawn)

			# Update AI's
			for spawnId, npc in self._spawns.items():
				if not npc.isDead(): npc.ai.update()

			self._AIworld.update()

		return task.cont
