<commit_before>### Constants ###

globals = @

CODE_CHANGE_TIMEOUT = 1000
MODEL_FORMATTING_INDENTATION = 2
GAME_DIMENSIONS = [960, 540]

MessageType = GE.makeConstantSet("Error", "Info")

SPINNER_OPTS = 
  lines: 9
  length: 7
  width: 4
  radius: 10
  corners: 1
  rotate: 0
  color: '#000'
  speed: 1
  trail: 60
  shadow: false
  hwaccel: false
  className: 'spinner'
  zIndex: 2e9
  top: 'auto'
  left: 'auto'

EVAL_ASSETS = 
  underscore: "lib/underscore/underscore.js"
  gamEvolveCommon: "gamEvolveCommon.js"
  sylvester: "lib/sylvester/sylvester.src.js"

GAME_JSON_PROPERTY_TO_EDITOR =
  model: "modelEditor"
  assets: "assetsEditor"
  actions: "actionsEditor"
  tools: "toolsEditor"
  layout: "layoutEditor"
  services: "servicesEditor"


### Globals ###

editors = {}
log = null
evalLoadedAssets = []

services = {}

spinner = new Spinner(SPINNER_OPTS)

currentModel = new GE.Model()
currentFrame = 0
currentModelData = null
currentAssets = null
currentActions = null
currentTools = null
currentLayout = null
currentServices = null
currentEvaluator = null

# Contains references to object URLS or DOM elements
assetNamesToData = {} 
compiledActions = {}
compiledTools = {}

isPlaying = false
automaticallyUpdatingModel = false
gameScreenScale = 1

# Find the correct function across browsers
requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  window.webkitRequestAnimationFrame || window.msRequestAnimationFrame

### Functions ###

adjustEditorToSize = (editor) -> 
  session = editor.session

  editor.resize()
  if session.getUseWrapMode()
    characterWidth = editor.renderer.characterWidth
    contentWidth = editor.renderer.scroller.clientWidth

    if contentWidth > 0  
      limit = parseInt(contentWidth / characterWidth, 10)
      session.setWrapLimitRange(limit, limit)

handleResize = -> 
  # Update the ACE editors
  for editorName, editor of editors then adjustEditorToSize(editor)
  adjustEditorToSize(log)

  # Update the scale sent to stepLoop()
  screenElement = $('#gameContent')
  scale = Math.min(screenElement.parent().outerWidth() / GAME_DIMENSIONS[0], screenElement.parent().outerHeight() / GAME_DIMENSIONS[1])
  roundedScale = scale.toFixed(2)
  newSize = [
    screenElement.parent().outerWidth() - roundedScale * GAME_DIMENSIONS[0]
    screenElement.parent().outerHeight() - roundedScale * GAME_DIMENSIONS[1]
  ]
  screenElement.css 
    "-webkit-transform": "scale(#{roundedScale})"
    "transform": "scale(#{roundedScale})"
    "width": "#{newSize[0]}px"
    "height": "#{newSize[1]}px"
    "left": "#{newSize[0] / 2}px"
    "top": "#{newSize[1] / 2}px"

showMessage = (messageType, message) ->
  switch messageType
    when MessageType.Error
      $("#topAlertMessage").html(message)
      $("#topAlert").show()
    when MessageType.Info
      $("#topInfoMessage").html(message)
      $("#topInfo").show()
    else throw new Error("Incorrect messageType")

clearMessage = -> $("#topAlert, #topInfo").hide()

logWithPrefix = (logType, values...) ->
  if GE.logLevels[logType]
    log.clearSelection()
    log.navigateFileEnd()
    valueStrings = for value in values 
      if _.isString(value) then value else JSON.stringify(value)
    log.insert(logType+": "+getFormattedTime()+" "+valueStrings.join("  ")+"\n")
  else
    logWithPrefix(GE.logLevels.ERROR, "Bad logType parameter '"+logType+"' in log for message '"+message+"'")

togglePlayMode = ->
  if isPlaying
    isPlaying = false
    for editorId, editor of editors
      editor.setReadOnly(false)
      $('#'+editorId).fadeTo('slow', 1)
    $("#playButton").button "option",
      label: "Play" 
      icons: 
        primary: "ui-icon-play"
  else
    isPlaying = true
    for editorId, editor of editors
      editor.setReadOnly(true)
      $('#'+editorId).fadeTo('slow', 0.2)
    handleAnimation()
    $("#playButton").button "option",
      label: "Pause" 
      icons: 
        primary: "ui-icon-pause"

setupLayout = ->
  # top
  $("#saveButton").button({ icons: { primary: "ui-icon-transferthick-e-w" }})
  $("#shareButton").button({ icons: { primary: "ui-icon-link" }})

  # west
  $("#playButton").button({ icons: { primary: "ui-icon-play" }, text: false })
  $("#timeSlider").slider
    orientation: "horizontal"
    range: "min"
    min: 0
    max: 0
    step: 1
    value: 0
  $("#resetButton").button({ icons: { primary: "ui-icon-arrowreturnthick-1-w" }, text: false })

  $("#west").tabs()
  $("#south").tabs().addClass( "ui-tabs-vertical ui-helper-clearfix" )
  $("#south li").removeClass( "ui-corner-top" ).addClass( "ui-corner-left" )
  $("#south li a").click(handleResize)

  # TODO: look for a callback for tabs
  $('body').layout 
    north__resizable: false
    north__closable: false
    north__size: 50
    west__size: 300
    applyDefaultStyles: true
    onresize: handleResize

setupButtonHandlers = ->
  $("#playButton").on "click", togglePlayMode

  $("#resetButton").on "click", ->
    currentFrame = 0
    currentModel = currentModel.atVersion(0)
    resetLogContent()

    # TODO: move these handlers to MVC events
    $("#timeSlider").slider "option", 
      value: 0
      max: 0

    automaticallyUpdatingModel = true
    editors.modelEditor.setValue(JSON.stringify(currentModel.data, null, MODEL_FORMATTING_INDENTATION))
    # The new content will be selected by default
    editors.modelEditor.selection.clearSelection() 
    automaticallyUpdatingModel = false

    # Execute again
    executeCode()

  $("#timeSlider").on "slide", ->
    currentFrame = $(this).slider("value")

    # If done immediately, will block slider movement, so we postpone it
    GE.doLater ->
      automaticallyUpdatingModel = true
      editors.modelEditor.setValue(JSON.stringify(currentModel.atVersion(currentFrame).data, null, MODEL_FORMATTING_INDENTATION))
      # The new content will be selected by default
      editors.modelEditor.selection.clearSelection() 
      automaticallyUpdatingModel = false

      # Execute again
      executeCode()

# Mode should be something that ACE Editor recognizes, like "ace/mode/javascript"
setupEditor = (id, mode = "") ->
  editor = ace.edit(id)
  if mode then editor.getSession().setMode(mode)
  editor.getSession().setUseWrapMode(true)
  editor.setWrapBehavioursEnabled(true)
  return editor

reloadCode = (callback) ->
  #programId = window.location.search.slice(1)

  currentEvaluator = GE.makeEvaluator(evalLoadedAssets...)

  try
    currentModelData = JSON.parse(editors.modelEditor.getValue())
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Model error. #{error}")
    return showMessage(MessageType.Error, "<strong>Model error.</strong> #{error}")

  try
    currentActions = JSON.parse(editors.actionsEditor.getValue())
    compiledActions = GE.mapObject currentActions, (value, key) ->
      compiledAction = {}
      try
        for actionKey, actionValue of value
          compiledAction[actionKey] = switch actionKey
            when "update" then GE.compileUpdate(actionValue, currentEvaluator)
            when "listActiveChildren" then GE.compileListActiveChildren(actionValue, currentEvaluator)
            when "handleSignals" then GE.compileHandleSignals(actionValue, currentEvaluator)
            else actionValue
        return compiledAction
      catch compilationError
        throw new Error("Error compiling action '#{key}'. #{compilationError}")
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Actions error. #{error}")
    return showMessage(MessageType.Error, "<strong>Actions error.</strong> #{error}")

  try
    currentTools = JSON.parse(editors.toolsEditor.getValue())
    toolsContext = 
      tools: null
      log: null
    compiledTools = GE.mapObject currentTools, (value, key) -> 
      try
        toolFactory = GE.compileTool(value.body, toolsContext, value.args, currentEvaluator)
        return toolFactory(toolsContext)
      catch compilationError
        throw new Error("Error compiling tool '#{key}'. #{compilationError}")
    toolsContext.tools = compiledTools
    toolsContext.log = logWithPrefix
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Tools error. #{error}")
    return showMessage(MessageType.Error, "<strong>Tools error.</strong> #{error}")

  try
    currentLayout = JSON.parse(editors.layoutEditor.getValue())
    # TODO: compile expressions ahead of time
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Layout error. #{error}")
    return showMessage(MessageType.Error, "<strong>Layout error.</strong> #{error}")

  try
    serviceDefs = JSON.parse(editors.servicesEditor.getValue())

    # Destroy old services
    for serviceName, service of currentServices
      service.destroy()

    # Create new services
    currentServices = {}
    for serviceName, serviceDef of serviceDefs
      options = _.defaults serviceDef.options || {},
        elementSelector: '#gameContent'
        size: GAME_DIMENSIONS
      currentServices[serviceName] = services[serviceDef.type](options)
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Services error. #{error}")
    return showMessage(MessageType.Error, "<strong>Services error.</strong> #{error}")

  try
    newAssets = JSON.parse(editors.assetsEditor.getValue())
    updateAssets(newAssets, currentEvaluator)
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Assets error. #{error}")
    return showMessage(MessageType.Error, "<strong>Assets error.</strong> #{error}")

  # TODO: move these handlers to MVC events
  currentModel.atVersion(currentFrame).data = currentModelData

  $("#timeSlider").slider "option", 
    value: currentFrame
    max: currentFrame

  logWithPrefix(GE.logLevels.INFO, "Game updated")
  showMessage(MessageType.Info, "Game updated")
  callback(null)

# Unload previous assets and load new ones. The given list of evaluators are initialized with any JS assets.
# Updates currentAssets.
# TODO: move asset handling into gamEvolve.coffee. CSS could be handled by the HTML service. JS is harder.
updateAssets = (newAssetMap, evaluators...) ->
  # Remove old assets
  # TODO: Only update new assets
  for name, dataUrl of currentAssets
    splitUrl = splitDataUrl(dataUrl)
    if splitUrl.mimeType == "application/javascript"
      # Nothing to do, cannot unload JS!
    else if splitUrl.mimeType == "text/css"
      assetNamesToData[name].remove()
    else if splitUrl.mimeType.indexOf("image/") == 0
      URL.revokeObjectUrl(assetNamesToData[name])

  assetNamesToData = {}

  # Create new assets
  for name, dataUrl of newAssetMap
    splitUrl = splitDataUrl(dataUrl)
    if splitUrl.mimeType == "application/javascript"
      script = atob(splitUrl.data)
      for evaluator in evaluators
        evaluator(script)
    else if splitUrl.mimeType == "text/css"
      css = atob(splitUrl.data)
      assetNamesToData[name] = $('<style type="text/css"></style>').html(css).appendTo("head")
    else if splitUrl.mimeType.indexOf("image/") == 0
      assetNamesToData[name] = Util.strToObjectURL(splitUrl.data, splitUrl.mimeType)
    else
      assetNamesToData[name] = atob(splitUrl.data)

  currentAssets = newAssetMap

# Runs the currently loaded code on the current frame
# Returns a new model
executeCode = ->
  modelAtFrame = currentModel.atVersion(currentFrame)<commit_msg>Fixed image loading<commit_after>### Constants ###

globals = @

CODE_CHANGE_TIMEOUT = 1000
MODEL_FORMATTING_INDENTATION = 2
GAME_DIMENSIONS = [960, 540]

MessageType = GE.makeConstantSet("Error", "Info")

SPINNER_OPTS = 
  lines: 9
  length: 7
  width: 4
  radius: 10
  corners: 1
  rotate: 0
  color: '#000'
  speed: 1
  trail: 60
  shadow: false
  hwaccel: false
  className: 'spinner'
  zIndex: 2e9
  top: 'auto'
  left: 'auto'

EVAL_ASSETS = 
  underscore: "lib/underscore/underscore.js"
  gamEvolveCommon: "gamEvolveCommon.js"
  sylvester: "lib/sylvester/sylvester.src.js"

GAME_JSON_PROPERTY_TO_EDITOR =
  model: "modelEditor"
  assets: "assetsEditor"
  actions: "actionsEditor"
  tools: "toolsEditor"
  layout: "layoutEditor"
  services: "servicesEditor"


### Globals ###

editors = {}
log = null
evalLoadedAssets = []

services = {}

spinner = new Spinner(SPINNER_OPTS)

currentModel = new GE.Model()
currentFrame = 0
currentModelData = null
currentAssets = null
currentActions = null
currentTools = null
currentLayout = null
currentServices = null
currentEvaluator = null

assetNamesToData = {} 
assetNamesToObjectUrls = {}

compiledActions = {}
compiledTools = {}

isPlaying = false
automaticallyUpdatingModel = false
gameScreenScale = 1

# Find the correct function across browsers
requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  window.webkitRequestAnimationFrame || window.msRequestAnimationFrame

### Functions ###

adjustEditorToSize = (editor) -> 
  session = editor.session

  editor.resize()
  if session.getUseWrapMode()
    characterWidth = editor.renderer.characterWidth
    contentWidth = editor.renderer.scroller.clientWidth

    if contentWidth > 0  
      limit = parseInt(contentWidth / characterWidth, 10)
      session.setWrapLimitRange(limit, limit)

handleResize = -> 
  # Update the ACE editors
  for editorName, editor of editors then adjustEditorToSize(editor)
  adjustEditorToSize(log)

  # Update the scale sent to stepLoop()
  screenElement = $('#gameContent')
  scale = Math.min(screenElement.parent().outerWidth() / GAME_DIMENSIONS[0], screenElement.parent().outerHeight() / GAME_DIMENSIONS[1])
  roundedScale = scale.toFixed(2)
  newSize = [
    screenElement.parent().outerWidth() - roundedScale * GAME_DIMENSIONS[0]
    screenElement.parent().outerHeight() - roundedScale * GAME_DIMENSIONS[1]
  ]
  screenElement.css 
    "-webkit-transform": "scale(#{roundedScale})"
    "transform": "scale(#{roundedScale})"
    "width": "#{newSize[0]}px"
    "height": "#{newSize[1]}px"
    "left": "#{newSize[0] / 2}px"
    "top": "#{newSize[1] / 2}px"

showMessage = (messageType, message) ->
  switch messageType
    when MessageType.Error
      $("#topAlertMessage").html(message)
      $("#topAlert").show()
    when MessageType.Info
      $("#topInfoMessage").html(message)
      $("#topInfo").show()
    else throw new Error("Incorrect messageType")

clearMessage = -> $("#topAlert, #topInfo").hide()

logWithPrefix = (logType, values...) ->
  if GE.logLevels[logType]
    log.clearSelection()
    log.navigateFileEnd()
    valueStrings = for value in values 
      if _.isString(value) then value else JSON.stringify(value)
    log.insert(logType+": "+getFormattedTime()+" "+valueStrings.join("  ")+"\n")
  else
    logWithPrefix(GE.logLevels.ERROR, "Bad logType parameter '"+logType+"' in log for message '"+message+"'")

togglePlayMode = ->
  if isPlaying
    isPlaying = false
    for editorId, editor of editors
      editor.setReadOnly(false)
      $('#'+editorId).fadeTo('slow', 1)
    $("#playButton").button "option",
      label: "Play" 
      icons: 
        primary: "ui-icon-play"
  else
    isPlaying = true
    for editorId, editor of editors
      editor.setReadOnly(true)
      $('#'+editorId).fadeTo('slow', 0.2)
    handleAnimation()
    $("#playButton").button "option",
      label: "Pause" 
      icons: 
        primary: "ui-icon-pause"

setupLayout = ->
  # top
  $("#saveButton").button({ icons: { primary: "ui-icon-transferthick-e-w" }})
  $("#shareButton").button({ icons: { primary: "ui-icon-link" }})

  # west
  $("#playButton").button({ icons: { primary: "ui-icon-play" }, text: false })
  $("#timeSlider").slider
    orientation: "horizontal"
    range: "min"
    min: 0
    max: 0
    step: 1
    value: 0
  $("#resetButton").button({ icons: { primary: "ui-icon-arrowreturnthick-1-w" }, text: false })

  $("#west").tabs()
  $("#south").tabs().addClass( "ui-tabs-vertical ui-helper-clearfix" )
  $("#south li").removeClass( "ui-corner-top" ).addClass( "ui-corner-left" )
  $("#south li a").click(handleResize)

  # TODO: look for a callback for tabs
  $('body').layout 
    north__resizable: false
    north__closable: false
    north__size: 50
    west__size: 300
    applyDefaultStyles: true
    onresize: handleResize

setupButtonHandlers = ->
  $("#playButton").on "click", togglePlayMode

  $("#resetButton").on "click", ->
    currentFrame = 0
    currentModel = currentModel.atVersion(0)
    resetLogContent()

    # TODO: move these handlers to MVC events
    $("#timeSlider").slider "option", 
      value: 0
      max: 0

    automaticallyUpdatingModel = true
    editors.modelEditor.setValue(JSON.stringify(currentModel.data, null, MODEL_FORMATTING_INDENTATION))
    # The new content will be selected by default
    editors.modelEditor.selection.clearSelection() 
    automaticallyUpdatingModel = false

    # Execute again
    executeCode()

  $("#timeSlider").on "slide", ->
    currentFrame = $(this).slider("value")

    # If done immediately, will block slider movement, so we postpone it
    GE.doLater ->
      automaticallyUpdatingModel = true
      editors.modelEditor.setValue(JSON.stringify(currentModel.atVersion(currentFrame).data, null, MODEL_FORMATTING_INDENTATION))
      # The new content will be selected by default
      editors.modelEditor.selection.clearSelection() 
      automaticallyUpdatingModel = false

      # Execute again
      executeCode()

# Mode should be something that ACE Editor recognizes, like "ace/mode/javascript"
setupEditor = (id, mode = "") ->
  editor = ace.edit(id)
  if mode then editor.getSession().setMode(mode)
  editor.getSession().setUseWrapMode(true)
  editor.setWrapBehavioursEnabled(true)
  return editor

reloadCode = (callback) ->
  #programId = window.location.search.slice(1)

  currentEvaluator = GE.makeEvaluator(evalLoadedAssets...)

  try
    currentModelData = JSON.parse(editors.modelEditor.getValue())
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Model error. #{error}")
    return showMessage(MessageType.Error, "<strong>Model error.</strong> #{error}")

  try
    currentActions = JSON.parse(editors.actionsEditor.getValue())
    compiledActions = GE.mapObject currentActions, (value, key) ->
      compiledAction = {}
      try
        for actionKey, actionValue of value
          compiledAction[actionKey] = switch actionKey
            when "update" then GE.compileUpdate(actionValue, currentEvaluator)
            when "listActiveChildren" then GE.compileListActiveChildren(actionValue, currentEvaluator)
            when "handleSignals" then GE.compileHandleSignals(actionValue, currentEvaluator)
            else actionValue
        return compiledAction
      catch compilationError
        throw new Error("Error compiling action '#{key}'. #{compilationError}")
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Actions error. #{error}")
    return showMessage(MessageType.Error, "<strong>Actions error.</strong> #{error}")

  try
    currentTools = JSON.parse(editors.toolsEditor.getValue())
    toolsContext = 
      tools: null
      log: null
    compiledTools = GE.mapObject currentTools, (value, key) -> 
      try
        toolFactory = GE.compileTool(value.body, toolsContext, value.args, currentEvaluator)
        return toolFactory(toolsContext)
      catch compilationError
        throw new Error("Error compiling tool '#{key}'. #{compilationError}")
    toolsContext.tools = compiledTools
    toolsContext.log = logWithPrefix
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Tools error. #{error}")
    return showMessage(MessageType.Error, "<strong>Tools error.</strong> #{error}")

  try
    currentLayout = JSON.parse(editors.layoutEditor.getValue())
    # TODO: compile expressions ahead of time
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Layout error. #{error}")
    return showMessage(MessageType.Error, "<strong>Layout error.</strong> #{error}")

  try
    serviceDefs = JSON.parse(editors.servicesEditor.getValue())

    # Destroy old services
    for serviceName, service of currentServices
      service.destroy()

    # Create new services
    currentServices = {}
    for serviceName, serviceDef of serviceDefs
      options = _.defaults serviceDef.options || {},
        elementSelector: '#gameContent'
        size: GAME_DIMENSIONS
      currentServices[serviceName] = services[serviceDef.type](options)
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Services error. #{error}")
    return showMessage(MessageType.Error, "<strong>Services error.</strong> #{error}")

  try
    newAssets = JSON.parse(editors.assetsEditor.getValue())
    updateAssets(newAssets, currentEvaluator)
  catch error
    logWithPrefix(GE.logLevels.ERROR, "Assets error. #{error}")
    return showMessage(MessageType.Error, "<strong>Assets error.</strong> #{error}")

  # TODO: move these handlers to MVC events
  currentModel.atVersion(currentFrame).data = currentModelData

  $("#timeSlider").slider "option", 
    value: currentFrame
    max: currentFrame

  logWithPrefix(GE.logLevels.INFO, "Game updated")
  showMessage(MessageType.Info, "Game updated")
  callback(null)

# Unload previous assets and load new ones. The given list of evaluators are initialized with any JS assets.
# Updates currentAssets.
# TODO: move asset handling into gamEvolve.coffee. CSS could be handled by the HTML service. JS is harder.
updateAssets = (newAssetMap, evaluators...) ->
  # Remove old assets
  # TODO: Only update new assets
  for name, dataUrl of currentAssets
    splitUrl = splitDataUrl(dataUrl)
    if splitUrl.mimeType == "application/javascript"
      # Nothing to do, cannot unload JS!
    else if splitUrl.mimeType == "text/css"
      assetNamesToData[name].remove()
    else if splitUrl.mimeType.indexOf("image/") == 0
      URL.revokeObjectURL(assetNamesToObjectUrls[name])

  assetNamesToData = {}
  assetNamesToObjectUrls = {}

  # Create new assets
  for name, dataUrl of newAssetMap
    splitUrl = splitDataUrl(dataUrl)
    if splitUrl.mimeType == "application/javascript"
      script = atob(splitUrl.data)
      for evaluator in evaluators
        evaluator(script)
    else if splitUrl.mimeType == "text/css"
      css = atob(splitUrl.data)
      assetNamesToData[name] = $('<style type="text/css"></style>').html(css).appendTo("head")
    else if splitUrl.mimeType.indexOf("image/") == 0
      blob = Util.dataURLToBlob(dataUrl)
      objectUrl = URL.createObjectURL(blob)

      image = new Image()
      image.src = objectUrl
      # TODO: verify that images loaded correctly?

      assetNamesToObjectUrls[name] = objectUrl
      assetNamesToData[name] = image
    else
      assetNamesToData[name] = atob(splitUrl.data)

  currentAssets = newAssetMap

# Runs the currently loaded code on the current frame
# Returns a new model
executeCode = ->
  modelAtFrame = currentModel.atVersion(currentFrame)
{'fraction_non_alphanumeric': 0.12235003746308784, 'fraction_numerical': 0.00546520340252986, 'mean_word_length': 3.8555531778300876, 'pattern_counts': {'":': 14, '<': 31, '<?xml version=': 0, '>': 67, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '886262', 'n_tokens_mistral': 7032, 'n_tokens_neox': 6797, 'n_words': 1625}