Bus = require './Bus'
{me} = require 'core/auth'
LevelSession = require 'models/LevelSession'
utils = require 'core/utils'

module.exports = class LevelBus extends Bus

  @get: (levelID, sessionID) ->
    docName = "play/level/#{levelID}/#{sessionID}"
    return Bus.getFromCache(docName) or new LevelBus docName

  subscriptions:
    'tome:editing-began': 'onEditingBegan'
    'tome:editing-ended': 'onEditingEnded'
    'script:state-changed': 'onScriptStateChanged'
    'script:ended': 'onScriptEnded'
    'script:reset': 'onScriptReset'
    'surface:sprite-selected': 'onSpriteSelected'
    'level:show-victory': 'onVictory'
    'tome:spell-changed': 'onSpellChanged'
    'tome:spell-created': 'onSpellCreated'
    'tome:cast-spells': 'onCastSpells'
    'tome:winnability-updated': 'onWinnabilityUpdated'
    'application:idle-changed': 'onIdleChanged'
    'goal-manager:new-goal-states': 'onNewGoalStates'
    'god:new-world-created': 'onNewWorldCreated'

  constructor: ->
    super(arguments...)
    @changedSessionProperties = {}
    saveDelay = window.serverConfig?.sessionSaveDelay
    [wait, maxWait] = switch
      when not application.isProduction or not saveDelay then [1, 5]  # Save quickly in development.
      when me.isAnonymous() then [saveDelay.anonymous.min, saveDelay.anonymous.max]
      else [saveDelay.registered.min, saveDelay.registered.max]
    @saveSession = _.debounce @reallySaveSession, wait * 1000, {maxWait: maxWait * 1000}
    @playerIsIdle = false

  init: ->
    super()
    @fireScriptsRef = @fireRef?.child('scripts')

  setSession: (@session) ->
    @timerIntervalID = setInterval(@incrementSessionPlaytime, 1000)

  onIdleChanged: (e) ->
    @playerIsIdle = e.idle

  incrementSessionPlaytime: =>
    if @playerIsIdle then return
    @changedSessionProperties.playtime = true
    @session.set('playtime', (@session.get('playtime') ? 0) + 1)

  onPoint: ->
    return true

  onMeSynced: =>
    super()

  join: ->
    super()

  disconnect: ->
    @fireScriptsRef?.off()
    @fireScriptsRef = null
    super()

  removeFirebaseData: (callback) ->
    return callback?() unless @myConnection
    @myConnection.child('connected')
    @fireRef.remove()
    @onDisconnect.cancel(-> callback?())

  # UPDATING FIREBASE AND SESSION

  onEditingBegan: -> #@wizardRef?.child('editing').set(true)  # no more wizards
  onEditingEnded: -> #@wizardRef?.child('editing').set(false)  # no more wizards

  # HACK: Backbone does not work with nested documents, but we want to
  #   patch only those props that have changed. Look into plugins to
  #   give Backbone support for nested docs and update the code here.

  # TODO: The LevelBus doesn't need to be in charge of updating the
  #   LevelSession object. Either break this off into a separate class
  #   or have the LevelSession object listen for all these events itself.

  setSpells: (spells) ->
    @onSpellCreated spell: spell for spellKey, spell of spells

  onSpellChanged: (e) ->
    return unless @onPoint()
    code = @session.get('code')
    code ?= {}
    parts = e.spell.spellKey.split('/')

    code[parts[0]] ?= {}
    code[parts[0]][parts[1]] = e.spell.getSource()
    @changedSessionProperties.code = true
    @session.set({'code': code})
    @saveSession()

  onSpellCreated: (e) ->
    return unless @onPoint()
    spellTeam = e.spell.team
    @teamSpellMap ?= {}
    @teamSpellMap[spellTeam] ?= []

    unless e.spell.spellKey in @teamSpellMap[spellTeam]
      @teamSpellMap[spellTeam].push e.spell.spellKey
    @changedSessionProperties.teamSpells = true
    @session.set({'teamSpells': @teamSpellMap})
    @saveSession()
    if spellTeam is me.team or (e.spell.otherSession and spellTeam isnt e.spell.otherSession.get('team'))
      # https://github.com/codecombat/codecombat/issues/81
      @onSpellChanged e  # Save the new spell to the session, too.

  onCastSpells: (e) ->
    return unless @onPoint() and e.realTime
    # We have incremented state.submissionCount and reset state.flagHistory.
    @changedSessionProperties.state = true
    @saveSession()

  onWinnabilityUpdated: (e) ->
    return unless @onPoint() and e.winnable
    return unless e.level.get('slug') in ['ace-of-coders', 'elemental-wars']  # Mirror matches don't otherwise show victory, so we win here.
    return if @session.get('state')?.complete
    @onVictory()

  onNewWorldCreated: (e) ->
    return unless @onPoint()
    # Record the flag history.
    state = @session.get('state')
    flagHistory = (flag for flag in e.world.flagHistory when flag.source isnt 'code')
    return if _.isEqual state.flagHistory, flagHistory
    state.flagHistory = flagHistory
    @changedSessionProperties.state = true
    @session.set('state', state)
    @saveSession()

  onScriptStateChanged: (e) ->
    return unless @onPoint()
    @fireScriptsRef?.update(e)
    state = @session.get('state')
    scripts = state.scripts ? {}
    scripts.currentScript = e.currentScript
    scripts.currentScriptOffset = e.currentScriptOffset
    @changedSessionProperties.state = true
    @session.set('state', state)
    @saveSession()

  onScriptEnded: (e) ->
    return unless @onPoint()
    state = @session.get('state')
    scripts = state.scripts
    scripts.ended ?= {}
    return if scripts.ended[e.scriptID]?
    index = _.keys(scripts.ended).length + 1
    @fireScriptsRef?.child('ended').child(e.scriptID).set(index)
    scripts.ended[e.scriptID] = index
    @session.set('state', state)
    @changedSessionProperties.state = true
    @saveSession()

  onScriptReset: ->
    return unless @onPoint()
    @fireScriptsRef?.set({})
    state = @session.get('state')
    state.scripts = {}
    #state.complete = false  # Keep it complete once ever completed.
    @session.set('state', state)
    @changedSessionProperties.state = true
    @saveSession()

  onSpriteSelected: (e) ->
    return unless @onPoint()
    state = @session.get('state')
    state.selected = e.thang?.id or null
    @session.set('state', state)
    @changedSessionProperties.state = true
    @saveSession()

  onVictory: ->
    return unless @onPoint()
    state = @session.get('state')
    state.complete = true
    @session.set('state', state)
    @changedSessionProperties.state = true
    @reallySaveSession()  # Make sure it saves right away; don't debounce it.

  onNewGoalStates: (e) ->
    # TODO: this log doesn't capture when null-status goals are being set during world streaming. Where can they be coming from?
    goalStates = e.goalStates
    return console.error("Somehow trying to save null goal states!", newGoalStates) if _.find(newGoalStates, (gs) -> not gs.status)

    return unless e.overallStatus is 'success'
    newGoalStates = goalStates
    state = @session.get('state')
    oldGoalStates = state.goalStates or {}

    changed = false
    for goalKey, goalState of newGoalStates
      continue if oldGoalStates[goalKey]?.status is 'success' and goalState.status isnt 'success' # don't undo success, this property is for keying off achievements
      continue if utils.kindaEqual state.goalStates?[goalKey], goalState # Only save when goals really change
      changed = true
      oldGoalStates[goalKey] = _.cloneDeep newGoalStates[goalKey]

    if changed
      state.goalStates = oldGoalStates
      @session.set 'state', state
      @changedSessionProperties.state = true
      @saveSession()

  onPlayerJoined: (snapshot) =>
    super(arguments...)
    return unless @onPoint()
    players = @session.get('players')
    players ?= {}
    player = snapshot.val()
    return if players[player.id]?
    players[player.id] = {}
    @session.set('players', players)
    @changedSessionProperties.players = true
    @saveSession()

  onChatAdded: (snapshot) =>
    super(arguments...)
    chat = @session.get('chat')
    chat ?= []
    message = snapshot.val()
    return if message.system
    chat.push(message)
    chat = chat[chat.length-50...] if chat.length > 50
    @session.set('chat', chat)
    @changedSessionProperties.chat = true
    @saveSession()

  # Debounced as saveSession
  reallySaveSession: ->
    return if _.isEmpty @changedSessionProperties
    # don't let peeking admins mess with the session accidentally
    return unless @session.get('creator') is me.id
    return if @session.fake
    Backbone.Mediator.publish 'level:session-will-save', session: @session
    patch = {}
    patch[prop] = @session.get(prop) for prop of @changedSessionProperties
    @changedSessionProperties = {}

    # since updates are coming fast and loose for session objects
    # don't let what the server returns overwrite changes since the save began
    tempSession = new LevelSession _id: @session.id
    tempSession.save(patch, {patch: true, type: 'PUT'})

  destroy: ->
    clearInterval(@timerIntervalID)
    super()
