_ = require 'lodash'
eventric = require 'eventric'
fakePromise = require './fake_promise'
inmemoryRemote = require 'eventric-remote-inmemory'

# TODO: Find a better way to solve the dependency to these eventric internal components
DomainEvent = require 'eventric/src/domain_event'
domainEventIdGenerator = require 'eventric/src/aggregate/domain_event_id_generator'

class RemoteFactory

  wiredRemote: (contextName, domainEvents) ->
    wiredRemote = eventric.remote contextName
    wiredRemote._mostCurrentEmitOperation = fakePromise.resolve()
    wiredRemote._domainEvents = []
    wiredRemote._subscriberIds = []
    wiredRemote._commandStubs = []
    wiredRemote._context = eventric.context contextName
    wiredRemote._context.defineDomainEvents domainEvents
    wiredRemote.setClient inmemoryRemote.client

    wiredRemote.$populateWithDomainEvent = (domainEventName, aggregateId, domainEventPayload) ->
      @_domainEvents.push @_createDomainEvent domainEventName, aggregateId, domainEventPayload


    wiredRemote.$emitDomainEvent = (domainEventName, aggregateId, domainEventPayload) ->
      domainEvent = @_createDomainEvent domainEventName, aggregateId, domainEventPayload
      @_domainEvents.push domainEvent
      endpoint = inmemoryRemote.endpoint
      @_mostCurrentEmitOperation = @_mostCurrentEmitOperation.then ->
        contextEventPublish = endpoint.publish contextName, domainEvent
        contextAndNameEventPublish = endpoint.publish contextName, domainEvent.name, domainEvent
        if domainEvent.aggregate
          fullEventNamePublish = endpoint.publish contextName, domainEvent.name, domainEvent.aggregate.id, domainEvent
          Promise.all([contextEventPublish, contextAndNameEventPublish, fullEventNamePublish])
        else
          Promise.all([contextEventPublish, contextAndNameEventPublish])


    wiredRemote.$waitForEmitDomainEvent = ->
      wiredRemote._mostCurrentEmitOperation


    wiredRemote._createDomainEvent = (domainEventName, aggregateId, domainEventConstructorParams) ->

      DomainEventPayloadConstructor = wiredRemote._context.getDomainEventPayloadConstructor domainEventName

      if !DomainEventPayloadConstructor
        throw new Error "Tried to create domain event '#{domainEventName}' which is not defined"

      payload = {}
      DomainEventPayloadConstructor.apply payload, [domainEventConstructorParams]

      new DomainEvent
        id: domainEventIdGenerator.generateId()
        name: domainEventName
        aggregate:
          id: aggregateId
          name: 'EventricTesting'
        context: @_context.name
        payload: payload


    wiredRemote.findDomainEventsByName = (names) ->
      names = [names] unless names instanceof Array
      fakePromise.resolve @_domainEvents.filter (x) ->
        names.indexOf(x.name) > -1


    wiredRemote.findDomainEventsByNameAndAggregateId = (names, aggregateIds) ->
      names = [names] unless names instanceof Array
      aggregateIds = [aggregateIds] unless aggregateIds instanceof Array
      fakePromise.resolve @_domainEvents.filter (x) ->
        names.indexOf(x.name) > -1 and x.aggregate and aggregateIds.indexOf(x.aggregate.id) > -1


    originalSubscribeToAllDomainEvents = wiredRemote.subscribeToAllDomainEvents
    wiredRemote.subscribeToAllDomainEvents = ->
      originalSubscribeToAllDomainEvents.apply @, arguments
      .then (subscriberId) =>
        @_subscriberIds.push subscriberId
        subscriberId


    originalSubscribeToDomainEvent = wiredRemote.subscribeToDomainEvent
    wiredRemote.subscribeToDomainEvent = ->
      originalSubscribeToDomainEvent.apply @, arguments
      .then (subscriberId) =>
        @_subscriberIds.push subscriberId
        subscriberId


    originalSubscribeToDomainEventWithAggregateId = wiredRemote.subscribeToDomainEventWithAggregateId
    wiredRemote.subscribeToDomainEventWithAggregateId = ->
      originalSubscribeToDomainEventWithAggregateId.apply @, arguments
      .then (subscriberId) =>
        @_subscriberIds.push subscriberId
        subscriberId


    wiredRemote.$destroy = ->
      @_domainEvents = []
      @_commandStubs = []
      @_mostCurrentEmitOperation.then =>
        @_mostCurrentEmitOperation = fakePromise.resolve()
        subscriptionRemovals = []
        for subscriberId in @_subscriberIds
          subscriptionRemovals.push wiredRemote.unsubscribeFromDomainEvent subscriberId
        @_subscriberIds = []
        Promise.all subscriptionRemovals



    wiredRemote.$onCommand = (command, payload) ->
      commandStub =
        command: command
        payload: payload
        domainEvents: []
        yieldsDomainEvent: (eventName, aggregateId, payload) ->
          @domainEvents.push
            eventName: eventName
            aggregateId: aggregateId
            payload: payload
          @

      @_commandStubs.push commandStub
      commandStub


    originalCommand = wiredRemote.command
    wiredRemote.command = (command, payload) ->
      filteredCommandStubs = @_commandStubs.filter (commandStub) ->
        return false unless command is commandStub.command
        _.isEqual payload, commandStub.payload

      unless filteredCommandStubs.length
        return originalCommand.apply @, arguments

      emitDomainEventAsync = (domainEvent) =>
        setTimeout =>
          @$emitDomainEvent domainEvent.eventName,
            domainEvent.aggregateId,
            domainEvent.payload

      for filteredCommandStub in filteredCommandStubs
        for domainEvent in filteredCommandStub.domainEvents
          emitDomainEventAsync domainEvent

      fakePromise.resolveAsync()

    wiredRemote


module.exports = new RemoteFactory
