##
#	\namespace	cross3d.abstract.studiomaxapplication
#
#	\remarks	The StudiomaxApplication class will define all operations for application interaction. It is a singleton class, so calling cross3d.Application() will
#				always return the same instance of Application. One of its main functions is connecting application callbacks to cross3d.Dispatch.
#
#				The StudiomaxApplication is a QObject instance and any changes to the scene data can be controlled by connecting to the signals defined here.
#
#				When subclassing the AbstractScene, methods tagged as @abstractmethod will be required to be overwritten.  Methods tagged with [virtual]
#				are flagged such that additional operations could be required based on the needs of the method.  All @abstractmethod methods MUST be implemented
#				in a subclass.
#
#	\author		Mikeh
#	\author		Blur Studio
#	\date		06/07/11
#

import cross3d
from cross3d.abstract.abstractapplication import AbstractApplication
from Py3dsMax import mxs
from cross3d.enum import EnumGroup, Enum
from PyQt4.QtCore import QTimer
_n = mxs.pyhelper.namify
dispatch = None

# initialize callback scripts
_STUDIOMAX_CALLBACK_TEMPLATE = """
global cross3d
if ( cross3d == undefined ) do ( cross3d = pymax.import "cross3d" )
if ( cross3d != undefined ) do (
	local ms_args = (callbacks.notificationParam())
	cross3d.%(cls)s.%(function)s "%(signal)s" %(args)s
)
"""
_STUDIOMAX_CALLBACK_TEMPLATE_NO_ARGS = """
global cross3d
if ( cross3d == undefined ) do ( cross3d = pymax.import "cross3d" )
if ( cross3d != undefined ) do (
	cross3d.%(cls)s.%(function)s "%(signal)s"
)
"""
_STUDIOMAX_VIEWPORT_TEMPLATE = """
fn blurfn_%(signal)s =
(
	if ( cross3d == undefined ) do ( cross3d = pymax.import "cross3d" )
	if ( cross3d != undefined ) do (
		cross3d.%(cls)s.%(function)s "%(signal)s"
	)
)
"""

class _ConnectionType(EnumGroup):
	General = Enum()
	Viewport = Enum()

class _ConnectionDef:
	""" Class that stores all neccissary info to connect cross3d.dispatch to StudioMax

	Args:
		signal str: The name of the cross3d.dispatch signal.
		callback str: The name of StudioMax callback.
		arguments str: A string of maxscript arguments passed to signal when the callback is emitted.
		function str: The name of the cross3d.dispatch function. Used in the callback maxscript.
		callbackType _ConnectionType: Controls how the callback is connected to StudioMax.
		cls str: The name of the class called in cross3d. Normally 'dispatch'.
		associated _ConnectionDef: When this signal is connected, all _ConnectionDef's in this list
			are also connected to. This is used by filePostMerge to disable all callbacks durring
			the opening of a file.
	"""
	def __init__(self, signal, callback, arguments='', function='dispatch', callbackType=_ConnectionType.General, cls='dispatch', associated=[]):
		self.signal = signal
		self.callback = callback
		self.arguments = arguments
		self.function = function
		self.callbackType = callbackType
		self.cls = cls
		self.associated = associated

	@staticmethod
	def asDict(signal, callback, arguments = '', function = 'dispatch', callbackType=_ConnectionType.General):
		return {signal:_ConnectionDef(signal, callback, arguments, function, callbackType)}

class _ConnectionStore(object):
	def __init__(self):
		self._store = []

	def update(self, connection):
		self._store.append(connection)

	def getConnectionsBySignalName(self, signal):
		return [c for c in self._store if c.signal == signal]

	def getSignalNames(self):
		return list(set([c.signal for c in self._store]))

class StudiomaxApplication(AbstractApplication):
	# create a mapping of callbacks to be used when connecting signals
	_connectionMap = _ConnectionStore()
	_connectionMap.update(_ConnectionDef('sceneNewRequested', 'systemPreNew'))
	_connectionMap.update(_ConnectionDef('sceneNewFinished', 'systemPostNew'))
	_connectionMap.update(
		_ConnectionDef(
			'sceneOpenRequested',
			'filePreOpen',
			'""',
			function='_prePostCallback',
			cls='application'
		)
	)
	_connectionMap.update(
		_ConnectionDef(
			'sceneOpenFinished',
			'filePostOpen',
			'""',
			function='_prePostCallback',
			cls='application',
			associated=_connectionMap.getConnectionsBySignalName('sceneOpenRequested')
		)
	)
	_connectionMap.update(
		_ConnectionDef(
			'sceneMergeRequested',
			'filePreMerge',
			function='_prePostCallback',
			cls='application'
		)
	)
	_connectionMap.update(_ConnectionDef('sceneReferenceRequested', 'objectXrefPreMerge'))
	_connectionMap.update(_ConnectionDef('sceneReferenceRequested', 'sceneXrefPreMerge'))
	_connectionMap.update(
		_ConnectionDef(
			'sceneMergeFinished',
			'filePostMerge',
			function='_prePostCallback',
			cls='application',
			associated=_connectionMap.getConnectionsBySignalName('sceneMergeRequested')
		)
	)
	_connectionMap.update(_ConnectionDef('sceneReferenceFinished', 'objectXrefPostMerge'))
	_connectionMap.update(_ConnectionDef('sceneReferenceFinished', 'sceneXrefPostMerge'))
	_connectionMap.update(_ConnectionDef('sceneSaveRequested', 'filePreSave', '(if (ms_args != undefined) then (ms_args as string) else "")'))
	_connectionMap.update(_ConnectionDef('sceneSaveFinished', 'filePostSave', '(if (ms_args != undefined) then (ms_args as string) else "")'))
	_connectionMap.update(_ConnectionDef('scenePreReset', 'systemPreReset'))
	_connectionMap.update(_ConnectionDef('sceneReset', 'systemPostReset'))
	_connectionMap.update(_ConnectionDef('layerCreated', 'layerCreated'))
	_connectionMap.update(_ConnectionDef('layerDeleted', 'layerDeleted'))
	_connectionMap.update(_ConnectionDef('startupFinished', 'postSystemStartup'))
	_connectionMap.update(_ConnectionDef('shutdownStarted', 'preSystemShutdown'))
	_connectionMap.update(_ConnectionDef('sceneImportFinished', 'postImport'))
	_connectionMap.update(_ConnectionDef('selectionChanged', 'selectionSetChanged'))
	_connectionMap.update(_ConnectionDef('objectFreeze', 'nodeFreeze', 'ms_args', 'dispatchObject'))
	_connectionMap.update(_ConnectionDef('objectUnfreeze', 'nodeUnfreeze', 'ms_args', 'dispatchObject'))
	_connectionMap.update(_ConnectionDef('objectHide', 'nodeHide', 'ms_args', 'dispatchObject'))
	_connectionMap.update(_ConnectionDef('objectUnHide', 'nodeUnHide', 'ms_args', 'dispatchObject'))
	_connectionMap.update(_ConnectionDef('objectRenamed', 'nodeNameSet', '(if (ms_args != undefined) then (#(ms_args[1], ms_args[2], ms_args[3])) else #("", "", ""))', 'dispatchRename'))
	_connectionMap.update(_ConnectionDef('objectCreated', 'nodeCreated', 'ms_args', 'dispatchObject'))
	_connectionMap.update(_ConnectionDef('objectCloned', 'nodeCloned', 'ms_args', 'dispatchObject'))
	_connectionMap.update(_ConnectionDef('objectAdded', 'sceneNodeAdded', 'ms_args', 'dispatchObject'))
	_connectionMap.update(_ConnectionDef('objectPreDelete', 'nodePreDelete', 'ms_args', 'preDelete'))
	_connectionMap.update(_ConnectionDef('objectPostDelete', 'nodePostDelete', function = 'postDelete'))
	_connectionMap.update(_ConnectionDef('objectParented', 'nodeLinked', 'ms_args', 'dispatchObject'))
	_connectionMap.update(_ConnectionDef('objectUnparented', 'nodeUnlinked', 'ms_args', 'dispatchObject'))
	_connectionMap.update(_ConnectionDef('viewportRedrawn', '', function='dispatchFunction', callbackType=_ConnectionType.Viewport))

	def __init__(self):
		super(StudiomaxApplication, self).__init__()
		self._sceneMergeFinishedTimer = QTimer(self)
		self._sceneMergeFinishedTimer.setSingleShot(True)
		self._sceneMergeFinishedTimer.timeout.connect(self._sceneMergeFinishedTimeout)
		# Variable used to prevent emiting signals when a file is being opened.
		self._openingScene = False
		self._disconnectNames = set()

	def _connectStudiomaxSignal(self, connDef, cross3dSignal):
		"""
			\remarks	Responsible for connecting a signal to studiomax
		"""
		# store the maxscript methods needed
		if connDef.callbackType == _ConnectionType.Viewport:
			signal = _STUDIOMAX_VIEWPORT_TEMPLATE % {
				'cls':'dispatch',
				'function':connDef.function,
				'signal':cross3dSignal
			}
			# Ensure that if the old signal existed it is removed before redefining it.
			# If function is undefined it will do nothing
			mxs.unregisterRedrawViewsCallback(getattr(mxs, 'blurfn_%s' % cross3dSignal))
			mxs.execute(signal)
			mxs.registerRedrawViewsCallback(getattr(mxs, 'blurfn_%s' % cross3dSignal))
		else:
			# Connect the callback
			self._addCallback(connDef, cross3dSignal)
			# Connect any associated callbacks using a diffrent ID name allows us to disconnect
			# this signal without affecting any direct connections to the associated callbacks
			for reqDef in connDef.associated:
				self._addCallback(reqDef, reqDef.signal, 'cross3dcallbacks_{}'.format(connDef.callback))

	def _addCallback(self, connDef, cross3dSignal, callbackName='cross3dcallbacks'):
		if connDef.arguments:
			script = _STUDIOMAX_CALLBACK_TEMPLATE % {
				'cls':connDef.cls,
				'function':connDef.function,