###
 * coffeescript-ui - Coffeescript User Interface System (CUI)
 * Copyright (c) 2013 - 2016 Programmfabrik GmbH
 * MIT Licence
 * https://github.com/programmfabrik/coffeescript-ui, http://www.coffeescript-ui.org
###

# Events Inter-"Process"-Communication for CUI
#
#
#
# @example How to listen to and trigger events
#
# Event.listen
#   	type: [ "click", "dblclick" ]
#     node: jQuery Element or CUI DOM Element
#     call: (ev, info) ->
#     selector: jQuery like path selector to filter events
#
#
# Event.trigger
#   	type: "content-resize"
#     node: jQuery Element or CUI DOM Element
#     bubble: set to yes if event should bubble up or down the DOM tree
#     info: info Map, contains eventsEvent for DOMElements and
#           the internal "waits" queue
#
# Event.ignore
#   	type: "<type>"
#     node: jQuery or DOM Element
#
#
# CUIEvents bound to a node will be checked for the existance in the
# DOM tree prio execution. If they don't exist (after they
# have been inserted), the CUI.Listener will be deleted.
#
# All events need to be registered or a warning is output at the console.
#
# CUI.Events.registerEvent(options)
#    options are the default options for the event
#  	type: <type>
#    bubble: true|false
#
#
class CUI.Events extends CUI.Element

	@defaults:
		maxWait: 1500

	@__listeners = []
	@__eventRegistry = {}

	@__getListenersForNode: (node) ->
		if node == document or node == window
			@__listeners
		else
			CUI.dom.data(node, "listeners")

	@__registerListener: (listener) ->
		CUI.util.assert(listener instanceof CUI.Listener, "CUI.Events.__registerListener", "listener needs to be instance of Listener", listener: listener)

		node = listener.getNode()
		listeners = @__getListenersForNode(node)
		if not listeners
			listeners = []
			CUI.dom.data(node, "listeners", listeners)

		listeners.push(listener)

		if node instanceof HTMLElement
			node.setAttribute("data-cui-listeners", "")
		@

	@__getActiveListeners: (doc=document) ->
		if doc == document
			listeners = @__listeners.slice(0)
		else
			listeners = []
			if CUI.dom.matches(doc, '[data-cui-listeners]')
				listeners.push.apply(listeners, CUI.dom.data(doc, "listeners"))

		for el in CUI.dom.matchSelector(doc, "[data-cui-listeners]")
			listeners.push.apply(listeners, CUI.dom.data(el, "listeners"))
		listeners

	@unregisterListener: (listener) ->
		node = listener.getNode()
		arr = @__getListenersForNode(node)
		CUI.util.assert(arr, "CUI.Events.unregisterListeners", "Listeners not found for node.", node: node, listener: listener)
		# console.error "unregistring listeenr", listener.getUniqueId()
		CUI.util.removeFromArray(listener, arr)
		if arr.length == 0 and node instanceof HTMLElement
			node.removeAttribute("cui-events-listener-element")
			CUI.dom.removeData(node, "listeners")
			# console.debug "removing listeners from node", node[0]
		@

	# wait for an event on a node
	@wait: (_opts) ->
		opts = CUI.Element.readOpts _opts, "CUI.Events.wait",
			# event type
			type:
				mandatory: true
				check: String
			node:
				mandatory: true
				check: (v) ->
					CUI.dom.isNode(v)
			# optionally wait for a timeout
			# if set to <= 0, wait forever
			maxWait:
				default: CUI.defaults.class.Events.defaults.maxWait
				check: (v) ->
					i = parseInt(v)
					if isNaN(i)
						false
					else if i == -1
						true
					else if i >= 0
						true
					else
						false

		dfrs = []
		listeners = []

		_node = CUI.dom.getNode(opts.node)

		dfr = new CUI.Deferred()
		listeners.push CUI.Events.listen
			type: opts.type
			node: _node
			call: ->
				dfr.resolve()
				return

		dfrs.push(dfr)

		master_dfr = new CUI.Deferred()
		master_dfr.always ->
			for listener in listeners
				listener.destroy()
			return

		CUI.when(dfrs)
		.fail ->
			master_dfr.reject()
		.done ->
			master_dfr.resolve()

		if opts.maxWait >= 0
			CUI.setTimeout
				ms: opts.maxWait
				call: ->
					for dfr in dfrs
						if dfr.state() == "pending"
							dfr.reject()
					return

		master_dfr.promise()


	# register a listener
	# @param listener PlainObject or CUI.Listener
	@listen: (_listener) ->
		listener = CUI.Listener.require(_listener, "CUI.Events.listen")

		@__registerListener(listener)

		listener


	@trigger: (_event) ->
		event = CUI.Event.require(_event, "CUI.Events.trigger")

		# allwo event calls to return a promise
		# wait for all promises before
		# returning from this methos
		#
		info = event.getInfo()
		waits = []
		info.__waits = waits

		# use the standard event system for this kind of events
		bubble = event.isBubble()
		sink = event.isSink()
		exclude = event.isExcludeSelf()
		node = event.getNode()

		# console.debug "trigger event", event.getType(), info, event.getUniqueId(), bubble, sink, exclude, event.isInDOM()
		if bubble or not event.isInDOM()
			event.dispatch()
		else
			# dispatch sets a native event and through that, the event
			# gains a target. without the dispatch, we need to set
			# the target for this event
			event.setTarget(node)

		if exclude and not bubble and not sink
			CUI.util.assert(false, "CUI.Events.trigger", "Unable to trigger event with bubble == false, sink == false and exclude_self == true.", event: event)

		if sink or (not sink and not bubble and not exclude and event.isInDOM())
			# if event.getType() == "toolbox"
			# 	console.debug "sink event...", event.getType(), event.getNode()
			triggerListeners = []
			for listener, idx in @__getActiveListeners()
				if event.getType() not in listener.getTypes()
					continue

				if listener.matchesEvent(event) == null
					continue

				# console.error "triggering...", event.getType()

				triggerListeners.push(listener)

			# if triggerListeners.length == 0
			# 	console.warn("CUI.Events.trigger: No listeners found for Event #{event.getType()}.", event: event, activeListeners: @active())

			triggerListeners.sort (a, b) ->
				CUI.util.compareIndex(a.getDepthFromLastMatchedEvent(), b.getDepthFromLastMatchedEvent())

			stopNodes = []
			ev_node = event.getNode()

			for listener in triggerListeners
				listener_node = listener.getNode()
				# console.debug "listener:", listener, listener.getDepthFromLastMatchedEvent()

				if listener_node and stopNodes.length > 0
					listener_node_parents = CUI.dom.parents(listener_node)
					skip = false
					for stopNode in stopNodes
						for listener_node_parent in listener_node_parents
							if listener_node_parent == stopNode
								skip = true
								break

					if skip
						# node is below the stop node, skip
						continue

				event.setCurrentTarget(listener_node)
				listener.handleEvent(event, "sink")

				if event.isImmediatePropagationStopped()
					# console.debug "immediate stopped!"
					break

				# add to stop nodes if the depth is at least 0 meaning that
				# the listener has a node
				if event.isPropagationStopped() and listener_node
					# console.debug "adding stop node", listener_node[0]
					stopNodes.push(listener_node)

		return CUI.when(waits)


	@ignore: (filter, doc=document) -> # , debug=false) ->
		# console.debug "CUI.Events.ignore", filter, filter.instance?.getUniqueId?()
		for listener in @__getActiveListeners(doc)
			if not filter or CUI.util.isEmptyObject(filter) or listener.matchesFilter(filter)
				# if debug
				# 	console.debug("CUI.Events.ignore: ignoring listener:", listener.getNode(), DOM.data(listener.getNode()).listeners?.length, filter.instance?.getUniqueId?())
				listener.destroy()
		@

	@dump: (filter={}) ->
		for listener in @__getActiveListeners()
			if CUI.util.isEmptyObject(filter) or listener.matchesFilter(filter)
				console.debug("Listener", listener.getTypes(), (if listener.getNode() then "NODE" else "-"), listener)
		@

	@dumpTopLevel: ->
		for listener in @__listeners
			console.debug("Listener [document, window]", listener.getTypes(), listener.getInstance())

		for listener in CUI.dom.data(document.documentElement, "listeners")
			console.debug("Listener [document.documentElement]", listener.getTypes(), listener.getInstance(), listener)
		@

	@hasEventType: (type) ->
		!!@__eventRegistry[type]

	# returns event info by type
	@getEventType: (type) ->
		ev = @__eventRegistry[type]
		CUI.util.assert(ev, "Unknown event type \"#{type}\". Use CUI.Events.registerEvent to register this type.")
		return ev

	@getEventTypeAliases: (type) ->
		@getEventType(type).alias or [type]

	@registerEvent: (event, allow_array=true) ->
		if not CUI.util.isArray(event.type) or not allow_array
			CUI.util.assert(CUI.util.isString(event?.type) and event.type.length > 0, "CUI.Events.registerEvent", "event.type must be String.", event: event)

		register_other_type = (_type) =>
			_event = CUI.util.copyObject(event, true)
			_event.type = _type
			@registerEvent(_event, false)

		if CUI.util.isArray(event.type)
			for type in event.type
				register_other_type(type)
		else
			if event.hasOwnProperty("DOMEvent")
				console.error("event.DOMEvent is obsolete")
				delete(event.DOMEvent)

			if event.hasOwnProperty("CUIEvent")
				console.error("event.CUIEvent is obsolete")
				delete(event.CUIEvent)

			@__eventRegistry[event.type] = event

			if event.alias
				for type in event.alias
					if not @__eventRegistry[type]
						register_other_type(type)
		@

	@__init: ->
		defaults =
			BrowserEvents:
				bubble: true

			DOM:
				bubble: true

			CUI:
				eventClass: CUI.CUIEvent
				sink: true

			KeyboardEvents:
				eventClass: CUI.KeyboardEvent
				bubble: true

			MouseEvents:
				eventClass: CUI.MouseEvent
				bubble: true

			TouchEvents:
				eventClass: CUI.TouchEvent
				bubble: true


		for block, events of {
			MouseEvents:
				mousemove: {}
				mouseover: {}
				mouseout: {}
				mouseleave: {}
				mouseenter: {}
				wheel:
					eventClass: CUI.WheelEvent
					bubble: false
				mousedown: {}
				mouseup: {}
				click: {}
				dblclick: {}
				contextmenu: {}

			TouchEvents:
				touchstart: {}
				touchend: {}
				touchmove: {}
				touchcancel: {}
				touchforchange: {}
				gesturestart: {}
				gestureend: {}
				gesturechange: {}

			KeyboardEvents:
				input:
					bubble: false
				keyup: {}
				keydown: {}
				keypress: {}

			BrowserEvents:
				beforeunload: {}
				unload: {}
				load: {}
				error: {}
				close: {}
				popstate: {}
				dragstart: {}
				dragleave: {}
				dragenter: {}
				message: {}
				fullscreenchange:
					alias: "fullscreenchange mozfullscreenchange webkitfullscreenchange MSFullscreenChange".split(" ")
				hashchange:
					bubble: false
				change:
					bubble: false
				focus:
					bubble: false
				blur:
					bubble: false
				paste:
					bubble: false
				dragover:
					bubble: false
				drop:
					bubble: false
				scroll:
					bubble: false
				selectstart:
					bubble: false
				animationstart:
					alias: "animationstart MSAnimationStart webkitAnimationStart".split(" ")
					bubble: false
				animationend:
					alias: "animationend MSAnimationEnd webkitAnimationEnd".split(" ")
					bubble: false
				transitionend:
					alias: "transitionend webkitTransitionEnd MSTransitionEnd".split(" ")
					bubble: false
				resize:
					bubble: false

			DOM:
				"content-resize":
					eventClass: CUI.CUIEvent

			CUI:
				# "load_server": {}
				# "unload_server": {}
				"viewport-resize": {}
		}
			for type, ev of events
				CUI.util.mergeMap(ev, defaults[block])
				ev.type = type
				@registerEvent(ev)

CUI.Events.__init()
CUI.defaults.class.Events = CUI.Events
