########################################################################
#	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/>.
#
########################################################################

# Panda3d
from panda3d.core import Vec3
from panda3d.core import PandaNode, NodePath, TextNode
from panda3d.core import Camera, OrthographicLens, PGTop

from direct.showbase import DirectObject

# Local
from core.db import Maps, AssetsPath
from core.character import Character
import os

class MiniMap(DirectObject.DirectObject):
	def __init__(self, mapId):
		self._mapId = mapId

		# Create 2d display stuff
		self.dr = base.win.makeDisplayRegion()
		self.dr.setSort(20)
		self.myCamera2d = NodePath(Camera('myCam2d'))
		lens = OrthographicLens()
		lens.setFilmSize(2, 2)
		lens.setNearFar(-1000, 1000)
		self.myCamera2d.node().setLens(lens)

		self.myRender2d = NodePath('myRender2d')
		self.myRender2d.setDepthTest(False)
		self.myRender2d.setDepthWrite(False)
		self.myCamera2d.reparentTo(self.myRender2d)
		self.dr.setCamera(self.myCamera2d)

		self._aspectRatio = base.getAspectRatio()
		self.myAspect2d = self.myRender2d.attachNewNode(PGTop('myAspect2d'))
		self.myAspect2d.setScale(1.0 / self._aspectRatio, 1.0, 1.0)

		# we now get buffer thats going to hold the texture of our new scene
		#self.mapWidth = Maps[mapId].width
		#self.mapHeight = Maps[mapId].height
		self.mapWidth = 100
		self.mapHeight = 100 # TODO

		self.mapWidthRatio = self.mapWidth / self.mapHeight
		self.mapHeightRatio = self.mapHeight / self.mapWidth
		if self.mapWidthRatio > 1: self.mapHeightRatio = 1
		else: self.mapWidthRatio = 1

		self.textureBuffer = base.win.makeTextureBuffer("miniMapBuffer", 256, 256)

		# now we have to setup a new scene graph to make this scene
		self.miniMapRender = NodePath("miniMapRender")

		# this takes care of setting up ther camera properly
		self.altCam = base.makeCamera(self.textureBuffer)
		self.altCam.reparentTo(self.miniMapRender)
		self.altCam.setPos(0, -24, 0)

		self._2dMap = loader.loadModel(
			Maps[mapId].orthoFilePath
		)
		self._2dMap.setScale(self.mapWidthRatio, 1, self.mapHeightRatio)
		self._2dMap.reparentTo(self.miniMapRender)

		self.altCam.lookAt(self._2dMap)

		self._model = loader.loadModel(os.path.join(AssetsPath.widgets, "miniMap.egg"))
		self._model.reparentTo(self.myAspect2d)
		self._model.setPos(-1.50, 0, 0.7)

		self.resize()

		projectionObj = self._model.find("**/miniMap")
		tex = self.textureBuffer.getTexture()
		projectionObj.setTexture(tex, 1)
		projectionObj.reparentTo(self._model)

		# Player head
		self._playerHead = self._model.find("**/miniMapHead")
		self._playerHead.reparentTo(self._model)

		# Zoom buttons
		self._zoomInButton = self._model.find("**/miniMapZoomIn")
		self._zoomOutButton = self._model.find("**/miniMapZoomOut")
		self._zoomInButton.reparentTo(self._model)
		self._zoomOutButton.reparentTo(self._model)

		self.accept("m", self.zoomIn)
		self.accept("n", self.zoomOut)
		self.accept("b", self.resize)

		self.accept("v", base.bufferViewer.toggleEnable)
		base.bufferViewer.setPosition("llcorner")
		base.bufferViewer.setCardSize(1.0, 0.0)

		base.mouseHandler.connect('mouse1', self._onLeftMouseClick, priority=1)
		#base.mouseHandler.connect('wheel_up', self._onLeftMouseClick, priority=1)
		#base.mouseHandler.connect('wheel_down', self._onLeftMouseClick, priority=1)
		self.accept('WINDOW_RESIZED', self.resize)

	def __del__(self):
		print("Destroyed MiniMap")

	"""Events
	"""
	def resize(self):
		""" Keep the minimap always the same size.
		"""

		# Size of the miniMap-frame as reference
		blenderUnitWidth = 0.5
		blenderUnitHeight = 0.5

		screenWidth = base.win.getProperties().getXSize()
		screenHeight = base.win.getProperties().getYSize()
		ratio = base.getAspectRatio()

		zeroXPx = screenWidth / 2
		zeroZPx = screenHeight / 2

		oneUnitPerPixelX = ratio / zeroXPx
		oneUnitPerPixelZ = 1 / zeroZPx

		wantPixelsWidth = 175
		wantPixelsHeight = 175

		currentWidthPx = blenderUnitWidth / oneUnitPerPixelX
		currentHeightPx = blenderUnitHeight / oneUnitPerPixelZ

		widthScale = wantPixelsWidth / currentWidthPx
		heightScale = wantPixelsHeight / currentHeightPx

		self._model.setScale(widthScale, 1, heightScale)

		# Now re-position (TODO re-evaluate this method)
		wantPixelsTop = 25
		wantPixelsLeft = 15

		topPx = 1 - ((wantPixelsTop + (wantPixelsHeight / 2)) * oneUnitPerPixelZ)
		leftPx = ratio - ((wantPixelsLeft + (currentWidthPx / 2)) * oneUnitPerPixelX) # move to right

		self._model.setPos(leftPx, 0, topPx)

	def destroy(self):
		self.ignoreAll()
		self.removeAllTasks()

		loader.unloadModel(self._model)
		self._model.removeNode()
		del self._model
		self._model = None

		loader.unloadModel(self._2dMap)
		del self._2dMap
		self._2dMap = None

		self.altCam.removeNode()
		self.altCam = None

		self.myCamera2d.removeNode()
		self.myCamera2d = None
		self.myRender2d.removeNode()
		self.myRender2d = None
		self.myAspect2d.removeNode()
		self.myAspect2d = None
		self.miniMapRender.removeNode()
		self.miniMapRender = None

		base.graphicsEngine.removeWindow(self.textureBuffer)
		self.textureBuffer = None

		self.dr = None

	def setPos(self, pos):
		self._onPlayerPositionUpdateEvent(pos)

	def setOrientation(self, omega):
		self.altCam.setR(omega)

	def modOrientation(self, omega):
		self._onPlayerRotateEvent(omega)

	def _onPlayerRotateEvent(self, omega):
		self.altCam.setR(self.altCam, -omega)

	def _onPlayerPositionUpdateEvent(self, characterPos):
		self.altCam.setX(((characterPos[0] / self.mapWidth) * 16) * self.mapWidthRatio) # 2d map == 16x16 blender units
		self.altCam.setZ(((characterPos[1] / self.mapHeight) * 16) * self.mapHeightRatio)

	def _onLeftMouseClick(self, args=[]):
		mpos = base.mouseWatcherNode.getMouse()
		mousePos = (mpos.getX() * self._aspectRatio, mpos.getY())

		zoomInButtonPos = (
			((self._model.getX() + self._zoomInButton.getX())),
			self._model.getZ() + self._zoomInButton.getZ()
		);

		zoomOutButtonPos = (
			((self._model.getX() + self._zoomOutButton.getX())),
			self._model.getZ() + self._zoomOutButton.getZ()
		);

		margin = 0.03

		if (mousePos[0] + margin >=  zoomInButtonPos[0]
		and mousePos[0] - margin <=  zoomInButtonPos[0]
		and mousePos[1] + margin >=  zoomInButtonPos[1]
		and mousePos[1] - margin <=  zoomInButtonPos[1]):
			self.zoomIn()
			return True

		elif (mousePos[0] + margin >=  zoomOutButtonPos[0]
		and mousePos[0] - margin <=  zoomOutButtonPos[0]
		and mousePos[1] + margin >=  zoomOutButtonPos[1]
		and mousePos[1] - margin <=  zoomOutButtonPos[1]):
			self.zoomOut()
			return True

		else: return False

	@property
	def camera(self): return self.altCam

	# TODO it makes different on how large a map is (-4 * mapRatio?)
	def zoomIn(self):
		if self.altCam.getY() < (-24):
			self.altCam.setY(self.altCam, 1)
			return True
		return False

	# TODO it makes different on how large a map is (-96 * mapRatio?)
	def zoomOut(self):
		if self.altCam.getY() > (-128):
			self.altCam.setY(self.altCam, -1)
			return True
		return False

class InputLocker(DirectObject.DirectObject):
	def __init__(self):
		self._lockInput = False
		self.accept('lockInput', self._setLockInput)

	def _setLockInput(self, state):
		self._lockInput = state

	def locked(self): return self._lockInput

class PlayerCharacter(Character):
	def __init__(self, world, worldNP, characterModel):
		Character.__init__(self, world, worldNP, characterModel)

class PlayerController:
	def __init__(self, character=None, mapId=None):
		"""
		@type character: CharacterProto or None
		@param character: 
		"""
		self._character = None
		self.miniMap = None

		self.character = character
		self.mapId = mapId

		self.keyMap = {
			"shuffleLeft": 0,
			"shuffleRight": 0,
			"forward": 0,
			"backward": 0,
			"jump": 0,

			"walk": 0,
			"rotateLeft": 0,
			"rotateRight": 0,
			"cameraUp": 0,
			"cameraDown" : 0,

			"leftMouse" : 0,
			"rightMouse" : 0
		}
		self._previousKeyMap = self.keyMap.copy()

		base.accept('space', self.setKey, ["jump", True])
		#base.accept('c', self.doCrouch)

		base.accept("a", self.setKey, ["shuffleLeft", True])
		base.accept("d", self.setKey, ["shuffleRight", True])
		base.accept("w", self.setKey, ["forward", True])
		base.accept("W", self.setKey, ["walk", True])
		base.accept("shift", self.setKey, ["walk", True])
		base.accept("s", self.setKey, ["backward", True])
		base.accept("q", self.setKey, ["rotateLeft", True])
		base.accept("e", self.setKey, ["rotateRight", True])
		base.accept("r", self.setKey, ["cameraUp", True])
		base.accept("f", self.setKey, ["cameraDown", True])

		base.accept("a-up", self.setKey, ["shuffleLeft", False])
		base.accept("d-up", self.setKey, ["shuffleRight", False])
		base.accept("w-up", self.setKey, ["forward", False])
		base.accept("W-up", self.setKey, ["walk", False])
		base.accept("shift-up", self.setKey, ["walk", False])
		base.accept("s-up", self.setKey, ["backward", False])
		base.accept("q-up", self.setKey, ["rotateLeft", False])
		base.accept("e-up", self.setKey, ["rotateRight", False])
		base.accept("r-up", self.setKey, ["cameraUp", False])
		base.accept("f-up", self.setKey, ["cameraDown", False])

		base.accept("p", self.toggleCharacter) # TODO this is a test

		self._mouseEvents = {
			"leftJustClicked" : True,
			"rightJustClicked" : True
		}
		self._cursorBackup = [0, 0]
		self._cursorPrevPos = [0, 0]
		self._currentCameraAngle = [0, 0]
		self._cursorBusy = False

		self._cameraZoomLevel = 2.0
		self._cameraZoomMax = 20
		self._cameraZoomMin = 0
		self._cameraHorizontalAngle = 0;
		self._cameraVerticalAngle = 0;

		base.mouseHandler.connect('mouse1', self.setKey, args=["leftMouse", True], priority=10)
		base.mouseHandler.connect('mouse1-up', self.setKey, args=["leftMouse", False], priority=10)

		base.mouseHandler.connect('mouse3', self.setKey, args=["rightMouse", True], priority=10)
		base.mouseHandler.connect('mouse3-up', self.setKey, args=["rightMouse", False], priority=10)

		base.mouseHandler.connect('wheel_up', self.mouseWheelUp, priority=10)
		base.mouseHandler.connect('wheel_down', self.mouseWheelDown, priority=10)

		self._cursorBackup = [0, 0]
		self._cursorPrevPos = [0, 0]

		self._cameraZoomLevel = 2.0
		self._cameraZoomMax = 20
		self._cameraZoomMin = 0
		self._cameraHorizontalAngle = 0
		self._cameraVerticalAngle = 0

		self._inputLocker = InputLocker()
		self._inputChanged = False
		self._orientationChanged = False
		self._movingAnimation = ""

		self.setup()

	def toggleCharacter(self):
		""" This is here for testing.
		"""
		if base.world.player:
			if self.character == base.world.player:
				selectedId = base.world.npcsManager.selectedNpcModel.selectedId
				npc = base.world.npcsManager.getSpawn(selectedId)
				self.character = npc
			else:
				self.character = base.world.player

			base.uiManager.characterChanged()

	def setup(self):
		# Character camera
		self.floater = NodePath(PandaNode("floater"))
		self.camFloater = NodePath(PandaNode("camFloater"))
		self.camFloater.reparentTo(self.floater)
		self.camFloater.setPos(0, -5, 0.6)

		# Player name node
		# TODO more dynamic height
		self.characterName = TextNode('characterName')
		self.characterNameNP = self.floater.attachNewNode(self.characterName)
		self.characterNameNP.setScale(0.1)
		self._attach()

		base.camera.lookAt( 0, 0, 0 )

	def destroy(self):
		taskMgr.remove('userInteract')
		self.floater.removeNode()
		self.camFloater.removeNode()
		self.characterNameNP.removeNode()

		if self.miniMap:
			self._destroyMiniMap()

	def _destroyMiniMap(self):
		self.miniMap.destroy()
		self.miniMap = None

	def _attach(self):
		if self.character:
			self.floater.detachNode()

			self.floater.reparentTo(self.character.characterNP)
			self.floater.setZ(self.character.characterNP, 1.2) # Set floater height TODO make dynamic
			self.characterNameNP.setPos((0.1, .1, 0.3))
			base.camera.reparentTo(self.camFloater)

			self.characterName.setText(self.character.characterData.name)

			self.miniMap.setOrientation(self.character.getOrientation())

	@property
	def character(self): return self._character

	@character.setter
	def character(self, character):
		"""
		@type character: CharacterProto or None
		@param character: 
		"""
		if self._character:
			taskMgr.remove('userInteract')
			self._character.hasDied.disconnect(self.characterDied)

		self._character = character

		if character:
			character.hasDied.connect(self.characterDied)
			taskMgr.add(self.update, 'userInteract')
			self._attach()

	def characterDied(self, spawnId):
		if self.character != base.world.player:
			self.toggleCharacter()
			return

		base.uiManager.actionMessenger.print("You died! Game over! Hehe yeah kidding, this is still TODO!", 20)
		taskMgr.remove('userInteract')

	@property
	def mapId(self): return self._mapId

	@mapId.setter
	def mapId(self, mapId):
		self._mapId = mapId
		if mapId is not None:
			if self.miniMap:
				pass
				# self.miniMap.setMapId() TODO
			else:
				self.miniMap = MiniMap(self.mapId)
		elif self.miniMap:
			self._destroyMiniMap()

	def centerCursor(self):
		props = base.win.getProperties();
		base.win.movePointer(0, int(props.getXSize() / 2), int(props.getYSize() / 2))

	def backupCursorPosition(self):
		mw = base.mouseWatcherNode
		self._cursorBackup = [mw.getMouseX(), mw.getMouseY()]

	def restoreCursorPosition(self):
		props = base.win.getProperties()
		x = int((props.getXSize() / 2) * (self._cursorBackup[0] + 1 ))
		y = int((props.getYSize() / 2) * (-self._cursorBackup[1] + 1 ))
		base.win.movePointer(0, x, y)
		self._cursorBackup = [0, 0]

	def update(self, task):
		dt = globalClock.getDt()
		if self.character and not self.character.isDead(): self.processInput(dt)
		return task.cont

	@property
	def cursorBusy(self): return self._cursorBusy

	def rotate(self, omega): # override
		self._orientationChanged = True
		self.character.rotate(omega)

	def rotateLeft(self, dt): # override
		self._orientationChanged = True
		return self.character.rotateLeft(dt)

	def rotateRight(self, dt): # override
		self._orientationChanged = True
		return self.character.rotateRight(dt)

	def mouseWheelUp(self):
		if (self.camFloater.getY()+0.3) < -1.0:
			self.camFloater.setY(self.camFloater.getY()+0.3)
			return True
		return False

	def mouseWheelDown(self):
		if (self.camFloater.getY()+-0.3) > -15:
			self.camFloater.setY(self.camFloater.getY()-0.3)
			return True
		return False

	def setKey(self, key, value):# override
		self._inputChanged = True
		self.keyMap[key] = value

	def moveCameraVertical(self, amount):
		newZ = self.camFloater.getZ(self.character.characterNP) + amount
		if newZ < 15 and newZ > -15: # Max zoomout
			self.camFloater.setZ(self.camFloater, amount)

	def processInput(self, dt):
		omega = 0.0
		mouseForward = False

		camSen = 220
		charSen = 32

		if self.keyMap["leftMouse"] or self.keyMap["rightMouse"]:
			if self.keyMap["leftMouse"] and self.keyMap["rightMouse"]:
				mouseForward = True

			if self.keyMap["rightMouse"]:
				if self._mouseEvents["rightJustClicked"]:
					self._mouseEvents["rightJustClicked"] = False
					if not self._cursorBackup[0]:
						self._cursorBusy = True
						base.mouseHandler.showCursor(False);
						self.backupCursorPosition();
						self.centerCursor();
				else:
					# Move camera
					xDiff = base.mouseWatcherNode.getMouseX()
					yDiff = base.mouseWatcherNode.getMouseY()

					self.moveCameraVertical(-(yDiff * camSen) * dt)

					# Rotate characterNP
					omega = -xDiff * charSen
					self.rotate(omega)
					self.centerCursor()

			elif self.keyMap["leftMouse"]:
				if self._mouseEvents["leftJustClicked"]:
					self._mouseEvents["leftJustClicked"] = False
					if not self._cursorBackup[0]:
						self._cursorBusy = True
						base.mouseHandler.showCursor(False);
						self.backupCursorPosition();
						self.centerCursor();
				else:
					xDiff = base.mouseWatcherNode.getMouseX() * 560
					yDiff = base.mouseWatcherNode.getMouseY() * 300

					self.moveCameraVertical(-yDiff * dt)

					self.floater.setH(self.floater, -xDiff * dt)
					self.centerCursor();

		elif self._cursorBackup[0]:
			self._mouseEvents["leftJustClicked"] = True
			self._mouseEvents["rightJustClicked"] = True
			self.restoreCursorPosition();
			self._cursorBusy = False
			base.mouseHandler.showCursor();
			mouseForward = False

		# Restore camera behind character if moving and not leftmouse
		if self.keyMap['forward'] and not self.keyMap["leftMouse"]:
			if self.floater.getH() > 1:
				self.floater.setH(self.floater, -1)
			elif self.floater.getH() < -1:
				self.floater.setH(self.floater, 1)

		moveVec = Vec3()
		if self.keyMap['forward'] or mouseForward:
			self.character.forward(dt, moveVec)
		elif self.keyMap['backward']:
			self.character.backward(dt, moveVec)

		if self.keyMap['shuffleLeft']:
			self.character.shuffleLeft(dt, moveVec)
		elif self.keyMap['shuffleRight']:
			self.character.shuffleRight(dt, moveVec)

		if self.keyMap['rotateLeft']:
			self.miniMap.modOrientation(self.rotateLeft(dt))
		elif self.keyMap['rotateRight']:
			self.miniMap.modOrientation(self.rotateRight(dt))

		if self._orientationChanged:
			self._orientationChanged = False
			# Update mini-map orientation
			if self.miniMap:
				self.miniMap.setOrientation(self.character.getOrientation())

		""" Animations
		"""
		if self._movingAnimation == "run":
			if not (self.keyMap["forward"] or mouseForward):
				self.character.actorNP.stop()
				self.character.actorNP.loop("idle")
				self._movingAnimation = ""

			elif self.keyMap["jump"]:
				self.character.actorNP.stop()
				self.character.actorNP.setPlayRate(1.2, "runJump")
				self.character.actorNP.play("runJump")
				self._movingAnimation = "runJump"

		elif self._movingAnimation == "runJump":
			if self.character.characterCont.isOnGround():
				self.character.actorNP.loop("run")
				self._movingAnimation = "run"

		else:
			if self.keyMap["forward"] or mouseForward:
				self.character.actorNP.loop("run")
				self._movingAnimation = "run"

		""" Apply inputs
		"""
		if self.keyMap["jump"]:
			self.keyMap["jump"] = False
			self.character.doJump()

		# Move
		self.character.setPos(moveVec)

		self.character.updatePreviousPos()
		if self.character.isMoving:
			if self.miniMap: self.miniMap.setPos(self.character._previousCharacterPos)

		if self.keyMap['cameraUp']:
			self.moveCameraVertical(0.2)
		elif self.keyMap['cameraDown']:
			self.moveCameraVertical(-0.2)

		base.camera.lookAt(self.floater)
