CocoModel = require './CocoModel'
LevelComponent = require './LevelComponent'
LevelSystem = require './LevelSystem'
ThangType = require './ThangType'

module.exports = class Level extends CocoModel
  @className: 'Level'
  @schema: require 'schemas/models/level'
  @levels:
    'dungeons-of-kithgard': '5411cb3769152f1707be029c'
    'defense-of-plainswood': '541b67f71ccc8eaae19f3c62'
  urlRoot: '/db/level'
  editableByArtisans: true

  serialize: (options) ->
    {supermodel, session, otherSession, @headless, @sessionless, cached=false} = options
    o = @denormalize supermodel, session, otherSession # hot spot to optimize

    # Figure out Components
    o.levelComponents = if cached then @getCachedLevelComponents(supermodel) else $.extend true, [], (lc.attributes for lc in supermodel.getModels LevelComponent)
    @sortThangComponents o.thangs, o.levelComponents, 'Level Thang'
    @fillInDefaultComponentConfiguration o.thangs, o.levelComponents # hot spot to optimize

    # Figure out Systems
    systemModels = $.extend true, [], (ls.attributes for ls in supermodel.getModels LevelSystem)
    o.systems = @sortSystems o.systems, systemModels
    @fillInDefaultSystemConfiguration o.systems

    # Figure out ThangTypes' Components
    tmap = {}
    tmap[t.thangType] = true for t in o.thangs ? []
    sessionHeroes = [session?.get('heroConfig')?.thangType, otherSession?.get('heroConfig')?.thangType]
    o.thangTypes = []
    for tt in supermodel.getModels ThangType
      if tmap[tt.get('original')] or
        (tt.get('kind') isnt 'Hero' and tt.get('kind')? and tt.get('components') and not tt.notInLevel) or
        (tt.get('kind') is 'Hero' and (@isType('course', 'course-ladder', 'game-dev') or tt.get('original') in sessionHeroes))
          o.thangTypes.push (original: tt.get('original'), name: tt.get('name'), components: $.extend(true, [], tt.get('components')))
    @sortThangComponents o.thangTypes, o.levelComponents, 'ThangType'
    @fillInDefaultComponentConfiguration o.thangTypes, o.levelComponents

    o.picoCTFProblem = @picoCTFProblem if @picoCTFProblem

    o

  cachedLevelComponents: null

  getCachedLevelComponents: (supermodel) ->
    @cachedLevelComponents ?= {}
    levelComponents = supermodel.getModels LevelComponent
    newLevelComponents = []
    for levelComponent in levelComponents
      if levelComponent.hasLocalChanges()
        newLevelComponents.push $.extend(true, {}, levelComponent.attributes)
        continue
      @cachedLevelComponents[levelComponent.id] ?= @cachedLevelComponents[levelComponent.id] = $.extend(true, {}, levelComponent.attributes)
      newLevelComponents.push(@cachedLevelComponents[levelComponent.id])
    newLevelComponents

  denormalize: (supermodel, session, otherSession) ->
    o = $.extend true, {}, @attributes
    if o.thangs and @isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev')
      thangTypesWithComponents = (tt for tt in supermodel.getModels(ThangType) when tt.get('components')?)
      thangTypesByOriginal = _.indexBy thangTypesWithComponents, (tt) -> tt.get('original')  # Optimization
      for levelThang in o.thangs
        @denormalizeThang(levelThang, supermodel, session, otherSession, thangTypesByOriginal)
    o

  denormalizeThang: (levelThang, supermodel, session, otherSession, thangTypesByOriginal) ->
    levelThang.components ?= []
    isHero = /Hero Placeholder/.test(levelThang.id) and @isType('hero', 'hero-ladder', 'hero-coop')
    if isHero and otherSession
      # If it's a hero and there's another session, find the right session for it.
      # If there is no other session (playing against default code, or on single player), clone all placeholders.
      # TODO: actually look at the teams on these Thangs to determine which session should go with which placeholder.
      if levelThang.id is 'Hero Placeholder 1' and session.get('team') is 'humans'
        session = otherSession
      else if levelThang.id is 'Hero Placeholder' and session.get('team') is 'ogres'
        session = otherSession

    # Empty out placeholder Components and store their values if we're the hero placeholder.
    if isHero
      placeholders = {}
      placeholdersUsed = {}
      placeholderThangType = thangTypesByOriginal[levelThang.thangType]
      unless placeholderThangType
        console.error "Couldn't find placeholder ThangType for the hero!"
        isHero = false
      else
        for defaultPlaceholderComponent in placeholderThangType.get('components')
          placeholders[defaultPlaceholderComponent.original] = defaultPlaceholderComponent
        for thangComponent in levelThang.components
          placeholders[thangComponent.original] = thangComponent
        levelThang.components = []  # We have stored the placeholder values, so we can inherit everything else.
        heroThangType = session?.get('heroConfig')?.thangType
        levelThang.thangType = heroThangType if heroThangType

    thangType = thangTypesByOriginal[levelThang.thangType]

    configs = {}
    for thangComponent in levelThang.components
      configs[thangComponent.original] = thangComponent

    for defaultThangComponent in thangType?.get('components') or []
      if levelThangComponent = configs[defaultThangComponent.original]
        # Take the ThangType default Components and merge level-specific Component config into it
        copy = $.extend true, {}, defaultThangComponent.config
        levelThangComponent.config = _.merge copy, levelThangComponent.config

      else
        # Just add the Component as is
        levelThangComponent = $.extend true, {}, defaultThangComponent
        levelThang.components.push levelThangComponent

      if isHero and placeholderComponent = placeholders[defaultThangComponent.original]
        placeholdersUsed[placeholderComponent.original] = true
        placeholderConfig = placeholderComponent.config ? {}
        levelThangComponent.config ?= {}
        config = levelThangComponent.config
        if placeholderConfig.pos  # Pull in Physical pos x and y
          config.pos ?= {}
          config.pos.x = placeholderConfig.pos.x
          config.pos.y = placeholderConfig.pos.y
          config.rotation = placeholderConfig.rotation
        else if placeholderConfig.team  # Pull in Allied team
          config.team = placeholderConfig.team
        else if placeholderConfig.significantProperty  # For levels where we cheat on what counts as an enemy
          config.significantProperty = placeholderConfig.significantProperty
        else if placeholderConfig.programmableMethods
          # Take the ThangType default Programmable and merge level-specific Component config into it
          copy = $.extend true, {}, placeholderConfig
          programmableProperties = config?.programmableProperties ? []
          copy.programmableProperties = _.union programmableProperties, copy.programmableProperties ? []
          levelThangComponent.config = config = _.merge copy, config
        else if placeholderConfig.extraHUDProperties
          config.extraHUDProperties = _.union(config.extraHUDProperties ? [], placeholderConfig.extraHUDProperties)
        else if placeholderConfig.voiceRange  # Pull in voiceRange
          config.voiceRange = placeholderConfig.voiceRange
          config.cooldown = placeholderConfig.cooldown

    if isHero
      if equips = _.find levelThang.components, {original: LevelComponent.EquipsID}
        inventory = session?.get('heroConfig')?.inventory
        equips.config ?= {}
        equips.config.inventory = $.extend true, {}, inventory if inventory
      for original, placeholderComponent of placeholders when not placeholdersUsed[original]
        levelThang.components.push placeholderComponent

    # Load the user's chosen hero AFTER getting stats from default char
    if /Hero Placeholder/.test(levelThang.id) and @isType('course') and not @headless and not @sessionless
      heroThangType = me.get('heroConfig')?.thangType or ThangType.heroes.captain
      levelThang.thangType = heroThangType if heroThangType

  sortSystems: (levelSystems, systemModels) ->
    [sorted, originalsSeen] = [[], {}]
    visit = (system) ->
      return if system.original of originalsSeen
      systemModel = _.find systemModels, {original: system.original}
      return console.error 'Couldn\'t find model for original', system.original, 'from', systemModels unless systemModel
      for d in systemModel.dependencies or []
        system2 = _.find levelSystems, {original: d.original}
        visit system2
      #console.log 'sorted systems adding', systemModel.name
      sorted.push {model: systemModel, config: $.extend true, {}, system.config}
      originalsSeen[system.original] = true
    visit system for system in levelSystems ? []
    sorted

  sortThangComponents: (thangs, levelComponents, parentType) ->
    # Here we have to sort the Components by their dependencies.
    # It's a bit tricky though, because we don't have either soft dependencies or priority levels.
    # Example: Programmable must come last, since it has to override any Component-provided methods that any other Component might have created. Can't enumerate all soft dependencies.
    # Example: Plans needs to come after everything except Programmable, since other Components that add plannable methods need to have done so by the time Plans is attached.
    # Example: Collides doesn't depend on Allied, but if both exist, Collides must come after Allied. Soft dependency example. Can't just figure out a proper priority to take care of it.
    # Example: Moves doesn't depend on Acts, but if both exist, Moves must come after Acts. Another soft dependency example.
    # Decision? Just special case the sort logic in here until we have more examples than these two and decide how best to handle most of the cases then, since we don't really know the whole of the problem yet.
    # TODO: anything that depends on Programmable will break right now.

    originalsToComponents = _.indexBy levelComponents, 'original'  # Optimization for speed
    alliedComponent = _.find levelComponents, name: 'Allied'
    actsComponent = _.find levelComponents, name: 'Acts'

    for thang in thangs ? []
      originalsToThangComponents = _.indexBy thang.components, 'original'
      sorted = []
      visit = (c, namesToIgnore) ->
        return if c in sorted
        lc = originalsToComponents[c.original]
        console.error thang.id or thang.name, 'couldn\'t find lc for', c, 'of', levelComponents unless lc
        return unless lc
        return if namesToIgnore and lc.name in namesToIgnore
        if lc.name is 'Plans'
          # Plans always comes second-to-last, behind Programmable
          visit c2, [lc.name, 'Programmable'] for c2 in thang.components
        else if lc.name is 'Programmable'
          # Programmable always comes last
          visit c2, [lc.name] for c2 in thang.components
        else
          for d in lc.dependencies or []
            c2 = originalsToThangComponents[d.original]
            unless c2
              dependent = originalsToComponents[d.original]
              dependent = dependent?.name or d.original
              console.error parentType, thang.id or thang.name, 'does not have dependent Component', dependent, 'from', lc.name
            visit c2 if c2
          if lc.name is 'Collides' and alliedComponent
            if allied = originalsToThangComponents[alliedComponent.original]
              visit allied
          if lc.name is 'Moves' and actsComponent
            if acts = originalsToThangComponents[actsComponent.original]
              visit acts
        #console.log thang.id, 'sorted comps adding', lc.name
        sorted.push c
      for comp in thang.components
        visit comp
      thang.components = sorted

  fillInDefaultComponentConfiguration: (thangs, levelComponents) ->
    # This is slow, so I inserted some optimizations to speed it up by caching the eventual defaults of commonly-used Components.
    @defaultComponentConfigurations ?= {}
    cached = 0
    missed = 0
    cachedConfigs = 0
    for thang in thangs ? []
      for component in thang.components or []
        isPhysical = component.original is LevelComponent.PhysicalID
        if not isPhysical and defaultConfiguration = _.find @defaultComponentConfigurations[component.original], ((d) -> _.isEqual component, d.originalComponent)
          component.config = defaultConfiguration.defaultedConfig
          ++cached
          continue
        continue unless lc = _.find levelComponents, {original: component.original}
        unless isPhysical
          originalComponent = $.extend true, {}, component
        component.config ?= {}
        TreemaUtils.populateDefaults(component.config, lc.configSchema ? {}, tv4)
        @lastType = 'component'
        @lastOriginal = component.original
        unless isPhysical
          @defaultComponentConfigurations[component.original] ?= []
          @defaultComponentConfigurations[component.original].push originalComponent: originalComponent, defaultedConfig: component.config
          ++cachedConfigs
        ++missed
    #console.log 'cached', cached, 'missed', missed

  fillInDefaultSystemConfiguration: (levelSystems) ->
    for system in levelSystems ? []
      system.config ?= {}
      TreemaUtils.populateDefaults(system.config, system.model.configSchema, tv4)
      @lastType = 'system'
      @lastOriginal = system.model.name

  dimensions: ->
    width = 0
    height = 0
    for thang in @get('thangs') or []
      for component in thang.components
        c = component.config
        continue unless c?
        width = c.width if c.width? and c.width > width
        height = c.height if c.height? and c.height > height
    return {width: width, height: height}

  isLadder: ->
    return @get('type')?.indexOf('ladder') > -1

  isProject: ->
    return @get('shareable') is 'project'

  isType: (types...) ->
    return @get('type', true) in types

  fetchNextForCourse: ({ levelOriginalID, courseInstanceID, courseID, sessionID }, options={}) ->
    if courseInstanceID
      options.url = "/db/course_instance/#{courseInstanceID}/levels/#{levelOriginalID}/sessions/#{sessionID}/next"
    else
      options.url = "/db/course/#{courseID}/levels/#{levelOriginalID}/next"
    @fetch(options)

  getSolutions: ->
    return [] unless hero = _.find (@get("thangs") ? []), id: 'Hero Placeholder'
    return [] unless config = _.find(hero.components ? [], (x) -> x.config?.programmableMethods?.plan)?.config
    solutions = _.cloneDeep config.programmableMethods.plan.solutions ? []
    for solution in solutions
      try
        solution.source = _.template(solution.source)(config?.programmableMethods?.plan.context)
      catch e
        console.error "Problem with template and solution comments for", @get('slug'), e
    solutions
