class Projection

  constructor: (@_eventric) ->
    @log = @_eventric.log
    @_handlerFunctions    = {}
    @_projectionInstances = {}
    @_domainEventsApplied = {}


  initializeInstance: (projectionName, Projection, params, @_context) ->
    new Promise (resolve, reject) =>

      if typeof Projection is 'function'
        projection = new Projection
      else
        projection = Projection

      if @_context._di
        for diName, diFn of @_context._di
          projection[diName] = diFn

      projectionId = @_eventric.generateUid()

      aggregateId = null
      projection.$subscribeHandlersWithAggregateId = (_aggregateId) ->
        aggregateId = _aggregateId


      @log.debug "[#{@_context.name}] Clearing ProjectionStores #{projection.stores} of #{projectionName}"
      @_clearProjectionStores projection.stores, projectionName
      .then =>
        @log.debug "[#{@_context.name}] Finished clearing ProjectionStores of #{projectionName}"
        @_injectStoresIntoProjection projectionName, projection
      .then =>
        @_callInitializeOnProjection projectionName, projection, params
      .then =>
        @log.debug "[#{@_context.name}] Replaying DomainEvents against Projection #{projectionName}"
        @_parseEventNamesFromProjection projection
      .then (eventNames) =>
        @_applyDomainEventsFromStoreToProjection projectionId, projection, eventNames, aggregateId
      .then (eventNames) =>
        @log.debug "[#{@_context.name}] Finished Replaying DomainEvents against Projection #{projectionName}"
        @_subscribeProjectionToDomainEvents projectionId, projectionName, projection, eventNames, aggregateId
      .then =>
        @_projectionInstances[projectionId] = projection
        event =
          id: projectionId
          projection: projection

        @_context.publish "projection:#{projectionName}:initialized", event
        @_context.publish "projection:#{projectionId}:initialized", event

        resolve projectionId

      .catch (err) ->
        reject err


  _callInitializeOnProjection: (projectionName, projection, params) ->
    new Promise (resolve, reject) =>
      if not projection.initialize
        @log.debug "[#{@_context.name}] No initialize function on Projection #{projectionName} given, skipping"
        return resolve projection

      @log.debug "[#{@_context.name}] Calling initialize on Projection #{projectionName}"
      projection.initialize params, =>
        @log.debug "[#{@_context.name}] Finished initialize call on Projection #{projectionName}"
        resolve projection


  _injectStoresIntoProjection: (projectionName, projection) ->
    new Promise (resolve, reject) =>
      if not projection.stores
        return resolve()

      projection["$store"] ?= {}
      @_eventric.eachSeries projection.stores, (projectionStoreName, next) =>
        @log.debug "[#{@_context.name}] Injecting ProjectionStore #{projectionStoreName} into Projection #{projectionName}"
        @_context.getProjectionStore projectionStoreName, projectionName
        .then (projectionStore) =>
          if projectionStore
            projection["$store"][projectionStoreName] = projectionStore
            @log.debug "[#{@_context.name}] Finished Injecting ProjectionStore #{projectionStoreName} into Projection #{projectionName}"
            next()

        .catch (err) ->
          next err

      , (err) ->
        return reject err if err
        resolve()


  _clearProjectionStores: (projectionStores, projectionName) ->
    new Promise (resolve, reject) =>
      if not projectionStores
        return resolve()

      @_eventric.eachSeries projectionStores, (projectionStoreName, next) =>
        @log.debug "[#{@_context.name}] Clearing ProjectionStore #{projectionStoreName} for #{projectionName}"
        @_context.clearProjectionStore projectionStoreName, projectionName
        .then =>
          @log.debug "[#{@_context.name}] Finished clearing ProjectionStore #{projectionStoreName} for #{projectionName}"
          next()
        .catch (err) ->
          next err
      , (err) ->
        resolve()


  _parseEventNamesFromProjection: (projection) ->
    new Promise (resolve, reject) =>
      eventNames = []
      for key, value of projection
        if (key.indexOf 'handle') is 0 and (typeof value is 'function')
          eventName = key.replace /^handle/, ''
          eventNames.push eventName
      resolve eventNames


  _applyDomainEventsFromStoreToProjection: (projectionId, projection, eventNames, aggregateId) ->
    new Promise (resolve, reject) =>
      @_domainEventsApplied[projectionId] = {}

      if aggregateId
        findEvents = @_context.findDomainEventsByNameAndAggregateId eventNames, aggregateId
      else
        findEvents = @_context.findDomainEventsByName eventNames

      findEvents
      .then (domainEvents) =>
        if not domainEvents or domainEvents.length is 0
          return resolve eventNames

        @_eventric.eachSeries domainEvents, (domainEvent, next) =>
          @_applyDomainEventToProjection domainEvent, projection
          .then =>
            @_domainEventsApplied[projectionId][domainEvent.id] = true
            next()

        , (err) ->
          return reject err if err
          resolve eventNames

      .catch (err) ->
        reject err


  _subscribeProjectionToDomainEvents: (projectionId, projectionName, projection, eventNames, aggregateId) ->
    new Promise (resolve, reject) =>
      domainEventHandler = (domainEvent, done = ->) =>
        if @_domainEventsApplied[projectionId][domainEvent.id]
          return done()

        @_applyDomainEventToProjection domainEvent, projection
        .then =>
          @_domainEventsApplied[projectionId][domainEvent.id] = true
          event =
            id: projectionId
            projection: projection
            domainEvent: domainEvent
          @_context.publish "projection:#{projectionName}:changed", event
          @_context.publish "projection:#{projectionId}:changed", event
          done()

        .catch (err) ->
          done err

      @_eventric.eachSeries eventNames, (eventName, done) =>
        if aggregateId
          subscriberPromise = @_context.subscribeToDomainEventWithAggregateId eventName, aggregateId, domainEventHandler
        else
          subscriberPromise = @_context.subscribeToDomainEvent eventName, domainEventHandler
        subscriberPromise
        .then (subscriberId) =>
          @_handlerFunctions[projectionId] ?= []
          @_handlerFunctions[projectionId].push subscriberId
          done()
        .catch (err) ->
          done err
      , (err) ->
        return reject err if err
        resolve()


  _applyDomainEventToProjection: (domainEvent, projection) =>
    new Promise (resolve, reject) =>
      if !projection["handle#{domainEvent.name}"]
        @log.debug "Tried to apply DomainEvent '#{domainEvent.name}' to Projection without a matching handle method"
        resolve()
        return

      handleDomainEvent = projection["handle#{domainEvent.name}"] domainEvent
      Promise.all [handleDomainEvent]
      .then ([result]) ->
        resolve result


  getInstance: (projectionId) ->
    @_projectionInstances[projectionId]


  destroyInstance: (projectionId) ->
    if not @_handlerFunctions[projectionId]
      return @_eventric.log.error 'Missing attribute projectionId'

    unsubscribePromises = []
    for subscriberId in @_handlerFunctions[projectionId]
      unsubscribePromises.push @_context.unsubscribeFromDomainEvent subscriberId

    delete @_handlerFunctions[projectionId]
    delete @_projectionInstances[projectionId]

    Promise.all unsubscribePromises


module.exports = Projection