## BugData
##
## This module provides functions to efficiently store structured data in the CyGame script data.
##
## The data consists of nested dictionaries called Tables. Each Table is pickled by this
## module to maintain compatibility with SdToolKit. Access to the values (slots) in a Table
## is provided by standard [] notation, and they not pickled individually.
##
## You can nest Tables to any depth and store any value--even dictionaries and objets--into
## individual slots. Tables and slots are located using key chains: lists of string identifiers
## such as ("Reminders", "queues") or ("UnitCnt", "UNITArcher", "tt1").
##
## Notes
##
##   - Must be cleared at the start of the GameStart event
##   - Must be loaded at the start of the OnLoad event
##   - Must be saved at the end of the OnPreSave event
##
##   - The key of the root Table is "root", but you must not include it in your key chains.
##
## This module stores its data in a way that is 100% save compatible with SdToolKit.
## Since BUG's SdToolKit has been modified to use this module you can continue to use
## SdToolKit's API. Switching to use this module could provide a minor speed improvement
## if you modify large data structures at the cost of tying your code to this module.
##
## TODO
##
##   - Drop Mod/Entity
##   - getData(): uses pickle
##   - getValue() or []: direct
##   - Allows arbitrary nesting of pickled dicts, including raw values at the top-level
##
## Copyright (c) 2010 The BUG Mod.
##
## Author: EmperorFool

from CvPythonExtensions import CyGame
import BugUtil
import cPickle as pickle


## Data Access

def hasTable(*keys):
	"""
	Returns True if the chain of key(s) leads to a table, False otherwise.
	"""
	return getGameData().hasTable(*keys)

def findTable(*keys):
	"""
	If the key(s) lead to an existing table, it is returned. Otherwise None is returned.
	
	This function will not create any new tables. You must check the return value for None
	or you will get an exception.
	"""
	return getGameData().findTable(*keys)

def getTable(*keys):
	"""
	Returns the table at the end of the chain of key(s), creating any missing tables along the way.
	
	This function always returns a valid table.
	"""
	return getGameData().getTable(*keys)

def deleteTable(*keys):
	"""
	If the chain of key(s) leads to a table, it is deleted and True is returned; otherwise False is returned.
	"""
	return getGameData().delTable(*keys)


## Event Handlers and Internal Access

def getGameData():
	if not g_data:
		BugUtil.warn("BugData not loaded; loading now")
		initGameData().load()
	return g_data

def initGameData():
	global g_data
	g_data = RootTable()
	return g_data


## Event Handlers

def onGameStart(argsList):
	initGameData()

def onGameLoad(argsList):
	initGameData().load()

def save():
	getGameData().save()


## Globals

g_data = None


## Table Classes

class Table(object):
	def __init__(self, key, data):
		self.key = key
		self.data = data
		self.dirty = False
		self.children = dict()
	def __str__(self):
		return self.key
	def load(self):
		pass
	def save(self):
		if self.children:
			BugUtil.debug("BugData - saving %s's children", self)
			for table in self.children.itervalues():
				table.save()
		if self.dirty:
			BugUtil.debug("BugData - saving %s", self)
			self._save()
			self.dirty = False
		else:
			BugUtil.debug("BugData - %s not dirty", self)
	def _save(self):
		pass
	def __del__(self):
		if self.dirty:
			BugUtil.warn("Data not saved: %s", self)
	
	def setData(self, data):
		self.data = data
		self.dirty = True
		BugUtil.debug("BugData - new data for %s: %r", self, self.data)
	def __contains__(self, key):
		BugUtil.debug("BugData - %s in %s: %s", key, self, key in self.data)
		return key in self.data
	def __getitem__(self, key):
		BugUtil.debug("BugData - %s.%s = %r", self, key, self.data[key])
		return self.data[key]
	def __setitem__(self, key, value):
		BugUtil.debug("BugData - %s.%s -> %r", self, key, value)
		self.data[key] = value
		self.dirty = True
	def __delitem__(self, key):
		BugUtil.debug("BugData - del %s.%s", self, key)
		try:
			del self.data[key]
			self.dirty = True
		except:
			pass
	
	def hasValue(self, key):
		return key in self
	def getValue(self, key):
		return self[key]
	def setValue(self, key, value):
		self[key] = value
	def delValue(self, key):
		del self[key]
	
	def hasTable(self, *keys):
		if len(keys) == 1:
			return self._hasTable(keys[0])
		else:
			table = self._findTable(keys[0])
			if table is None:
				return False
			else:
				return table.hasTable(*keys[1:])
	def _hasTable(self, key):
		return key in self.data
	def findTable(self, *keys):
		table = self._findTable(keys[0])
		if table is None or len(keys) == 1:
			return table
		else:
			return table.findTable(*keys[1:])
	def _findTable(self, key):
		if self._hasTable(key):
			BugUtil.debug("BugTable - found %s in %s", key, self)
			return self._getTable(key)
		else:
			BugUtil.debug("BugTable - %s not found in %s", key, self)
			return None
	def getTable(self, *keys):
		if len(keys) == 1:
			return self._getTable(keys[0])
		else:
			return self._getTable(keys[0]).getTable(*keys[1:])
	def _getTable(self, key):
		if self._isOpen(key):
			BugUtil.debug("BugData - returning open table %s.%s", self, key)
			return self.children[key]
		elif self._hasTable(key):
			return self._openTable(key, pickle.loads(self[key]))
		else:
			return self._openTable(key, dict())
	def delTable(self, *keys):
		if len(keys) == 1:
			return self._delTable(keys[0])
		else:
			table = self._findTable(keys[0])
			if table is None:
				return False
			else:
				return table.delTable(*keys[1:])
	def _delTable(self, key):
		self._closeTable(key)
		if self._hasTable(key):
			BugUtil.debug("BugData - deleting %s.%s", self, key)
			del self[key]
	
	def _isOpen(self, key):
		return key in self.children
	def _openTable(self, key, data):
		BugUtil.debug("BugData - opening %s.%s: %r", self, key, data)
		table = ChildTable(self, key, data)
		self.children[key] = table
		return table
	def _closeTable(self, key):
		if self._isOpen(key):
			BugUtil.debug("BugData - closing %s.%s", self, key)
			table = self.children[key]
			table.discard()
			del self.children[key]


class RootTable(Table):
	def __init__(self):
		super(RootTable, self).__init__("root", dict())
		self.game = CyGame()
	def load(self):
		BugUtil.debug("BugData - loading %s", self)
		script = self.game.getScriptData()
		if script:
			self.data = pickle.loads(script)
			BugUtil.debug("BugData - loaded %r", self.data)
	def _save(self):
		self.game.setScriptData(pickle.dumps(self.data))


class ChildTable(Table):
	def __init__(self, parent, key, data):
		super(ChildTable, self).__init__(key, data)
		self.parent = parent
	def __str__(self):
		if self.parent:
			return str(self.parent) + "." + self.key
		else:
			return "detached " + self.key
	def discard(self):
		if self.dirty:
			BugUtil.debug("BugData - discarding dirty %s", self)
		else:
			BugUtil.debug("BugData - discarding %s", self)
		self.parent = None
	def load(self):
		if self.parent:
			self.data = pickle.loads(self.parent[self.key])
	def _save(self):
		if self.parent:
			self.parent[self.key] = pickle.dumps(self.data)
