var ex = this

var input = use('input')

var DEAD = Symbol()
var GARBAGE = Symbol()
var FILE = Symbol()
var TIMERS = Symbol()
var REGGIES = Symbol()
var UNDERLINGS = Symbol()
var OVERLING = Symbol()

function add_timer(obj, fn, seconds) {
  var timers = obj[TIMERS]
  
  var stop = function () {
    if (!timer) return
    timers.delete(stop)
    timer.fn = null
    timer = null
  }

  function execute() {
    if (fn) timer.remain = fn(stop.seconds)
    if (!timer) return
    if (!timer.remain) stop()
    else stop.seconds = timer.remain
  }

//  var timer = os.make_timer(execute)
  timer.remain = seconds

  stop.remain = seconds
  stop.seconds = seconds
  
  timers.push(stop)
  return stop
}

globalThis.Register = {
  registries: [],

  add_cb(name) {
    var n = {}
    var fns = []

    n.register = function (fn, oname) {
      if (typeof fn != 'function') return

      var dofn = function (...args) {
        fn(...args)
      }
      Object.defineProperty(dofn, 'name', {value:`do_${oname}`})

      var left = 0
      var right = fns.length - 1
      dofn.layer = fn.layer
      dofn.layer ??= 0

      while (left <= right) {
        var mid = Math.floor((left + right) / 2)
        if (fns[mid] == dofn.layer) {
          left = mid
          break
        } else if (fns[mid].layer < dofn.layer) left = mid + 1
        else right = mid - 1
      }

      fns.splice(left, 0, dofn)
      return function () {
        fns.delete(dofn)
      }
    }

    prosperon[name] = function (...args) {
      fns.forEach(fn => {
        fn(...args)
      })
    }
    Object.defineProperty(prosperon[name], 'name', {value:name})
    prosperon[name].fns = fns

    n.clear = function () {
      fns = []
    }

    Register[name] = n
    Register.registries[name] = n
    return n
  },
}

Register.pull_registers = function pull_registers(obj) {
  var reggies = []
  for (var reg in Register.registries) {
    if (typeof obj[reg] == "function")
      reggies.push(reg)
  }
  return reggies
}

Register.register_obj = function register_obj(obj, reg) {
  var fn = obj[reg].bind(obj)
  fn.layer = obj[reg].layer
  var name = obj.ur ? obj.ur.name : obj.toString()
  obj[TIMERS].push(Register.registries[reg].register(fn, name))
  if (!obj[reg].name) Object.defineProperty(obj[reg], 'name', {value:`${obj._file}_${reg}`})
}

Register.check_registers = function check_registers(obj) {
  if (obj[REGGIES]) {
    if (obj[REGGIES].length == 0) return
    for (var reg of obj[REGGIES])
      Register.register_obj(obj,reg)
    return
  }
  for (var reg in Register.registries) {
    if (typeof obj[reg] == "function")
      Register.register_obj(obj,reg)
  }
}

Register.add_cb("appupdate")
Register.add_cb("update").doc = "Called once per frame."
Register.add_cb("physupdate")
Register.add_cb("gui")
Register.add_cb("hud")
Register.add_cb("draw")
Register.add_cb("imgui")
Register.add_cb("app")


var actor = {}

actor.toString = function() { return this[FILE] }

actor.spawn = function spawn(script, config, actor_context) {
  if (this[DEAD]) throw new Error("Attempting to spawn on a dead actor")
  var prog
  if (!script) {
    prog = {}
    prog.module_ret = {}
    prog.prog_fn = function() {}
  } else {
    prog = script_fn(script)
    if (!prog.prog_fn) throw new Error(`Script ${script} is not an actor script or has no actor component`)
  }

  var underling
  prog.module_ret.__proto__ = actor
  underling = Object.create(prog.module_ret)
  underling[OVERLING] = this
  underling[FILE] = script
  underling[TIMERS] = []
  underling[UNDERLINGS] = new Set()

  // Make $_ available to the actor (either passed context or the engine's $_)
  var actor_dollar = actor_context || $_
  Object.defineProperty(underling, '$_', {
    value: actor_dollar,
    writable:false,
    enumerable:false,
    configurable:false
  })

  Object.defineProperty(underling, 'overling', {
    get() { return this[OVERLING] },
    enumerable:true,
    configurable:false
  })

  Object.defineProperty(underling, 'underlings', {
    get() { return new Set(this[UNDERLINGS]) },
    enumerable:true,
    configurable:false
  })

  Object.defineProperty(underling, 'spawn', {
    value: function(script, config) {
      return actor.spawn.call(this, script, config, actor_dollar)
    },
    writable:false,
    enumerable:true,
    configurable:false
  })

  Object.defineProperty(underling, 'kill', {
    value: actor.kill,
    writable:false,
    enumerable:true,
    configurable:false
  })

  Object.defineProperty(underling, 'delay', {
    value: actor.delay,
    writable:false,
    enumerable:true,
    configurable:false
  })

  try {
    // Pass $_ as a parameter to actor scripts
    prog.prog_fn.call(underling, actor_dollar)
  } catch(e) { throw e; }

  if (underling[DEAD]) return null;

  if (typeof config == 'object') Object.assign(underling, config)

  if (!underling[REGGIES])
    underling.__proto__[REGGIES] = Register.pull_registers(underling)

  Register.check_registers(underling)
  if (underling.awake) underling.awake()

  this[UNDERLINGS].add(underling)
  if (underling.tag) act.tag_add(underling.tag, underling)

  underling[GARBAGE] = underling.garbage
  return underling
}

actor.clear = function actor_clear() {
  this[UNDERLINGS].forEach(p => {
    p.kill()
  })
  this[UNDERLINGS].clear()
}

actor.kill = function kill() {
  if (this[DEAD]) return
  this[DEAD] = true
  this[TIMERS].slice().forEach(t => t())
  delete this[TIMERS]
  input.do_uncontrol(this)

  this.clear()
  this[OVERLING][UNDERLINGS].delete(this)
  delete this[UNDERLINGS]

  if (typeof this.garbage == "function") this.garbage()
  if (typeof this.then == "function") this.then()

  act.tag_clear_guid(this)
}
actor.kill.doc = `Remove this actor and all its underlings from existence.`

actor.delay = function(fn, seconds) {
  if (this[DEAD]) return
  add_timer(this, fn, seconds)
}
actor.delay.doc = `Call 'fn' after 'seconds' with 'this' set to the actor.`

actor[UNDERLINGS] = new Set()


ex[cell.DOC] = `
A set of utilities for iterating over a hierarchy of actor-like objects, as well
as managing tag-based lookups. Objects are assumed to have a "objects" property,
pointing to children or sub-objects, forming a tree. 
`

function eachobj(obj, fn) {
  var val = fn(obj)
  if (val) return val
  for (var o in obj.objects) {
    if (obj.objects[o] == obj) log.error(`Object ${obj.toString()} is referenced by itself.`)
    val = eachobj(obj.objects[o], fn)
    if (val) return val
  }
}

ex.all_objects = function (fn, startobj = world) {
  return eachobj(startobj, fn)
}
ex.all_objects[cell.DOC] = `
:param fn: A callback function that receives each object. If it returns a truthy value, iteration stops and that value is returned.
:param startobj: The root object at which iteration begins, default is the global "world".
:return: The first truthy value returned by fn, or null if none.
Iterate over each object (and its sub-objects) in the hierarchy, calling fn for each one. 
`

ex.find_object = function (fn, startobj = world) {}
ex.find_object[cell.DOC] = `
:param fn: A callback or criteria to locate a particular object.
:param startobj: The root object at which search begins, default "world".
:return: Not yet implemented. 
Intended to find a matching object within the hierarchy. 
`

var gtags = {}

ex.tag_add = function (tag, obj) {
  gtags[tag] ??= new Set()
  gtags[tag].add(obj)
}
ex.tag_add[cell.DOC] = `
:param tag: A string tag to associate with the object.
:param obj: The object to add under this tag.
:return: None
Associate the given object with the specified tag. Creates a new tag set if it does not exist.
`

ex.tag_rm = function (tag, obj) {
  delete gtags[tag].delete(obj)
}
ex.tag_rm[cell.DOC] = `
:param tag: The tag to remove the object from.
:param obj: The object to remove from the tag set.
:return: None
Remove the given object from the specified tag’s set, if it exists.
`

ex.tag_clear_guid = function (obj) {
  for (var tag in gtags) gtags[tag].delete(obj)
}
ex.tag_clear_guid[cell.DOC] = `
:param obj: The object whose tags should be cleared.
:return: None
Remove the object from all tag sets.
`

ex.objects_with_tag = function (tag) {
  if (!gtags[tag]) return []
  return Array.from(gtags[tag])
}
ex.objects_with_tag[cell.DOC] = `
:param tag: A string tag to look up.
:return: An array of objects associated with the given tag. 
Retrieve all objects currently tagged with the specified tag.
`

function parse_file(content, file) {
  if (!content) return {}
  if (content.match()
  if (!/^\s*---\s*$/m.test(content)) {
    var part = content.trim()
    if (part.match(/return\s+[^;]+;?\s*$/)) {
      return { module: part }
    }
    return { program: part }
  }
  var parts = content.split(/\n\s*---\s*\n/)
  var module = parts[0]
  if (!/\breturn\b/.test(module))
    throw new Error(`Malformed file: ${file}. Module section must end with a return statement.`)

  try {
    new Function(module)()
  } catch (e) {
    throw new Error(`Malformed file: ${file}. Module section must end with a return statement.\n` + e.message)
  }

  var pad = '\n'.repeat(module.split('\n').length + 4)
  return {
    module,
    program: pad + parts[1]
  }
}

// path is the path of a module or script to resolve
var script_fn = function script_fn(path, args) {
  var parsed = {}
  var file = resources.find_script(path)
  
  if (!file) {
    parsed.module_ret = bare_load(path)
    if (!parsed.module_ret) throw new Error(`Module ${path} could not be created`)
    return parsed
  }

  var content = io.slurp(file)
  var parsed = parse_file(content, file)
  var module_name = file.name()
  parsed.module_ret = bare_load(path)
  parsed.module_ret ??= {}

  if (parsed.module) {
    // Create a context object with args
    var context = Object.create(parsed.module_ret)
    context.__args__ = args || []
    var mod_script = `(function setup_${module_name}_module(){ var self = this; var $ = this; var exports = {}; var module = {exports: exports}; var define = null; var arg = this.__args__; ${parsed.module}})`
    var module_fn = js.eval(file, mod_script)
    parsed.module_ret = module_fn.call(context)
    if (parsed.module_ret == null || parsed.module_ret == null)
      throw new Error(`Module ${module_name} must return a value`)
    parsed.module_fn = module_fn
  }
  
  parsed.program ??= ""
  var prog_script = `(function use_${module_name}($_) { var self = this; var $ = this.__proto__; ${parsed.program}})`
  parsed.prog_fn = js.eval(file, prog_script)

  return parsed
}.hashify()

return ex
