CocoClass = require 'core/CocoClass'
utils = require 'core/utils'

module.exports = class GoalManager extends CocoClass
  # The Goal Manager is created both on the main thread and
  # each time the world is generated. The one in world generation
  # records which code and world related goals
  # are completed or failed, and then the results are sent back
  # and saved to the main thread instance.
  # The main instance handles goals based on UI notifications,
  # and keeps track of what the goals are at any given point.

  # Goals can only have only one goal property. Otherwise who knows what will happen.
  # If you want weird goals or hybrid goals, make a custom goal.

  nextGoalID: 0
  nicks: ['GoalManager']

  constructor: (@world, @initialGoals, @team, options) ->
    super()
    @options = options or {}
    @init()

  init: ->
    @goals = []
    @goalStates = {} # goalID -> object (complete, frameCompleted)
    @userCodeMap = {} # @userCodeMap.thangID.methodName.aether.raw = codeString
    @thangTeams = {}
    @initThangTeams()
    @addGoal goal for goal in @initialGoals if @initialGoals

  initThangTeams: ->
    return unless @world
    for thang in @world.thangs when thang.team and thang.isAttackable
      continue unless thang.team
      @thangTeams[thang.team] = [] unless @thangTeams[thang.team]
      @thangTeams[thang.team].push(thang.id)

  subscriptions:
    'god:new-world-created': 'onNewWorldCreated'
    'god:new-html-goal-states': 'onNewHTMLGoalStates'
    'level:restarted': 'onLevelRestarted'

  backgroundSubscriptions:
    'world:thang-died': 'onThangDied'
    'world:thang-touched-goal': 'onThangTouchedGoal'
    'world:thang-left-map': 'onThangLeftMap'
    'world:thang-collected-item': 'onThangCollectedItem'
    'world:user-code-problem': 'onUserCodeProblem'
    'world:lines-of-code-counted': 'onLinesOfCodeCounted'

  onLevelRestarted: ->
    @goals = []
    @goalStates = {}
    @userCodeMap = {}
    @notifyGoalChanges()
    @addGoal goal for goal in @initialGoals if @initialGoals

  # INTERFACE AND LIFETIME OVERVIEW

  # world generator gets current goals from the main instance
  getGoals: -> @goals

  # background instance created by world generator,
  # gets these goals and code, and is told to be all ears during world gen
  setGoals: (@goals) ->
  setCode: (@userCodeMap) -> @updateCodeGoalStates()
  worldGenerationWillBegin: ->
    @initGoalStates()
    @checkForInitialUserCodeProblems()

  # World generator feeds world events to the goal manager to keep track
  submitWorldGenerationEvent: (channel, event, frameNumber) ->
    func = @backgroundSubscriptions[channel]
    func = utils.normalizeFunc(func, @)
    return unless func
    func.call(@, event, frameNumber)

  # after world generation, generated goal states
  # are grabbed to send back to main instance
  worldGenerationEnded: (finalFrame) -> @wrapUpGoalStates(finalFrame)
  getGoalStates: -> @goalStates

  # main instance gets them and updates their existing goal states,
  # passes the word along
  onNewWorldCreated: (e) ->
    @world = e.world
    @updateGoalStates(e.goalStates) if e.goalStates?

  onNewHTMLGoalStates: (e) ->
    @updateGoalStates(e.goalStates) if e.goalStates?

  updateGoalStates: (newGoalStates) ->
    for goalID, goalState of newGoalStates
      continue unless @goalStates[goalID]?
      @goalStates[goalID] = goalState
    @notifyGoalChanges()

  # IMPLEMENTATION DETAILS

  addGoal: (goal) ->
    goal = $.extend(true, {}, goal)
    goal.id = @nextGoalID++ if not goal.id
    return if @goalStates[goal.id]?
    @goals.push(goal)
    goal.isPositive = @goalIsPositive goal.id
    @goalStates[goal.id] = {status: 'incomplete', keyFrame: 0, team: goal.team}
    @notifyGoalChanges()
    return unless goal.notificationGoal
    f = (channel) => (event) => @onNote(channel, event)
    channel = goal.notificationGoal.channel
    @addNewSubscription(channel, f(channel))

  notifyGoalChanges: ->
    return if @options.headless
    overallStatus = @checkOverallStatus()
    event =
      goalStates: @goalStates
      goals: @goals
      overallStatus: overallStatus
      timedOut: @world? and (@world.totalFrames is @world.maxTotalFrames and overallStatus not in ['success', 'failure'])
    Backbone.Mediator.publish('goal-manager:new-goal-states', event)

  checkOverallStatus: (ignoreIncomplete=false) ->
    overallStatus = null
    goals = if @goalStates then _.values @goalStates else []
    goals = (g for g in goals when not g.optional)
    goals = (g for g in goals when g.team in [undefined, @team]) if @team
    statuses = (goal.status for goal in goals)
    overallStatus = 'success' if statuses.length > 0 and _.every(statuses, (s) -> s is 'success' or (ignoreIncomplete and s is null))
    overallStatus = 'failure' if statuses.length > 0 and 'failure' in statuses
    #console.log 'got overallStatus', overallStatus, 'from goals', goals, 'goalStates', @goalStates, 'statuses', statuses
    overallStatus

  # WORLD GOAL TRACKING

  initGoalStates: ->
    @goalStates = {}
    return unless @goals
    for goal in @goals
      state = {
        status: null # should eventually be either 'success', 'failure', or 'incomplete'
        keyFrame: 0 # when it became a 'success' or 'failure'
        team: goal.team
        optional: goal.optional
      }
      @initGoalState(state, [goal.killThangs, goal.saveThangs], 'killed')
      for getTo in goal.getAllToLocations ? []
        @initGoalState(state, [getTo.getToLocation?.who, []], 'arrived')
      for keepFrom in goal.keepAllFromLocations ? []
        @initGoalState(state, [[], keepFrom.keepFromLocation?.who], 'arrived')
      @initGoalState(state, [goal.getToLocations?.who, goal.keepFromLocations?.who], 'arrived')
      @initGoalState(state, [goal.leaveOffSides?.who, goal.keepFromLeavingOffSides?.who], 'left')
      @initGoalState(state, [goal.collectThangs?.targets, goal.keepFromCollectingThangs?.targets], 'collected')
      @initGoalState(state, [goal.codeProblems], 'problems')
      @initGoalState(state, [_.keys(goal.linesOfCode ? {})], 'lines')
      @goalStates[goal.id] = state

  checkForInitialUserCodeProblems: ->
    # There might have been some user code problems reported before the goal manager started listening.
    return unless @world
    for thang in @world.thangs when thang.isProgrammable
      for message, problem of thang.publishedUserCodeProblems
        @onUserCodeProblem {thang: thang, problem: problem}, 0

  onThangDied: (e, frameNumber) ->
    for goal in @goals ? []
      @checkKillThangs(goal.id, goal.killThangs, e.thang, frameNumber) if goal.killThangs?
      @checkKillThangs(goal.id, goal.saveThangs, e.thang, frameNumber) if goal.saveThangs?

  checkKillThangs: (goalID, targets, thang, frameNumber) ->
    return unless thang.id in targets or thang.team in targets
    @updateGoalState(goalID, thang.id, 'killed', frameNumber)

  onThangTouchedGoal: (e, frameNumber) ->
    for goal in @goals ? []
      @checkArrived(goal.id, goal.getToLocations.who, goal.getToLocations.targets, e.actor, e.touched.id, frameNumber) if goal.getToLocations?
      if goal.getAllToLocations?
        for getTo in goal.getAllToLocations
          @checkArrived(goal.id, getTo.getToLocation.who, getTo.getToLocation.targets, e.actor, e.touched.id, frameNumber)
      @checkArrived(goal.id, goal.keepFromLocations.who, goal.keepFromLocations.targets, e.actor, e.touched.id, frameNumber) if goal.keepFromLocations?
      if goal.keepAllFromLocations?
        for keepFrom in goal.keepAllFromLocations
          @checkArrived(goal.id, keepFrom.keepFromLocation.who , keepFrom.keepFromLocation.targets, e.actor, e.touched.id, frameNumber )

  checkArrived: (goalID, who, targets, thang, touchedID, frameNumber) ->
    return unless touchedID in targets
    return unless thang.id in who or thang.team in who
    @updateGoalState(goalID, thang.id, 'arrived', frameNumber)

  onThangLeftMap: (e, frameNumber) ->
    for goal in @goals ? []
      @checkLeft(goal.id, goal.leaveOffSides.who, goal.leaveOffSides.sides, e.thang, e.side, frameNumber) if goal.leaveOffSides?
      @checkLeft(goal.id, goal.keepFromLeavingOffSides.who, goal.keepFromLeavingOffSides.sides, e.thang, e.side, frameNumber) if goal.keepFromLeavingOffSides?

  checkLeft: (goalID, who, sides, thang, side, frameNumber) ->
    return if sides and side and not (side in sides)
    return unless thang.id in who or thang.team in who
    @updateGoalState(goalID, thang.id, 'left', frameNumber)

  onThangCollectedItem: (e, frameNumber) ->
    for goal in @goals ? []
      @checkCollected(goal.id, goal.collectThangs.who, goal.collectThangs.targets, e.actor, e.item.id, frameNumber) if goal.collectThangs?
      @checkCollected(goal.id, goal.keepFromCollectingThangs.who, goal.keepFromCollectingThangs.targets, e.actor, e.item.id, frameNumber) if goal.keepFromCollectingThangs?

  checkCollected: (goalID, who, targets, thang, itemID, frameNumber) ->
    return unless itemID in targets
    return unless thang.id in who or thang.team in who
    @updateGoalState(goalID, itemID, 'collected', frameNumber)

  onUserCodeProblem: (e, frameNumber) ->
    for goal in @goals ? [] when goal.codeProblems
      @checkCodeProblem goal.id, goal.codeProblems, e.thang, frameNumber

  checkCodeProblem: (goalID, who, thang, frameNumber) ->
    return unless thang.id in who or thang.team in who
    @updateGoalState goalID, thang.id, 'problems', frameNumber

  onLinesOfCodeCounted: (e, frameNumber) ->
    for goal in @goals ? [] when goal.linesOfCode
      @checkLinesOfCode goal.id, goal.linesOfCode, e.thang, e.linesUsed, frameNumber

  checkLinesOfCode: (goalID, who, thang, linesUsed, frameNumber) ->
    return unless linesAllowed = who[thang.id] ? who[thang.team]
    @updateGoalState goalID, thang.id, 'lines', frameNumber if linesUsed > linesAllowed

  wrapUpGoalStates: (finalFrame) ->
    for goalID, state of @goalStates
      if state.status is null
        if @goalIsPositive(goalID)
          state.status = 'incomplete'
        else
          state.status = 'success'
          state.keyFrame = 'end' # special case for objective UI to handle

  # UI EVENT GOAL TRACKING

  onNote: (channel, e) ->
    # TODO for UI event related goals

  # HELPER FUNCTIONS

  # It's a pretty similar pattern for each of the above goals.
  # Once you determine a thang has done the thing, you mark it done in the progress object.
  initGoalState: (state, whos, progressObjectName) ->
    # 'whos' is an array of goal 'who' values.
    # This inits the progress object for the goal tracking.

    arrays = (prop for prop in whos when prop?.length)
    return unless arrays.length
    state[progressObjectName] ?= {}
    for array in arrays
      for thang in array
        if @thangTeams[thang]?
          for t in @thangTeams[thang]
            state[progressObjectName][t] = false
        else
          state[progressObjectName][thang] = false

  getGoalState: (goalID) ->
    @goalStates[goalID].status

  setGoalState: (goalID, status) ->
    state = @goalStates[goalID]
    state.status = status
    if overallStatus = @checkOverallStatus true
      matchedGoals = (_.find(@goals, {id: goalID}) for goalID, goalState of @goalStates when goalState.status is overallStatus)
      mostEagerGoal = _.min matchedGoals, 'worldEndsAfter'
      victory = overallStatus is 'success'
      tentative = overallStatus is 'success'
      @world?.endWorld victory, mostEagerGoal.worldEndsAfter, tentative if mostEagerGoal isnt Infinity

  updateGoalState: (goalID, thangID, progressObjectName, frameNumber) ->
    # A thang has done something related to the goal!
    # Mark it down and update the goal state.
    goal = _.find @goals, {id: goalID}
    state = @goalStates[goalID]
    stateThangs = state[progressObjectName]
    stateThangs[thangID] = true
    success = @goalIsPositive goalID
    if success
      numNeeded = goal.howMany ? Math.max(1, _.size stateThangs)
    else
      # saveThangs: by default we would want to save all the Thangs, which means that we would want none of them to be 'done'
      numNeeded = _.size(stateThangs) - Math.max((goal.howMany ? 1), _.size stateThangs) + 1
    numDone = _.filter(stateThangs).length
    #console.log 'needed', numNeeded, 'done', numDone, 'of total', _.size(stateThangs), 'with how many', goal.howMany, 'and stateThangs', stateThangs, 'for', goalID, thangID, 'on frame', frameNumber, 'all Thangs', _.keys(stateThangs), _.values(stateThangs)
    return unless numDone >= numNeeded
    return if state.status and not success  # already failed it; don't wipe keyframe
    state.status = if success then 'success' else 'failure'
    state.keyFrame = frameNumber
    #console.log goalID, 'became', success, 'on frame', frameNumber, 'with overallStatus', @checkOverallStatus true
    if overallStatus = @checkOverallStatus true
      matchedGoals = (_.find(@goals, {id: goalID}) for goalID, goalState of @goalStates when goalState.status is overallStatus)
      mostEagerGoal = _.min matchedGoals, 'worldEndsAfter'
      victory = overallStatus is 'success'
      tentative = overallStatus is 'success'
      @world?.endWorld victory, mostEagerGoal.worldEndsAfter, tentative if mostEagerGoal isnt Infinity

  goalIsPositive: (goalID) ->
    # Positive goals are completed when all conditions are true (kill all these thangs)
    # Negative goals fail when any are true (keep all these thangs from being killed)
    goal = _.find(@goals, {id: goalID}) ? {}
    return false for prop of goal when @positiveGoalMap[prop] is 0
    true

  positiveGoalMap:
    killThangs: 1
    saveThangs: 0
    getToLocations: 1
    getAllToLocations: 1
    keepFromLocations: 0
    keepAllFromLocations: 0
    leaveOffSides: 1
    keepFromLeavingOffSides: 0
    collectThangs: 1
    keepFromCollectingThangs: 0
    linesOfCode: 0
    codeProblems: 0

  updateCodeGoalStates: ->
    # TODO

  # teardown

  destroy: ->
    super()
