Events = require('events')

class Freakset extends Events.EventEmitter

  Freakset.Actor = {}
  Freakset.Actor.Group  = require('./freakset/actor/group').Actor
  Freakset.Actor.Guard  = require('./freakset/actor/guard').Actor
  Freakset.Actor.Rescue = require('./freakset/actor/rescue').Actor
  Freakset.Actor.Step   = require('./freakset/actor/step').Actor

  @build: (opts) ->
    new Freakset(opts ||= {})

  constructor: (opts) ->
    opts ||= {}
    @klass            = Freakset
    @stack            = opts.stack ||= []
    @store            = opts.store ||= {}
    @outerFreakset    = opts.outerFreakset ||= undefined
    @resolvedCallback = opts.resolvedCallback ||= undefined
    @runInParallel    = opts.runInParallel ||= undefined

    @running = no
    @modificationIndex = 0
    @innerFreaksets = []

  step: (scope, opts, fn) ->
   Freakset.Actor.Step.build @, scope, opts, fn

  group: (scope, opts, fn) ->
    Freakset.Actor.Group.build @, scope, opts, fn

  rescue: (scope, errFn, opts, fn) ->
    Freakset.Actor.Rescue.build @, scope, errFn, opts, fn

  guard: (scope, opts, fn) ->
    Freakset.Actor.Guard.build @, scope, opts, fn

  isRunning: ->
    return true if @running == yes

    if @innerFreaksets.length != 0
      for innerFreakset in @innerFreaksets
        if innerFreakset.isRunning()
          return true

    return false

  root: ->
    if @outerFreakset? then @outerFreakset.root() else @

  next: ->
    return false if @root().canceled?
    @running = yes

    actor = @stack.shift()

    unless actor?
      if @runInParallel?
        if @parallelStepCount? && @parallelStepCount == 0
          return @finish()
        else
          return
      else
        return @finish()

    @emit("beforeRunningActor", @, actor)
    @emit("before:#{actor.scope}")

    switch actor.type
      when 'group'  then @runActor(Freakset.Actor.Group.run, actor)
      when 'guard'  then @runActor(Freakset.Actor.Guard.run, actor)
      when 'rescue' then @runActor(Freakset.Actor.Rescue.run, actor)
      when 'step'   then @runActor(Freakset.Actor.Step.run, actor)

    @emit("afterRunningActor", @, actor)
    @emit("after:#{actor.scope}")


  commit: ->
    # Decrease the finished step count in parallel mode
    if @runInParallel? && @parallelStepCount
      @parallelStepCount -= 1

    @next()

  compile: (store, resolvedCallback) ->
    # Attach resolvedCallback
    if resolvedCallback?
      @resolvedCallback = resolvedCallback

    # Prepare store if its undefined
    store ||= {}

    set = @buildAndRunFreakset(stack: @stack, store: store, resolvedCallback: resolvedCallback)
    return set

  cancel: ->
    @root().canceled = yes
    @root().running = no
    @root().resolveRoot()

  finish: ->
    unless @root().canceled?
      @running = no
      @resolveRoot()
      @outerFreakset.commit() if @outerFreakset
      return true

  resolveRoot: ->
    root = @root()
    if !root.isRunning()
      @emit('finish', root)
      root.resolvedCallback.call(root) if root.resolvedCallback?

  hasActor: (scope) ->
    (_.detect @stack, (actor) -> actor.scope == scope)?

  # ## Internal Helper


  buildAndRunFreakset: (opts) ->
    # Build the Freakset by copying the stack and passing the store
    opts.stack = opts.stack.slice() if opts.stack?
    freakset = Freakset.build(opts)

    @copyEventListeners(freakset)

    # Only emit 'compile' if the root-set is run
    if freakset == freakset.root()
      @emit('compile', freakset)

    freakset.next()
    return freakset

  copyEventListeners: (freakset) ->
    if @_events?
      for event of @_events
        for listener in @listeners(event)
          freakset.on(event, listener)

  runActor: (runner, actor) ->
    chainableEventFns = []

    @modificationIndex = 0

    if @_events?
      chainableEventFns = @findChainableEventFns('aroundRunningActor')
      chainableEventFns = chainableEventFns.concat(@findChainableEventFns("around:#{actor.scope}"))

    if chainableEventFns.length > 0
      @runEventFnChain(chainableEventFns, runner, actor)
    else
      runner(@, actor)

  findChainableEventFns: (eventName) ->
    if @_events[eventName]? && (typeof @_events[eventName] == 'function' || @_events[eventName].length > 0)
      if typeof @_events[eventName] == 'function'
        return [@_events[eventName]]
      else
        return _.clone(@_events[eventName])
    else
      return []

  runEventFnChain: (eventFns, runner, actor) ->
    eventFn = eventFns.shift()
    eventFn @, actor, =>
      if eventFns.length > 0
        @runEventFnChain(eventFns, runner, actor)
      else
        runner(@, actor)

  modifyStack: (set, actor, opts) ->
    opts ||= {}

    if !opts.insertAt && !opts.insertAtTop && !opts.insertBefore && !opts.insertAfter && !opts.replace && @isRunning()
      # Default insert position for runtime modifications of freaksets
      opts.insertAt = @modificationIndex
      @modificationIndex += 1

    if opts.insertAt? && typeof opts.insertAt is 'number'
      set.stack.splice(opts.insertAt, 0, actor)

    else if opts.insertAtTop? && opts.insertAtTop is yes
      # insert at top
      set.stack.unshift actor

    else if opts.insertBefore? && typeof opts.insertBefore is "string"
      # insert before
      [index, set] = @findActorByScope(set, opts.insertBefore)
      if index != -1
        set.stack.splice(index, 0, actor)

    else if opts.insertAfter? && typeof opts.insertAfter is "string"
      # insert after
      [index, set] = @findActorByScope(set, opts.insertAfter)
      if index != -1
        set.stack.splice(index + 1, 0, actor)

    else if opts.replace? && typeof opts.replace is "string"
      # replace
      [index, set] = @findActorByScope(set, opts.replace)
      if index != -1
        set.stack.splice(index, 1, actor)

    else
      set.stack.push actor

  findActorByScope: (set = @, scope) ->
    actor = _.detect set.stack, (actor) -> actor.scope == scope

    if actor?
      return [set.stack.indexOf(actor), set]
    else
      for innerSet in set.innerFreaksets
        [index, set] = @findActorByScope(innerSet, scope)
        return [index, set] if index != -1

      return [-1, null]


# Export the Build function
exports.Freakset = Freakset
