###
coffee -cb /opt/node/bb/dev/node_modules/atbar/atbar.cof
###

atbarVersion = "0.4.1"

_ = 	require 'underscore'
_.mixin require 'underscore.string'
_.mixin require 'underscore.inspector'


# represents the root atbar wrapper function or a nested @_ wrapper function
class AtbarNode
	
# --------------------------------------------------
#             CONSTRUCTOR
# --------------------------------------------------

	# Root has @parent == null and @root == @
	# atbarArgs are args of atbar or @_ wrapper function
	constructor: (@parent, @label, @options, @func) -> 
		@root = @parent?.root ? @							# root is copied into every node
		if @parent then for own attr of @parent				# all _attrs are inherited
			if attr[0] == '_' then @[attr] = @parent[attr]
		@children = []
		@activeCallbacks = {}
		@callbackResults = []
		if @parent 
			if @parent.id isnt 'root' then @id = @parent.id + '-' + label
			else @id = label
		else @id = 'root'


# --------------------------------------------------
#             WRAPPER  @_
# --------------------------------------------------

	# just save this atbar wrapper for later use
	_: (args...) ->
		label = @children.length; options = null
		switch args.length
			when 1 then func = args[0]
			when 2 
				[arg1, func] = args
				switch typeof arg1 
					when 'string' then label = arg1
					when 'object' then options = arg1; if options.label then label = options.label
					else @execErr 'first @_ arg has invalid type'; return
			else @execErr '@_ has wrong number of arguments'; return
		if typeof func != 'function' then @execErr 'last @_ arg not a function'; return
		if label in ['atbar', 'root', 'first', 'loop', 'next', 'throw', 'enter', 'exit']
			@execErr label + ' is a reserved word'
			return
		@children.push new AtbarNode @, label, options, func


# --------------------------------------------------
#		CALLBACK GENERATOR @$()
# --------------------------------------------------

	# callback generator function @$
	$: (args...) -> 
		level = 0; strings = []; data = null 
		for arg in args then switch typeof arg 
			when 'number' then level = arg
			when 'string' then strings.unshift arg
			when 'object' then data = arg
			else @execErr 'invalid arg type for callback generator: ' + typeof arg; return ->
		[destSpec, srcLabel] = strings
		
		# first is same as enter except one level up
		if destSpec == 'first' then level++
		
		# find node that is level number of ancestors up
		levelNode = @; for i in [0...level] then levelNode = levelNode?.parent
		if not levelNode then @execErr 'callback level param above top level'; return ->
		
		# find target node relative to levelNode using alogorithm based on destSpec
		destNode = levelNode; destSpec ?= 'next'
		if destSpec == 'throw' then destSpec = 'catch'
		if destSpec != 'loop' then switch destSpec
			when 'root' then destNode = @root.children[0]
			when 'enter', 'first'
				if not (firstChild = destNode.children[0]) then return ->
				destNode = firstChild
			when 'next' then destNode = destNode.nextNode()
			when 'exit' then destNode = @root
			else while (destNode = destNode.nextNode()) 
				if destNode.label == destSpec or destNode == @root then break

		# finished scanning with no match?
		if destNode == @root 
			node = @
			if destSpec == 'catch' 
				return do (node) -> (err) -> node.exit err, null
			else if destSpec not in ['next', 'root', 'first', 'loop', 'enter', 'exit']
				@execErr 'Destination for ' + destSpec + ' not found'; return ->

		#register callback being generated
		seqNum = @root.nextCbSeqNum++
		callbackInfo = 
			seqNum:seqNum, srcNode:@, destNode:destNode
			level:level, srcLabel:srcLabel, destSpec:destSpec, data:data
		if not destNode.options?.nojoin
			destNode.activeCallbacks[seqNum] = callbackInfo
			@root.totalActiveCallbacks++
		
		# generate and return callback function
		do (callbackInfo) -> 
			(cbArgs...) -> callbackInfo.cbArgs = cbArgs; callbackInfo.destNode.call callbackInfo


# --------------------------------------------------
#		CALL BACK TO DESTINATION
# --------------------------------------------------

# this is usually called by the generated callback
	call: (callbackInfo) ->
	
		{seqNum, srcNode, srcLabel, destNode, destSpec, cbArgs, data} = callbackInfo
		
		# ignore callbacks after atbar is finished
		if @root.finished 
			if @_trace then @printNode srcNode.id, destSpec, true, false, 
				'callback after atbar finished'
			return
		if not @options?.nojoin
			# make sure callback was expected
			if not @activeCallbacks[seqNum]
				if @_trace then @printNode srcNode.id, destSpec, true, false, 
					'unregistered or duplicate callback'
				return
				
			# we have a successful callback, un-register it and save results
			@root.totalActiveCallbacks--
			delete @activeCallbacks[seqNum]
		
		# clear callback results if starting new sequence
		if @callbacksComplete then @callbackResults = []	
			
		# results may be accessed directly from javascript
		@data = data
		@callbackResults.push callbackInfo
		@callbacksComplete = (_.size(@activeCallbacks) == 0)
		
		# is this an exit callback with nothing left to do?  Then this atbar is done successfully.
		if @ == @root and destSpec == 'exit'
			@exit null, @callbackResults
			return
			
		# if func being called has a param labeled _throw and the arg for that is non-empty,
		#    then throw an exception
		if cbArgs.length
			if not @throwParamIndexes
				@throwParamIndexes = []
				for param, idx in @func.toString().match(/^\s*function\s*\(([^)]*)\)/i)[1].split ','
					if _.trim(param).toLowerCase() == '_throw' then @throwParamIndexes.push idx
			for tpi in @throwParamIndexes 
				if cbArgs[tpi] then @throw cbArgs[tpi]; return
				
		# no callbacks will be honored until last one comes in
		if not @callbacksComplete
			if @_trace then @printNode srcNode.id, destSpec, true, false, 
				'Callback ignored, waiting for ' + _.size(@activeCallbacks) + ' more'
			return
			
		# execution flowed off of the end of the top level -- exit atbar if nothing running
		if @ == @root and destSpec != 'loop' 
			@exitIfIdle null, cbArgs, srcNode.id; 
			return
		
		# tracing option
		if @_trace 
			if srcLabel == 'atbar' 
				console.log "Starting atbar trace (Version #{atbarVersion}) ..."
			else 
				@printNode srcNode.id, destSpec, true, false

		# call the wrapped func so it will gather @_'s and let @$'s generate callbacks
		try
			@func.apply @, cbArgs
		catch e
			if @root.finished then throw e
			@$('throw')(e)

		# start the first occurrence of @_ inside the wrapped func
		# if no @_ then this execution chain dies
		if @children.length then @$('enter')()


# --------------------------------------------------
#       MISC
# --------------------------------------------------
		
	# find next sibling node, if no next sibling then return next node of parent
	# if no node can be found at all, return root
	nextNode: ->
		node = @
		while node
			siblings = node.parent?.children
			if siblings then for sibling, idx in siblings
				if sibling == node and (nextNode = siblings[idx+1]) then return nextNode
			if (parent = node.parent) then return parent.nextNode()
			else break
		return @root          

	# exit entire atbar routine and callback if requested
	exit: (err, resp) -> 
		@root.finished = true
		@listing(); 
		if @root.exitCallback then @root.exitCallback err, resp
		else if err 
			throw err		# if error and no callback to use, throw a real exception
		
	exitIfIdle: (err, resp, id) ->
		if @root.totalActiveCallbacks == 0 
			if @_trace and id
				@printNode id, null, true, false, 'Nothing to do, exiting ...'
			@exit err, resp

	# convenience method to do fake throw
	throw: (e) -> @$('throw')(e)

	# general execution error
	execErr: (msg) -> 
		msg = "atbar execution error at #{@id}: #{msg}"
		@$('throw') msg


# --------------------------------------------------
#       DEBUGGING
# --------------------------------------------------

	# debugging log
	log: (args...) -> if @_debug # and @label != 'root'
		msg  = ''; if typeof args[0] == 'string' then msg = args.shift()
		dump = ''; for arg in args then dump += '\n' + _.inspect arg, 1
		console.log "\natbar log at #{@id}: " + msg + dump

	# dummy function to make it easy to disable log call by changing @logx to @logx
	logx: ->

	# dump node as one line of listing (trace = false) or trace (trace = true)
	printNode: (src = '', label, trace, returnText, comment) ->
		if comment then code = comment
		else
			code = @func.toString().replace(/\s+/g,' ').replace /^\s*function\s*/, ''
			if code.length > 80 then code = code[0...80] + ' ...'
		src  = src + (if label then '(' + label + ')' else '')
		if trace 
			time = (+(new Date) - @root.startTime)/1e3
			line = _.sprintf '%8.3f %-3d %12s -> %-7s %s', 
					time, @root.totalActiveCallbacks, src, @id, code
		else 
			line = _.sprintf '%-10s %s', @id, code
		if returnText then line + '\n'
		else console.log line
			
	treeText: ->
		text = '';  
		treeTextRecurse = (node, depth) ->
			if node.label != 'root'
				pad = ''; for i in [0...depth] then pad = '   ' + pad
				text += pad + node.printNode null, null, false, true
			for child in node.children then treeTextRecurse child, depth+1
			null
		treeTextRecurse @root, 1
		text
		
	listing: -> 
		if @_listing
			console.log '\nListing of visited atbar functions ...'
			console.log @treeText()
		

# --------------------------------------------------
#       EXPORTS
# --------------------------------------------------

exports.run = (args...) ->
	options = _.trim(exports.options ? '').split /\W+/; funcs = []
	for arg in args
		switch typeof arg
			when 'function' then funcs.unshift arg
			when 'string'   then options.push arg
	if funcs.length > 1 then func = funcs[1]; exitCallback = funcs[0]
	else func = funcs[0]
	if func
		root = new AtbarNode null, 'root', null, func
		root.nextCbSeqNum = 0
		root.totalActiveCallbacks = 0
		root.exitCallback = exitCallback
		for opt in options then if opt then root['_' + opt] = true
		root.startTime = +(new Date)
		root.$('atbar', 'loop')()

