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


import os
import json


global AssetsPath
global Spells
global NPCs
global Maps
global Players
global Classes

class GenericData: # dict
	empty = {}
	def __init__(self, _id=-1, data=None):
		self._id = _id
		if data: self._load(_id, data)
		else: self.setEmpty()

	def _load(self, _id, data):
		self._id = _id
		self._data = data

	@property
	def id(self): return self._id

	@id.setter
	def id(self, value): self._id = value

	def data(self): return self._data

	def columns(self): return len(self.data())

	def setEmpty(self):
		self._data = self.empty.copy()

class GenericDataList(GenericData): # list
	empty = []
	def __init__(self, _id=-1, data=None):
		GenericData.__init__(self, _id=_id, data=data)

"""Quests
"""
class IdAmountPair(list):
	def __init__(self, data=[0,1]):
		list.__init__(self, data)

	def __hash__(self): return self.id

	@property
	def id(self): return self[0]
	@property
	def amount(self): return self[1]

	@id.setter
	def id(self, value): self[0] = value
	@amount.setter
	def amount(self, value): self[1] = value

class QuestRequirements(list):
	## [level=0, species=[], fractions=[], classes=[], completedQuests=[]]
	def __init__(self, data=[0,[],[],[],[]]):
		list.__init__(self, data)

	@property
	def level(self): return self[0]
	@property
	def species(self): return self[1]
	@property
	def fractions(self): return self[2]
	@property
	def classes(self): return self[3]
	@property
	def completedQuests(self): return self[4]

	@level.setter
	def level(self, value): self[0] = value
	@species.setter
	def species(self, value): self[1] = value
	@fractions.setter
	def fractions(self, value): self[2] = value
	@classes.setter
	def classes(self, value): self[3] = value
	@completedQuests.setter
	def completedQuests(self, value): self[4] = value

class QuestRewards(list):
	## [experience=0, items=[]]
	def __init__(self, data=[0,[]]):
		list.__init__(self, data)

	@property
	def experience(self): return self[0]
	@property
	def items(self): return self[1]

	@experience.setter
	def experience(self, value): self[0] = value
	@items.setter
	def items(self, value): self[1] = value

class QuestObjectives(list):
	## [talkTo=[], locations=[], defeats=[], items=[]]
	def __init__(self, data=[[],[],[],[]]):
		list.__init__(self, data)

	@property
	def talkTo(self):
		return [IdAmountPair(itemPair) for itemPair in self[0]]
		#return self[0]
	@property
	def locations(self): return self[1]
	@property
	def defeats(self):
		#return self[2]
		return [IdAmountPair(itemPair) for itemPair in self[2]]
	@property
	def items(self): return self[3]

	@talkTo.setter
	def talkTo(self, value): self[0] = value
	@locations.setter
	def locations(self, value): self[1] = value
	@defeats.setter
	def defeats(self, value): self[2] = value
	@items.setter
	def items(self, value): self[3] = value

class QuestData(GenericDataList):
	empty = ['new', '', [0,[],[],[],[]], [0,[]], [[],[],[],[]]]

	def __init__(self, _id=-1, data=None):
		GenericDataList.__init__(self, _id, data=data)
		if data:
			self._data[2] = QuestRequirements(self._data[2])
			self._data[3] = QuestRewards(self._data[3])
			self._data[4] = QuestObjectives(self._data[4])

		"""
		0 title = ""
		1 description = ""
		2 requirements = QuestRequirements
		3 rewards = QuestRewards
		4 objectives = QuestObjectives
		"""

	@property
	def title(self): return self._data[0]
	@property
	def description(self): return self._data[1]
	@property
	def requirements(self): return self._data[2]
	@property
	def rewards(self): return self._data[3]
	@property
	def objectives(self): return self._data[4]

	@title.setter
	def title(self, value): self._data[0] = value
	@description.setter
	def description(self, value): self._data[1] = value
	@requirements.setter
	def requirements(self, value): self._data[2] = value
	@rewards.setter
	def rewards(self, value): self._data[3] = value
	@objectives.setter
	def objectives(self, value): self._data[4] = value


"""Classes
"""
class ClassData(GenericDataList):
	empty = ['new',[]]

	def __init__(self, _id=-1, data=None):
		GenericDataList.__init__(self, _id, data=data)

		"""
		0	name
		1	default spells/actions
		"""

	@property
	def name(self): return self._data[0]
	@property
	def spells(self): return self._data[1] #[Spells[spellId] for spellId in self._data[1]]

	@name.setter
	def name(self, value): self._data[0] = value
	@spells.setter
	def spells(self, value):
		self._data[1] = value

""" Fractions
"""
class FractionData(GenericDataList):
	empty = ['new',[],[]]

	def __init__(self, _id=-1, data=None):
		GenericDataList.__init__(self, _id, data=data)

		"""
		0	name
		1	allies [] (list with fraction id's)
		2	enemies [] (list with fraction id's)
		"""

	@property
	def name(self): return self._data[0]
	@property
	def allies(self): return self._data[1]
	@property
	def enemies(self): return self._data[2]

	@name.setter
	def name(self, value): self._data[0] = value
	@allies.setter
	def allies(self, value): self._data[1] = value
	@enemies.setter
	def enemies(self, value): self._data[2] = value


"""CharacterData - base for PlayerData and NPCData
"""
class StatData:
	def __init__(self, data):
		self._data = data

	def data(self): return self._data

	@property
	def max(self): return int(self._data[0])
	@property
	def value(self): return int(self._data[1])

	@max.setter
	def max(self, value): self._data[0] = int(value)
	@value.setter
	def value(self, value): self._data[1] = int(value)

class StatsData(GenericDataList):
	empty = [
		(10, 0), # level 
		(80, 80), # health 
		(50, 50), # energy 
		(10, 1), # strength 
		(10, 1), # stamina 
		(25, 2), # resistance
		(10, 0.5), # reflexes 
		(10, 0) # spitit 
	]
	def __init__(self, _id=-1, data=None):
		GenericData.__init__(self, _id, data=data)
		"""
		0	level
		1	health
		2	energy
		3	strength
		4	stamina
		5	resistance
		6	reflexes
		7	spitit
		"""

	@property
	def level(self): return StatData(self._data[0])
	@property
	def health(self): return StatData(self._data[1])
	@property
	def energy(self): return StatData(self._data[2])
	@property
	def strength(self): return StatData(self._data[3])
	@property
	def stamina(self): return StatData(self._data[4])
	@property
	def resistance(self): return StatData(self._data[5])
	@property
	def reflexes(self): return StatData(self._data[6])
	@property
	def spitit(self): return StatData(self._data[7])

	@level.setter
	def level(self, value): self._data[0] = int(value)
	@health.setter
	def health(self, value): self._data[1] = int(value)
	@energy.setter
	def energy(self, value): self._data[2] = int(value)
	@strength.setter
	def strength(self, value): self._data[3] = int(value)
	@stamina.setter
	def stamina(self, value): self._data[4] = int(value)
	@resistance.setter
	def resistance(self, value): self._data[5] = int(value)
	@reflexes.setter
	def reflexes(self, value): self._data[6] = int(value)
	@spitit.setter
	def spitit(self, value): self._data[7] = int(value)

class PlayerStatsData(StatsData):
	empty = [
		(10, 0), # level 
		(80, 80), # health 
		(50, 50), # energy 
		(10, 1), # strength 
		(10, 1), # stamina 
		(25, 2), # resistance
		(10, 0.5), # reflexes 
		(10, 0), # spitit 
		(0,100) # experience
	]
	def __init__(self, _id=-1, data=None):
		StatsData.__init__(self, _id, data)
		"""
		8	experience
		"""

	@property
	def experience(self): return StatData(self._data[8])

	@experience.setter
	def experience(self, value): self._data[8] = value

class CharacterData(GenericDataList):
	"""
	0	name
	1	dirName
	2	file
	3	animations {}
	4	stats {}
	5	collisionShape {}
	6	speciesId
	7	fractionId
	8	spellIds []
	"""
	empty = [
		'new',
		'dir-name',
		'.egg',
		{'idle':'.egg', 'walk':'.egg', 'run':'.egg', 'jump':'.egg'},
		StatsData.empty,
		[1,1],
		1,
		[]
	]

	def __init__(self, _id=-1, data=None):
		GenericDataList.__init__(self, _id, data=data)

	@property
	def name(self): return self._data[0]
	@property
	def dirName(self): return self._data[1]
	@property
	def file(self): return self._data[2]
	@property
	def animations(self): return self._data[3]
	@property
	def stats(self): return StatsData(data = self._data[4])
	@property
	def collisionShape(self): return self._data[5]
	@property
	def speciesId(self): return self._data[6]
	@property
	def fractionId(self): return self._data[7]
	@property
	def spells(self): return self._data[8]

	@name.setter
	def name(self, value): self._data[0] = value
	@dirName.setter
	def dirName(self, value): self._data[1] = value
	@file.setter
	def file(self, value): self._data[2] = value
	@animations.setter
	def animations(self, value): self._data[3] = value
	@stats.setter
	def stats(self, value): self._data[4] = value
	@collisionShape.setter
	def collisionShape(self, value): self._data[5] = value
	@speciesId.setter
	def speciesId(self, value): self._data[6] = value
	@fractionId.setter
	def fractionId(self, value): self._data[7] = value
	@spells.setter
	def spells(self, value): self._data[8] = value


"""Players
"""
class PlayerData(CharacterData):
	empty = [
		'new',
		'dir-name',
		'.egg',
		{'idle':'.egg', 'walk':'.egg', 'run':'.egg', 'jump':'.egg'},
		PlayerStatsData.empty,
		[1,1],
		1,
		[]
	]
	def __init__(self, _id=-1, data=None):
		CharacterData.__init__(self, _id, data)

	@property
	def filePath(self): return os.path.join(AssetsPath.players, self.dirName, self.file)

	@property
	def stats(self):
		return PlayerStatsData(data = self._data[4])

	@stats.setter
	def stats(self, value): self._data[4] = value


"""Maps
"""
class MapData(GenericDataList):
	empty = ['New map','map','.egg','.egg']

	def __init__(self, _id=-1, data=None):
		GenericData.__init__(self, _id, data)
		"""
		0	name
		1	dir-name
		2	file-name.egg
		3	ortho-file-name.egg
		"""

	@property
	def name(self): return self._data[0]
	@property
	def dirName(self): return self._data[1]
	@property
	def file(self): return self._data[2]
	@property
	def filePath(self): return os.path.join(AssetsPath.maps, self.dirName, self.file)
	@property
	def orthoFile(self): return self._data[3]
	@property
	def orthoFilePath(self): return os.path.join(AssetsPath.maps, self.dirName, self.orthoFile)

	@property
	def spawns(self): return Spawns(self.dirName)

	@name.setter
	def name(self, value): self._data[0] = value
	@dirName.setter
	def dirName(self, value): self._data[1] = value
	@file.setter
	def file(self, value): self._data[2] = value
	@orthoFile.setter
	def orthoFile(self, value): self._data[3] = value

"""Spawn data
"""
class GenericSpawnData(GenericDataList):
	empty = [1,0,0,0,0]
	def __init__(self, _id=-1, data=None):
		GenericDataList.__init__(self, _id, data=data)
		"""
		0	characterId (npc or player-character)
		1	x
		2	y
		3	z
		4	o
		"""

		#if data: self._load(_id, data)

	@property
	def characterId(self): return self._data[0]
	@property
	def x(self): return self._data[1]
	@property
	def y(self): return self._data[2]
	@property
	def z(self): return self._data[3]
	@property
	def orientation(self): return self._data[4]

	@property
	def pos(self): return (self.x, self.y, self.z)

	@characterId.setter
	def characterId(self, value): self._data[0] = value
	@x.setter
	def x(self, value): self._data[1] = value
	@y.setter
	def y(self, value): self._data[2] = value
	@z.setter
	def z(self, value): self._data[3] = value
	@orientation.setter
	def orientation(self, value): self._data[4] = value

class NPCSpawnData(GenericSpawnData):
	empty = GenericSpawnData.empty + [10]
	def __init__(self, _id=-1, data=None):
		GenericSpawnData.__init__(self, _id, data)
		"""
		5	respawnTime - in seconds
		"""

	@property
	def respawnTime(self): return self._data[5]

	@respawnTime.setter
	def respawnTime(self, value): self._data[5] = value

"""NPCs
"""

class NPCData(CharacterData):
	"""
	0	name
	1	dirName
	2	file
	3	animations {}
	4	stats {}
	5	collisionShape []
	6	speciesId
	7	fractionId
	8	spellIds []
	"""
	empty = [
		'new',
		'dir-name',
		'.egg',
		{'idle':'.egg', 'walk':'.egg', 'run':'.egg', 'jump':'.egg'},
		StatsData.empty,
		[1,1],
		1,
		1,
		[]
	]
	def __init__(self, _id=-1, data=None):
		CharacterData.__init__(self, _id, data)

	@property
	def filePath(self): return os.path.join(AssetsPath.npcs, self.dirName, self.file)

"""Spells
"""
class SpellData(GenericDataList):
	empty = [1,0,0,0,[0,1],1,1,1,"new","unknown.png",""]
	def __init__(self, _id=-1, data=None):
		GenericDataList.__init__(self, _id, data=data)
		"""
		0. castTime
		1. coolDown
		2. methodId
		3. targetType (selected, self or range)
		4. range [start, end]
		5. facingAngle (set to 360 if not requiring facing target)
		6. impactPoints (- for damage, + for healing)
		7. energyCost
		8. name
		9. icon
		10. desc
		"""

	def iconPath(self):
		# return full icon path
		return os.path.join(str(AssetsPath), 'icons/', self.icon)

	@property
	def castTime(self): return float(self._data[0])
	@property
	def coolDown(self): return float(self._data[1])
	@property
	def method(self): return int(self._data[2])
	@property
	def targetType(self): return int(self._data[3])
	@property
	def range(self): return self._data[4]
	@property
	def facingAngle(self): return int(self._data[5])
	@property
	def impactPoints(self): return int(self._data[6])
	@property
	def energyCost(self): return int(self._data[7])
	@property
	def name(self): return self._data[8]
	@property
	def icon(self): return self._data[9]
	@property
	def desc(self): return self._data[10]

	@property
	def rangeStart(self): return self._data[4][0]
	@property
	def rangeEnd(self): return self._data[4][1]

	@castTime.setter
	def castTime(self, value): self._data[0] = float(value)
	@coolDown.setter
	def coolDown(self, value): self._data[1] = float(value)
	@method.setter
	def method(self, value): self._data[2] = int(value)
	@targetType.setter
	def targetType(self, value): self._data[3] = int(value)
	@range.setter
	def range(self, value): self._data[4] = value
	@facingAngle.setter
	def facingAngle(self, value): self._data[5] = int(value)
	@impactPoints.setter
	def impactPoints(self, value): self._data[6] = int(value)
	@energyCost.setter
	def energyCost(self, value): self._data[7] = int(value)
	@name.setter
	def name(self, value): self._data[8] = value
	@icon.setter
	def icon(self, value): self._data[9] = value
	@desc.setter
	def desc(self, value): self._data[10] = value

class _AssetsPath:
	def __init__(self, path='assets/'):
		self._path = path
		self._callbacks = []

	def __str__(self): return self._path
	def __repr__(self): return self._path

	@property
	def icons(self): return os.path.join(str(self), 'icons/')

	@property
	def npcs(self): return os.path.join(str(self), 'creatures/')

	@property
	def maps(self): return os.path.join(str(self), 'maps/')

	@property
	def players(self): return os.path.join(str(self), 'characters/')

	@property
	def widgets(self): return os.path.join(str(self), 'widgets/')

	def joinPath(self, other):
		return os.path.join(str(self), other)

	def set(self, path):
		self._path = path
		for cb in self._callbacks: cb()

	def addReloadCallback(self, cb):
		self._callbacks.append(cb)


class Table:
	def __init__(self, _file, _type):
		self._file = _file
		self._filePath = self.filePath()
		self._fileValid = False
		self._data = {}
		self._type = _type
		AssetsPath.addReloadCallback(self.__load)

	def columns(self): return len(self._type.empty) + 1 # +1 for id

	def __iter__(self):
		for _id, item in self._data.items(): yield item

	def __getitem__(self, key):
		return self._data.get(str(key))

	def __contains__(self, id):
		return bool(id in self._data)

	def reload(self): self.__load()

	def __load(self):
		self._data = {}
		if os.path.isfile(self.filePath()):
			print(" [OK] Found {0}".format(self._file))
			with open(self.filePath()) as f:
				try:
					data = json.load(f)
					for _id, d in data.items():
						self._data.update({_id : self._type(_id, d)})
				except json.decoder.JSONDecodeError as err:
					print("\t! Empty or corrupt file! Error: {0}".format(err))
		else:
			print(" [XX] Not found {0}".format(self._file))

	def __save(self):
		data = {}
		for item in self:
			data.update({item.id : item.data()})
		with open(self.filePath(), 'w') as fp:
			json.dump(data, fp)

	def _getNewId(self):
		for i in range(1, 1024):
			if str(i) not in self._data: return str(i)

	def filePath(self): return os.path.join(str(AssetsPath), self._file)

	def new(self, data=[]):
		if not data: data = self._type.empty
		_id = self._getNewId()
		self.edit(_id, data)
		return _id

	def edit(self, _id, newData):
		self._data.update({_id : self._type(_id, newData)})

	def remove(self, _id):
		self._data.pop(_id)

	def save(self): self.__save()

class _SpellMethods:
	names = ['Basic']

	basic = 0

class _SpellTargetTypes:
	names = ['Self', 'Selected', 'Range']

	self = 0
	selected = 1
	range = 2


class _Spells(Table):
	def __init__(self):
		Table.__init__(self, _file='spells.json', _type=SpellData)

	@property
	def methods(self):
		""" Returns the avaiable processing methods.
		A method will do the actual processing.

		@rtype: _SpellMethods
		@return:
		"""
		return _SpellMethods

	@property
	def targetTypes(self):
		""" Returns the avaiable target types.

		Self - Target self.
		Selected - Target selected.
		Range - Target all NPC's in set range.

		@rtype: _SpellTargetTypes
		@return:
		"""
		return _SpellTargetTypes

class _NPCs(Table):
	def __init__(self):
		Table.__init__(self, _file='npcs.json', _type=NPCData)

class _Maps(Table):
	def __init__(self):
		Table.__init__(self, _file='maps.json', _type=MapData)

class _Players(Table):
	def __init__(self):
		Table.__init__(self, _file='players.json', _type=PlayerData)

class _Classes(Table):
	def __init__(self):
		Table.__init__(self, _file='classes.json', _type=ClassData)

class _Fractions(Table):
	def __init__(self):
		Table.__init__(self, _file='fractions.json', _type=FractionData)

class _Quests(Table):
	def __init__(self):
		Table.__init__(self, _file='quests.json', _type=QuestData)

class Spawns(Table):
	def __init__(self, mapDir):
		_file = 'spawns.json'
		self._subPath = os.path.join(mapDir, _file)
		Table.__init__(self, _file=_file, _type=NPCSpawnData)
		self.reload()

	def filePath(self): return os.path.join(AssetsPath.maps, self._subPath)


AssetsPath = _AssetsPath()
Spells = _Spells()
NPCs = _NPCs()
Maps = _Maps()
Players = _Players()
Classes = _Classes()
Fractions = _Fractions()
Quests = _Quests()
