Show container ID better

Show container ID better

#!/usr/bin/env moon

-- Copyright (C) 2014 Sam Dodrill <xena@yolo-swag.com> All rights reserved.
--
-- This software is provided 'as-is', without any express or implied
-- warranty. In no event will the authors be held liable for any damages
-- arising from the use of this software.
--
-- Permission is granted to anyone to use this software for any purpose,
-- including commercial applications, and to alter it and redistribute it
-- freely, subject to the following restrictions:
--
-- 1. The origin of this software must not be misrepresented; you must not
--    claim that you wrote the original software. If you use this software
--    in a product, an acknowledgment in the product documentation would be
--    appreciated but is not required.
--
-- 2. Altered source versions must be plainly marked as such, and must not be
--    misrepresented as being the original software.
--
-- 3. This notice may not be removed or altered from any source
--    distribution.

export yaml = require "yaml"

--- yaml2Table loads a yaml file as text and returns it as a file.
--  Returns a table representing the yaml document and an error string or nil.
yaml2Table = (fname) -> --> table, error
  fin = io.open fname, "r"
  if fin == nil
    return {}, "File read (#{fname}) failed."

  data = fin\read "*all"
  ret = yaml.load data

  fin\close!

  return ret, nil

--- doCommand returns the output and return status of a command.
doCommand = (command) -> --> string, number
  n = os.tmpname!
  code = os.execute command .. " > " .. n
  lines = {}

  for line in io.lines n
    table.insert lines, line

  os.remove n

  lines, code

--- pairsByKeys is an iterator for a table aplhabetically
--  adapted from here: http://www.lua.org/pil/19.3.html
pairsByKeys = (t) -> --> function
  a = {}
  for n in pairs t
    table.insert a, n

  table.sort a

  i = 0
  iter = ->
    i += 1
    if a[i] == nil
      return nil
    else
      return a[i], t[a[i]]
  iter

export commands = {
  up: {"Brings up a development container", ->
    dcommand = "docker run -idt --name #{data.projname}-dev --hostname dev:#{data.projname} "
    path = "/home/#{data.user}/dev/"
    localdir = os.getenv "PWD"

    -- Is this go?
    if data.golang
      path = "/home/#{data.user}/go/src/#{repopath}"

    -- Add source code directory to mount
    dcommand ..= "-v #{localdir}:#{path} "

    -- Add ssh keys if needed
    if data.ssh
      dcommand ..= "-v /home/#{os.getenv "USER"}/.ssh:/home/#{data.user}/.ssh "

    image = data.base
    if data.base == nil and data.overlay ~= nil
      image = "dev-#{data.projname}"

    -- Append image name
    dcommand ..= image

    print "Starting up container for #{data.projname}"

    -- Start up the docker container
    lines, status = doCommand dcommand
    if status ~= 0
      print "Launch failed. Does this container already exist?"
      for _,line in pairs lines
        print "docker: " .. line
      os.exit status

    ctid = lines[#lines]

    -- report to user
    print "#{data.projname}-dev (ctid: #{ctid\sub(1,12)}) running!"
    print "To use this container please attach to it with:"
    print "    $ docker attach #{data.projname}-dev"
  }

  down: { "Destroys a development container", ->
    lines, status = doCommand "docker rm -f #{data.projname}-dev"
    if status ~= 0
      os.exit status

    print "Container destroyed."
  }

  establish: { "Create a Docker image from the manifest", ->
    if data.overlay == nil
      print "Error: docker container overlay not made. Exiting."
      os.exit 1

    dir = os.tmpname!
    os.remove dir

    out, err = doCommand "mkdir #{dir}"
    if err ~= 0
      print "Could not make temporary directory #{dir}. Dying."
      os.exit err

    fout = io.open "#{dir}/Dockerfile", "w"
    fout\write data.overlay
    fout\close!

    proc = io.popen "docker build -t dev-#{data.projname} #{dir}"
    for line in proc\lines!
      print line

    proc\close!

    _, err = doCommand "rm -rf #{dir}"
    if err ~= 0
      os.exit err

    print "Docker image dev-#{data.projname} created."
  }
}

if #arg == 0
  print "dev version 0.2\n"

  print "Usage: dev [command] <manifest>\n"

  print "  if manifest is undefined the default value"
  print "  .dev.yaml will be used.\n"

  print "Available commands:"
  for name,cmd in pairsByKeys commands
    print "%12s   %s"\format(name, cmd[1])

  os.exit 1

command = arg[1]\lower!

if commands[command] ~= nil
  export fname = arg[2]
  if fname == nil
    fname = ".dev.yaml"

  export data, err = yaml2Table fname
  if err ~= nil
    print "Cannot open file #{fname}. Please make sure this file exists or is readable."
    os.exit 1

  commands[command][2]!

#!/usr/bin/env moon

-- Copyright (C) 2014 Sam Dodrill <xena@yolo-swag.com> All rights reserved.
--
-- This software is provided 'as-is', without any express or implied
-- warranty. In no event will the authors be held liable for any damages
-- arising from the use of this software.
--
-- Permission is granted to anyone to use this software for any purpose,
-- including commercial applications, and to alter it and redistribute it
-- freely, subject to the following restrictions:
--
-- 1. The origin of this software must not be misrepresented; you must not
--    claim that you wrote the original software. If you use this software
--    in a product, an acknowledgment in the product documentation would be
--    appreciated but is not required.
--
-- 2. Altered source versions must be plainly marked as such, and must not be
--    misrepresented as being the original software.
--
-- 3. This notice may not be removed or altered from any source
--    distribution.

export yaml = require "yaml"

--- yaml2Table loads a yaml file as text and returns it as a file.
--  Returns a table representing the yaml document and an error string or nil.
yaml2Table = (fname) -> --> table, error
  fin = io.open fname, "r"
  if fin == nil
    return {}, "File read (#{fname}) failed."

  data = fin\read "*all"
  ret = yaml.load data

  fin\close!

  return ret, nil

--- doCommand returns the output and return status of a command.
doCommand = (command) -> --> string, number
  n = os.tmpname!
  code = os.execute command .. " > " .. n
  lines = {}

  for line in io.lines n
    table.insert lines, line

  os.remove n

  lines, code

--- pairsByKeys is an iterator for a table aplhabetically
--  adapted from here: http://www.lua.org/pil/19.3.html
pairsByKeys = (t) -> --> function
  a = {}
  for n in pairs t
    table.insert a, n

  table.sort a

  i = 0
  iter = ->
    i += 1
    if a[i] == nil
      return nil
    else
      return a[i], t[a[i]]
  iter

export commands = {
  up: {"Brings up a development container", ->
    dcommand = "docker run -idt --name #{data.projname}-dev --hostname dev:#{data.projname} "
    path = "/home/#{data.user}/dev/"
    localdir = os.getenv "PWD"

    -- Is this go?
    if data.golang
      path = "/home/#{data.user}/go/src/#{repopath}"

    -- Add source code directory to mount
    dcommand ..= "-v #{localdir}:#{path} "

    -- Add ssh keys if needed
    if data.ssh
      dcommand ..= "-v /home/#{os.getenv "USER"}/.ssh:/home/#{data.user}/.ssh "

    image = data.base
    if data.base == nil and data.overlay ~= nil
      image = "dev-#{data.projname}"

    -- Append image name
    dcommand ..= image

    print "Starting up container for #{data.projname}"

    -- Start up the docker container
    lines, status = doCommand dcommand
    if status ~= 0
      print "Launch failed. Does this container already exist?"
      for _,line in pairs lines
        print "docker: " .. line
      os.exit status

    ctid = lines[#lines]

    -- report to user
    print "#{data.projname}-dev (#{ctid\sub(1,6)}) running!"
    print "To use this container please attach to it with:"
    print "    $ docker attach #{data.projname}-dev"
  }

  down: { "Destroys a development container", ->
    lines, status = doCommand "docker rm -f #{data.projname}-dev"
    if status ~= 0
      os.exit status

    print "Container destroyed."
  }

  establish: { "Create a Docker image from the manifest", ->
    if data.overlay == nil
      print "Error: docker container overlay not made. Exiting."
      os.exit 1

    dir = os.tmpname!
    os.remove dir

    out, err = doCommand "mkdir #{dir}"
    if err ~= 0
      print "Could not make temporary directory #{dir}. Dying."
      os.exit err

    fout = io.open "#{dir}/Dockerfile", "w"
    fout\write data.overlay
    fout\close!

    proc = io.popen "docker build -t dev-#{data.projname} #{dir}"
    for line in proc\lines!
      print line

    proc\close!

    _, err = doCommand "rm -rf #{dir}"
    if err ~= 0
      os.exit err

    print "Docker image dev-#{data.projname} created."
  }
}

if #arg == 0
  print "dev version 0.2\n"

  print "Usage: dev [command] <manifest>\n"

  print "  if manifest is undefined the default value"
  print "  .dev.yaml will be used.\n"

  print "Available commands:"
  for name,cmd in pairsByKeys commands
    print "%12s   %s"\format(name, cmd[1])

  os.exit 1

command = arg[1]\lower!

if commands[command] ~= nil
  export fname = arg[2]
  if fname == nil
    fname = ".dev.yaml"

  export data, err = yaml2Table fname
  if err ~= nil
    print "Cannot open file #{fname}. Please make sure this file exists or is readable."
    os.exit 1

  commands[command][2]!


fixed utility functions for changing username/pw in CustomUsersApplication

import hmac_sha1, encode_base64 from require "lapis.util.encoding"
UsersApplication=require "lib.lazuli.src.lazuli.modules.user_management"
Profiles = require "models.profiles"
ACLs = require "models.acls"
ACL_Entries = require "models.acl_entries"
config = (require "lapis.config").get!
csrf = require "lapis.csrf"
Users = require "lazuli.modules.user_management.models.users"

prepareUserProfile=(user)->
  profile=Profiles\getOrCreateByUser user
  nobodyACL=ACLs\create {
    user_id: user.id
    name: "nobody"
    default_policy: false
  }
  everyoneACL=ACLs\create {
    user_id: user.id
    name: "everyone"
    default_policy: true
  }
  usersACL=ACLs\create {
    user_id: user.id
    name: "logged in users"
    default_policy: false
  }
  ACL_Entries\create {
    acl_id: usersACL.id
    target_type: ACL_Entries.target_types.include
    target_id: ACL_Entries.special_targets.include_logged_in
    policy: true
  }
  -- TODO: default ACLs, etc.

class CustomUsersApplication extends UsersApplication
  @before_filter =>
    if config.projectStage=="alpha" or config.projectStage=="beta"
      if @req.parsed_url.path=="/users/register/do" and @req.params_post.key~=encode_base64 hmac_sha1 config.secret, @req.params_post.username
        @write redirect_to: @url_for "index"
    @modules.user_management or={}
    @session.modules.user_management or={}
    if not @modules.user_management.providers
      if type(config.modules.user_management)=="table" and type(config.modules.user_management.providers)=="table"
        @modules.user_management.providers={k,require(k)(@,k,v) for k,v in pairs config.modules.user_management.providers}
      else
        @modules.user_management.providers={}
    @modules.user_management.csrf_token = csrf.generate_token @, "lazuli_modules_usermanagement"
    if @session.modules.user_management.currentuser and not @modules.user_management.currentuser
      @modules.user_management.currentuser = Users\find @session.modules.user_management.currentuser
  @superroute logout: "/logout"
  @superroute login: "/login"
  @superroute register: "/register"
  @superroute register_do: "/register/do", (ret)=>
    if (ret.status ~= 403) and @modules.user_management.currentuser
      prepareUserProfile @modules.user_management.currentuser
    ret
  @superroute login_do: "/login/do", (ret)=>
    if @modules.user_management.currentuser
      if @session.login_redirect
        red=@session.login_redirect
        @session.login_redirect=nil
        redirect_to: red
      else
        redirect_to: @url_for "profile_show", id: @modules.user_management.currentuser.id
    else
      ret

  @change_user_name: (user, newname, password)->
    if type(user)=="number"
      user=Users\find user
    return nil, "user not found" unless user
    oldhash=encode_base64(hmac_sha1(password,user.username..password))
    newhash=encode_base64(hmac_sha1(password,newname..password))
    return nil, "wrong password" unless user.pwHMACs1==oldhash
    user\update{
      username: newname
      pwHMACs1: newhash
    }

  @change_password: (user, newpassword)->
    if type(user)=="number"
      user=Users\find user
    return nil, "user not found" unless user
    newhash=encode_base64(hmac_sha1(newpassword,user.username..newpassword))
    user\update{
      pwHMACs1: newhash
    }

import hmac_sha1, encode_base64 from require "lapis.util.encoding"
UsersApplication=require "lib.lazuli.src.lazuli.modules.user_management"
Profiles = require "models.profiles"
ACLs = require "models.acls"
ACL_Entries = require "models.acl_entries"
config = (require "lapis.config").get!
csrf = require "lapis.csrf"

prepareUserProfile=(user)->
  profile=Profiles\getOrCreateByUser user
  nobodyACL=ACLs\create {
    user_id: user.id
    name: "nobody"
    default_policy: false
  }
  everyoneACL=ACLs\create {
    user_id: user.id
    name: "everyone"
    default_policy: true
  }
  usersACL=ACLs\create {
    user_id: user.id
    name: "logged in users"
    default_policy: false
  }
  ACL_Entries\create {
    acl_id: usersACL.id
    target_type: ACL_Entries.target_types.include
    target_id: ACL_Entries.special_targets.include_logged_in
    policy: true
  }
  -- TODO: default ACLs, etc.

class CustomUsersApplication extends UsersApplication
  @before_filter =>
    if config.projectStage=="alpha" or config.projectStage=="beta"
      if @req.parsed_url.path=="/users/register/do" and @req.params_post.key~=encode_base64 hmac_sha1 config.secret, @req.params_post.username
        @write redirect_to: @url_for "index"
    @modules.user_management or={}
    @session.modules.user_management or={}
    if not @modules.user_management.providers
      if type(config.modules.user_management)=="table" and type(config.modules.user_management.providers)=="table"
        @modules.user_management.providers={k,require(k)(@,k,v) for k,v in pairs config.modules.user_management.providers}
      else
        @modules.user_management.providers={}
    @modules.user_management.csrf_token = csrf.generate_token @, "lazuli_modules_usermanagement"
    if @session.modules.user_management.currentuser and not @modules.user_management.currentuser
      @modules.user_management.currentuser = Users\find @session.modules.user_management.currentuser
  @superroute logout: "/logout"
  @superroute login: "/login"
  @superroute register: "/register"
  @superroute register_do: "/register/do", (ret)=>
    if (ret.status ~= 403) and @modules.user_management.currentuser
      prepareUserProfile @modules.user_management.currentuser
    ret
  @superroute login_do: "/login/do", (ret)=>
    if @modules.user_management.currentuser
      if @session.login_redirect
        red=@session.login_redirect
        @session.login_redirect=nil
        redirect_to: red
      else
        redirect_to: @url_for "profile_show", id: @modules.user_management.currentuser.id
    else
      ret

  @change_user_name: (user, newname, password)->
    if type(user)=="number"
      user=Users\find user
    return nil, "user not found" unless user
    oldhash=encode_base64(hmac_sha1(password,user.username..password))
    newhash=encode_base64(hmac_sha1(password,newname..password))
    return nil, "wrong password" unless user.pwHMACs1==oldhash
    user\update{
      username: newname
      pwHMACs1: newhash
    }

  @change_password: (user, newpassword)->
    if type(user)=="number"
      user=Users\find user
    return nil, "user not found" unless user
    newhash=encode_base64(hmac_sha1(newpassword,user.username..newpassword))
    user\update{
      pwHMACs1: newhash
    }


Added tests for Entity setHitbox & setOrigin

require "busted"
require "tests.mock_love"

require "LunoPunk.Entity"
require "LunoPunk.Graphic"
require "LunoPunk.Mask"
require "LunoPunk.utils.moonscript"
require "LunoPunk.geometry.Rectangle"

entity_sanity_check = ->
	e = Entity!
	assert.are.equal 0, e\x!
	assert.are.equal 0, e\y!
	e

describe "Entity", ->
	e = nil
	before_each ->
		e = entity_sanity_check!

	it "construct", ->
		assert.are.equal true, e.active
		assert.are.equal false, e.autoClear
		assert.are.equal 0, e\x!
		assert.are.equal 0, e\y!
		assert.are.equal 0, e.originX
		assert.are.equal 0, e.originY
		assert.are.equal 0, e.width
		assert.are.equal 0, e.height
		assert.are.equal true, e.visible
		assert.are.equal true, e.collidable
		assert.are.equal false, e.followCamera

		-- Follow Camera coverage
		e.followCamera = true
		assert.are.equal 0, e\x!
		assert.are.equal 0, e\y!
		LP.camera.x += 1
		assert.are.equal 1, e\x!
		assert.are.equal 0, e\y!
		LP.camera.y -= 1
		assert.are.equal -1, e\y!
		e.followCamera = false
		assert.are.equal 0, e\x!
		assert.are.equal 0, e\y!

		e = Entity 10, 15
		assert.are.equal 10, e\x!
		assert.are.equal 15, e\y!
		assert.are.equal 0, e.originX
		assert.are.equal 0, e.originY
		assert.are.equal 0, e.width
		assert.are.equal 0, e.height

		g = Graphic!
		g.x, g.y = 25, 30
		e = Entity 10, 15, g
		assert.are.equal g, e\graphic!
		assert.are.equal 10, e\x!
		assert.are.equal 15, e\y!
		assert.are.equal 0, e.originX
		assert.are.equal 0, e.originY
		assert.are.equal 0, e.width
		assert.are.equal 0, e.height

		m = Mask!
		e = Entity 10, 15, g, m
		assert.are.equal m, e\mask!
		assert.are.equal 10, e\x!
		assert.are.equal 15, e\y!
		assert.are.equal 0, e.originX
		assert.are.equal 0, e.originY
		assert.are.equal 0, e.width
		assert.are.equal 0, e.height

	it "accessors", ->
		assert.are.equal 0, e\halfWidth!
		e.width = 10
		assert.are.equal 5, e\halfWidth!

		assert.are.equal 0, e\halfHeight!
		e.height = 20
		assert.are.equal 10, e\halfHeight!

		assert.are.equal 5, e\centerX!
		assert.are.equal 10, e\centerY!
		e.originX = 10
		assert.are.equal -5, e\centerX!
		e.originY = 10
		assert.are.equal 0, e\centerY!
		e\x 5
		assert.are.equal 0, e\centerX!
		e\y 10
		assert.are.equal 10, e\centerY!

		assert.are.equal -5, e\left!
		assert.are.equal 5, e\right!
		assert.are.equal 0, e\top!
		assert.are.equal 20, e\bottom!

		-- No scene
		assert.is.False e\onCamera!

		l = 5
		assert.is.Nil e\scene!
		assert.are.equal 10, e\layer!
		assert.are.equal l, e\layer l
		assert.are.equal 10, e\layer 10
		e\layer l
		assert.are.equal LP.BASELAYER!, e\layer false

		t = "Entity"
		assert.are.equal '', e\type!
		assert.are.equal t, e\type t
		assert.are.equal t, e\type!
		assert.are.equal t, e\type nil
		assert.are.equal '', e\type ''

		m = Mask!
		assert.is.Nil e\mask!
		assert.are.equal m, e\mask m
		assert.are.equal m, e\mask!
		assert.are.equal e, m.parent
		assert.are.equal m, e\mask nil
		assert.is.Nil e\mask false
		assert.is.Nil m.parent
		assert.is.Nil e\mask!

		g = Graphic!
		assert.is.Nil e\graphic!
		assert.are.equal g, e\graphic g
		assert.are.equal g, e\graphic!
		assert.is.Nil e\graphic false

		s = Scene!
		assert.is.Nil e\scene!
		assert.is.Nil e\world!
		assert.are.equal s, e\scene s
		assert.are.equal s, e\scene!
		assert.are.equal s, e\world!
		assert.is.Nil e\scene false
		assert.are.equal s, e\world s
		e\scene false

		-- Come back to some that branch if scene is set
		e\scene s
		assert.are.equal t, e\type t
		-- Hits a branch if e\type! is not ""
		assert.are.equal t, e\type t
		assert.are.equal l, e\layer l
		-- Hits a branch if e\layer! is not nil
		assert.are.equal l, e\layer l

		pending "Entity\\onCamera after scene is set"

	it "collidable", ->
		e = with Entity 5, 5
			.width = 10
			.height = 10
			\type "Entity"

		-- Collider
		c = with Entity 5, 5
			.width = 10
			.height = 10
			\type "Entity"

		-- No Scene
		assert.is.Nil e\collide "Entity", 10, 10

		LP.scene = Scene!
		LP.scene\add e
		LP.scene\add c
		LP.scene\updateLists!

		-- Not collidable
		e.collidable = false
		assert.is.Nil e\collide "Entity", 10, 10

		-- A collision
		e.collidable = true
		assert.are.equal c, e\collide "Entity", 10, 10
		assert.are.equal e, c\collide "Entity", 10, 10

		-- Bounds
		assert.are.equal c, e\collide "Entity", 5 - c.width, 5
		assert.are.equal c, e\collide "Entity", 5, 5 - c.height
		assert.are.equal e, c\collide "Entity", 5, 5
		assert.are.equal c, e\collide "Entity", 15, 15
		assert.are.equal e, c\collide "Entity", 15, 15

		-- Not a collision
		assert.is.Nil e\collide "Entity", 4 - c.width, 5
		assert.is.Nil e\collide "Entity", 5, 4 - c.height
		assert.is.Nil c\collide "Entity", 4 - e.width, 5
		assert.is.Nil c\collide "Entity", 5, 4 - e.height
		assert.is.Nil e\collide "Entity", 16, 15
		assert.is.Nil e\collide "Entity", 15, 16
		assert.is.Nil c\collide "Entity", 16, 15
		assert.is.Nil c\collide "Entity", 15, 16

		-- Mask collision
		m = Mask!
		e\mask Mask!
		c\mask Mask!
		assert.are.equal c, e\collide "Entity", 10, 10
		assert.are.equal e, c\collide "Entity", 10, 10

		-- Not a collision
		assert.is.Nil e\collide "Entity", 30, 30
		assert.is.Nil e\collide "Entity", 16, 16
		assert.are.equal c, e\collide "Entity", 15, 15

	it "setHitbox/setHitboxTo", ->
		e.width, e.height = 5, 5

		c = (w, h, x, y) ->
			assert.are.equal w, e.width
			assert.are.equal h, e.height
			assert.are.equal x, e.originX
			assert.are.equal y, e.originY

		-- Default
		c 5, 5, 0, 0

		e\setHitbox 5, 5
		c 5, 5, 0, 0

		e\setHitbox 10, 10, 2, 2
		c 10, 10, 2, 2

		r = Rectangle 2, 2, 5, 5
		e\setHitboxTo r
		c 5, 5, -2, -2

		r = { width: 10, height: 10, originX: 0, originY: 0}
		e\setHitboxTo r
		c 10, 10, 0, 0

	it "centerOrigin/setOrigin", ->
		e.width, e.height = 10, 10
		assert.are.equal 0, e.originX
		assert.are.equal 0, e.originY

		e\centerOrigin!
		assert.are.equal 5, e.originX
		assert.are.equal 5, e.originY

		e\setOrigin 0, 0
		assert.are.equal 0, e.originX
		assert.are.equal 0, e.originY

	it "distanceFrom", ->
		e2 = with Entity 20, 0
			.width = 10
			.height = 10

		e.width, e.height = 10, 10

		assert.are.equal 20, e\distanceFrom e2
		assert.are.equal 20, e2\distanceFrom e

		e2\moveTo 0, 20
		assert.are.equal 20, e\distanceFrom e2
		assert.are.equal 20, e2\distanceFrom e

		e2\moveTo 0, 0
		assert.are.equal 0, e\distanceFrom e2
		assert.are.equal 0, e2\distanceFrom e
		assert.are.equal 0, e\distanceFrom e2, true
		assert.are.equal 0, e2\distanceFrom e, true

		e2\moveTo 20, 0
		assert.are.equal 10, e\distanceFrom e2, true
		assert.are.equal 10, e2\distanceFrom e, true

		e2\moveTo 0, 20
		assert.are.equal 10, e\distanceFrom e2, true
		assert.are.equal 10, e2\distanceFrom e, true

	it "distanceToPoint", ->
		e.width, e.height = 10, 10

		assert.are.equal 20, e\distanceToPoint 20, 0
		assert.are.equal 20, e\distanceToPoint 0, 20
		assert.are.equal 0, e\distanceToPoint 0, 0

		assert.are.equal 10, e\distanceToPoint 20, 0, true
		assert.are.equal 10, e\distanceToPoint 0, 20, true
		assert.are.equal 0, e\distanceToPoint 0, 0, true

		assert.are.equal 10, e\distanceToPoint -10, 0
		assert.are.equal 10, e\distanceToPoint 0, -10
		assert.are.equal 10, e\distanceToPoint -10, 0, true
		assert.are.equal 10, e\distanceToPoint 0, -10, true

	it "distanceToRect", ->
		e.width, e.height = 10, 10

		assert.are.equal 10, e\distanceToRect 20, 0, 10, 10
		assert.are.equal 10, e\distanceToRect 0, 20, 10, 10
		assert.are.equal 0, e\distanceToRect 0, 0, 10, 10

		assert.are.equal 10, e\distanceToRect -20, 0, 10, 10
		assert.are.equal 10, e\distanceToRect 0, -20, 10, 10
		assert.are.equal 10, e\distanceToRect -20, 0, 10, 10
		assert.are.equal 10, e\distanceToRect 0, -20, 10, 10

	it "toString", ->
		assert.are.equal e\toString!, "Entity"
		assert.are.equal tostring(e), "Entity"

		class Ball extends Entity
		b = Ball!
		assert.are.equal b\toString!, "Ball"
		-- The metatable for __tostring is on the parent, not the child
		assert.are_not.equal tostring(b), "Ball"

	it "moveBy", ->
		e\moveBy 1, 0
		assert.are.equal 1, e\x!
		assert.are.equal 0, e\y!

		e\moveBy 0, 1
		assert.are.equal 1, e\x!
		assert.are.equal 1, e\y!

		e\moveBy 1, 1
		assert.are.equal 2, e\x!
		assert.are.equal 2, e\y!

		e\moveBy -1, -1
		assert.are.equal 1, e\x!
		assert.are.equal 1, e\y!

		e\moveBy 0, 0
		assert.are.equal 1, e\x!
		assert.are.equal 1, e\y!

		pending "Entity\\moveBy solidType and sweep"

	it "moveTo", ->
		e\moveTo 5, 0
		assert.are.equal 5, e\x!
		assert.are.equal 0, e\y!

		e\moveTo 0, 5
		assert.are.equal 0, e\x!
		assert.are.equal 5, e\y!

		e\moveTo 0, 0
		assert.are.equal 0, e\x!
		assert.are.equal 0, e\y!

		pending "Entity\\moveTo solidType and sweep"

	it "moveTowards", ->
		e\moveTowards 5, 0, 2
		assert.are.equal 2, e\x!
		assert.are.equal 0, e\y!

		e\moveTowards 5, 0, 2
		assert.are.equal 4, e\x!
		assert.are.equal 0, e\y!

		e\moveTowards 5, 0, 2
		assert.are.equal 5, e\x!
		assert.are.equal 0, e\y!

		e\moveTowards 5, 0, 2
		assert.are.equal 5, e\x!
		assert.are.equal 0, e\y!

		e\moveTowards 5, 5, 2
		assert.are.equal 5, e\x!
		assert.are.equal 2, e\y!

		e\moveTowards 5, 5, 2
		assert.are.equal 5, e\x!
		assert.are.equal 4, e\y!

		e\moveTowards 5, 5, 2
		assert.are.equal 5, e\x!
		assert.are.equal 5, e\y!

		e\moveTowards 7, 7, 1
		assert.are.equal 6, e\x!
		assert.are.equal 6, e\y!

		pending "Entity\\moveTowards solidType and sweep"

	it "moveAtAngle", ->
		e\moveAtAngle 0, 1
		assert.are.equal 1, e\x!
		assert.are.equal 0, e\y!

		e\moveAtAngle 90, 1
		assert.are.equal 1, e\x!
		assert.are.equal 1, e\y!

		e\moveAtAngle 30, 2
		assert.are.equal 3, e\x!
		assert.are.equal 2, e\y!

		e\moveAtAngle -90, 1
		assert.are.equal 3, e\x!
		assert.are.equal 1, e\y!

		e\moveAtAngle 180, 1
		assert.are.equal 2, e\x!
		assert.are.equal 1, e\y!

		pending "Entity\\moveAtAngle solidType and sweep"

	it "clamp hitbox", ->
		e = with Entity 0, 0
			.width = 25
			.height = 25

		-- Horizontal clamp
		e\clampHorizontal 0, 25
		assert.are.equal 0, e\x!

		e\clampHorizontal 0, 20
		assert.are.equal -5, e\x!

		e\clampHorizontal 0, 25
		assert.are.equal 0, e\x!

		e\clampHorizontal 5, 30
		assert.are.equal 5, e\x!

		e\clampHorizontal 5, 20
		assert.are.equal -5, e\x!
		e\x 0

		-- Vertical clamp
		e\clampVertical 0, 25
		assert.are.equal 0, e\y!

		e\clampVertical 0, 20
		assert.are.equal -5, e\y!

		e\clampVertical 0, 25
		assert.are.equal 0, e\y!

		e\clampVertical 5, 30
		assert.are.equal 5, e\y!

		e\clampVertical 5, 20
		assert.are.equal -5, e\y!

	it "centerGraphicInRect", ->
		g = Graphic!
		e = with Entity!
			.width = 10
			.height = 10
			\graphic g

		assert.are_not.equal e\halfWidth!, e\graphic!.x
		assert.are_not.equal e\halfHeight!, e\graphic!.y
		e\centerGraphicInRect!
		assert.are.equal e\halfWidth!, e\graphic!.x
		assert.are.equal e\halfHeight!, e\graphic!.y

require "busted"
require "tests.mock_love"

require "LunoPunk.Entity"
require "LunoPunk.Graphic"
require "LunoPunk.Mask"
require "LunoPunk.utils.moonscript"

entity_sanity_check = ->
	e = Entity!
	assert.are.equal 0, e\x!
	assert.are.equal 0, e\y!
	e

describe "Entity", ->
	e = nil
	before_each ->
		e = entity_sanity_check!

	it "construct", ->
		assert.are.equal true, e.active
		assert.are.equal false, e.autoClear
		assert.are.equal 0, e\x!
		assert.are.equal 0, e\y!
		assert.are.equal 0, e.originX
		assert.are.equal 0, e.originY
		assert.are.equal 0, e.width
		assert.are.equal 0, e.height
		assert.are.equal true, e.visible
		assert.are.equal true, e.collidable
		assert.are.equal false, e.followCamera

		-- Follow Camera coverage
		e.followCamera = true
		assert.are.equal 0, e\x!
		assert.are.equal 0, e\y!
		LP.camera.x += 1
		assert.are.equal 1, e\x!
		assert.are.equal 0, e\y!
		LP.camera.y -= 1
		assert.are.equal -1, e\y!
		e.followCamera = false
		assert.are.equal 0, e\x!
		assert.are.equal 0, e\y!

		e = Entity 10, 15
		assert.are.equal 10, e\x!
		assert.are.equal 15, e\y!
		assert.are.equal 0, e.originX
		assert.are.equal 0, e.originY
		assert.are.equal 0, e.width
		assert.are.equal 0, e.height

		g = Graphic!
		g.x, g.y = 25, 30
		e = Entity 10, 15, g
		assert.are.equal g, e\graphic!
		assert.are.equal 10, e\x!
		assert.are.equal 15, e\y!
		assert.are.equal 0, e.originX
		assert.are.equal 0, e.originY
		assert.are.equal 0, e.width
		assert.are.equal 0, e.height

		m = Mask!
		e = Entity 10, 15, g, m
		assert.are.equal m, e\mask!
		assert.are.equal 10, e\x!
		assert.are.equal 15, e\y!
		assert.are.equal 0, e.originX
		assert.are.equal 0, e.originY
		assert.are.equal 0, e.width
		assert.are.equal 0, e.height

	it "accessors", ->
		assert.are.equal 0, e\halfWidth!
		e.width = 10
		assert.are.equal 5, e\halfWidth!

		assert.are.equal 0, e\halfHeight!
		e.height = 20
		assert.are.equal 10, e\halfHeight!

		assert.are.equal 5, e\centerX!
		assert.are.equal 10, e\centerY!
		e.originX = 10
		assert.are.equal -5, e\centerX!
		e.originY = 10
		assert.are.equal 0, e\centerY!
		e\x 5
		assert.are.equal 0, e\centerX!
		e\y 10
		assert.are.equal 10, e\centerY!

		assert.are.equal -5, e\left!
		assert.are.equal 5, e\right!
		assert.are.equal 0, e\top!
		assert.are.equal 20, e\bottom!

		-- No scene
		assert.is.False e\onCamera!

		l = 5
		assert.is.Nil e\scene!
		assert.are.equal 10, e\layer!
		assert.are.equal l, e\layer l
		assert.are.equal 10, e\layer 10
		e\layer l
		assert.are.equal LP.BASELAYER!, e\layer false

		t = "Entity"
		assert.are.equal '', e\type!
		assert.are.equal t, e\type t
		assert.are.equal t, e\type!
		assert.are.equal t, e\type nil
		assert.are.equal '', e\type ''

		m = Mask!
		assert.is.Nil e\mask!
		assert.are.equal m, e\mask m
		assert.are.equal m, e\mask!
		assert.are.equal e, m.parent
		assert.are.equal m, e\mask nil
		assert.is.Nil e\mask false
		assert.is.Nil m.parent
		assert.is.Nil e\mask!

		g = Graphic!
		assert.is.Nil e\graphic!
		assert.are.equal g, e\graphic g
		assert.are.equal g, e\graphic!
		assert.is.Nil e\graphic false

		s = Scene!
		assert.is.Nil e\scene!
		assert.is.Nil e\world!
		assert.are.equal s, e\scene s
		assert.are.equal s, e\scene!
		assert.are.equal s, e\world!
		assert.is.Nil e\scene false
		assert.are.equal s, e\world s
		e\scene false

		-- Come back to some that branch if scene is set
		e\scene s
		assert.are.equal t, e\type t
		-- Hits a branch if e\type! is not ""
		assert.are.equal t, e\type t
		assert.are.equal l, e\layer l
		-- Hits a branch if e\layer! is not nil
		assert.are.equal l, e\layer l

		pending "Entity\\onCamera after scene is set"

	it "collidable", ->
		e = with Entity 5, 5
			.width = 10
			.height = 10
			\type "Entity"

		-- Collider
		c = with Entity 5, 5
			.width = 10
			.height = 10
			\type "Entity"

		-- No Scene
		assert.is.Nil e\collide "Entity", 10, 10

		LP.scene = Scene!
		LP.scene\add e
		LP.scene\add c
		LP.scene\updateLists!

		-- Not collidable
		e.collidable = false
		assert.is.Nil e\collide "Entity", 10, 10

		-- A collision
		e.collidable = true
		assert.are.equal c, e\collide "Entity", 10, 10
		assert.are.equal e, c\collide "Entity", 10, 10

		-- Bounds
		assert.are.equal c, e\collide "Entity", 5 - c.width, 5
		assert.are.equal c, e\collide "Entity", 5, 5 - c.height
		assert.are.equal e, c\collide "Entity", 5, 5
		assert.are.equal c, e\collide "Entity", 15, 15
		assert.are.equal e, c\collide "Entity", 15, 15

		-- Not a collision
		assert.is.Nil e\collide "Entity", 4 - c.width, 5
		assert.is.Nil e\collide "Entity", 5, 4 - c.height
		assert.is.Nil c\collide "Entity", 4 - e.width, 5
		assert.is.Nil c\collide "Entity", 5, 4 - e.height
		assert.is.Nil e\collide "Entity", 16, 15
		assert.is.Nil e\collide "Entity", 15, 16
		assert.is.Nil c\collide "Entity", 16, 15
		assert.is.Nil c\collide "Entity", 15, 16

		-- Mask collision
		m = Mask!
		e\mask Mask!
		c\mask Mask!
		assert.are.equal c, e\collide "Entity", 10, 10
		assert.are.equal e, c\collide "Entity", 10, 10

		-- Not a collision
		assert.is.Nil e\collide "Entity", 30, 30
		assert.is.Nil e\collide "Entity", 16, 16
		assert.are.equal c, e\collide "Entity", 15, 15

	it "centerOrigin", ->
		e.width, e.height = 10, 10
		assert.are.equal 0, e.originX
		assert.are.equal 0, e.originY

		e\centerOrigin!
		assert.are.equal 5, e.originX
		assert.are.equal 5, e.originY

	it "distanceFrom", ->
		e2 = with Entity 20, 0
			.width = 10
			.height = 10

		e.width, e.height = 10, 10

		assert.are.equal 20, e\distanceFrom e2
		assert.are.equal 20, e2\distanceFrom e

		e2\moveTo 0, 20
		assert.are.equal 20, e\distanceFrom e2
		assert.are.equal 20, e2\distanceFrom e

		e2\moveTo 0, 0
		assert.are.equal 0, e\distanceFrom e2
		assert.are.equal 0, e2\distanceFrom e
		assert.are.equal 0, e\distanceFrom e2, true
		assert.are.equal 0, e2\distanceFrom e, true

		e2\moveTo 20, 0
		assert.are.equal 10, e\distanceFrom e2, true
		assert.are.equal 10, e2\distanceFrom e, true

		e2\moveTo 0, 20
		assert.are.equal 10, e\distanceFrom e2, true
		assert.are.equal 10, e2\distanceFrom e, true

	it "distanceToPoint", ->
		e.width, e.height = 10, 10

		assert.are.equal 20, e\distanceToPoint 20, 0
		assert.are.equal 20, e\distanceToPoint 0, 20
		assert.are.equal 0, e\distanceToPoint 0, 0

		assert.are.equal 10, e\distanceToPoint 20, 0, true
		assert.are.equal 10, e\distanceToPoint 0, 20, true
		assert.are.equal 0, e\distanceToPoint 0, 0, true

		assert.are.equal 10, e\distanceToPoint -10, 0
		assert.are.equal 10, e\distanceToPoint 0, -10
		assert.are.equal 10, e\distanceToPoint -10, 0, true
		assert.are.equal 10, e\distanceToPoint 0, -10, true

	it "distanceToRect", ->
		e.width, e.height = 10, 10

		assert.are.equal 10, e\distanceToRect 20, 0, 10, 10
		assert.are.equal 10, e\distanceToRect 0, 20, 10, 10
		assert.are.equal 0, e\distanceToRect 0, 0, 10, 10

		assert.are.equal 10, e\distanceToRect -20, 0, 10, 10
		assert.are.equal 10, e\distanceToRect 0, -20, 10, 10
		assert.are.equal 10, e\distanceToRect -20, 0, 10, 10
		assert.are.equal 10, e\distanceToRect 0, -20, 10, 10

	it "toString", ->
		assert.are.equal e\toString!, "Entity"
		assert.are.equal tostring(e), "Entity"

		class Ball extends Entity
		b = Ball!
		assert.are.equal b\toString!, "Ball"
		-- The metatable for __tostring is on the parent, not the child
		assert.are_not.equal tostring(b), "Ball"

	it "moveBy", ->
		e\moveBy 1, 0
		assert.are.equal 1, e\x!
		assert.are.equal 0, e\y!

		e\moveBy 0, 1
		assert.are.equal 1, e\x!
		assert.are.equal 1, e\y!

		e\moveBy 1, 1
		assert.are.equal 2, e\x!
		assert.are.equal 2, e\y!

		e\moveBy -1, -1
		assert.are.equal 1, e\x!
		assert.are.equal 1, e\y!

		e\moveBy 0, 0
		assert.are.equal 1, e\x!
		assert.are.equal 1, e\y!

		pending "Entity\\moveBy solidType and sweep"

	it "moveTo", ->
		e\moveTo 5, 0
		assert.are.equal 5, e\x!
		assert.are.equal 0, e\y!

		e\moveTo 0, 5
		assert.are.equal 0, e\x!
		assert.are.equal 5, e\y!

		e\moveTo 0, 0
		assert.are.equal 0, e\x!
		assert.are.equal 0, e\y!

		pending "Entity\\moveTo solidType and sweep"

	it "moveTowards", ->
		e\moveTowards 5, 0, 2
		assert.are.equal 2, e\x!
		assert.are.equal 0, e\y!

		e\moveTowards 5, 0, 2
		assert.are.equal 4, e\x!
		assert.are.equal 0, e\y!

		e\moveTowards 5, 0, 2
		assert.are.equal 5, e\x!
		assert.are.equal 0, e\y!

		e\moveTowards 5, 0, 2
		assert.are.equal 5, e\x!
		assert.are.equal 0, e\y!

		e\moveTowards 5, 5, 2
		assert.are.equal 5, e\x!
		assert.are.equal 2, e\y!

		e\moveTowards 5, 5, 2
		assert.are.equal 5, e\x!
		assert.are.equal 4, e\y!

		e\moveTowards 5, 5, 2
		assert.are.equal 5, e\x!
		assert.are.equal 5, e\y!

		e\moveTowards 7, 7, 1
		assert.are.equal 6, e\x!
		assert.are.equal 6, e\y!

		pending "Entity\\moveTowards solidType and sweep"

	it "moveAtAngle", ->
		e\moveAtAngle 0, 1
		assert.are.equal 1, e\x!
		assert.are.equal 0, e\y!

		e\moveAtAngle 90, 1
		assert.are.equal 1, e\x!
		assert.are.equal 1, e\y!

		e\moveAtAngle 30, 2
		assert.are.equal 3, e\x!
		assert.are.equal 2, e\y!

		e\moveAtAngle -90, 1
		assert.are.equal 3, e\x!
		assert.are.equal 1, e\y!

		e\moveAtAngle 180, 1
		assert.are.equal 2, e\x!
		assert.are.equal 1, e\y!

		pending "Entity\\moveAtAngle solidType and sweep"

	it "clamp hitbox", ->
		e = with Entity 0, 0
			.width = 25
			.height = 25

		-- Horizontal clamp
		e\clampHorizontal 0, 25
		assert.are.equal 0, e\x!

		e\clampHorizontal 0, 20
		assert.are.equal -5, e\x!

		e\clampHorizontal 0, 25
		assert.are.equal 0, e\x!

		e\clampHorizontal 5, 30
		assert.are.equal 5, e\x!

		e\clampHorizontal 5, 20
		assert.are.equal -5, e\x!
		e\x 0

		-- Vertical clamp
		e\clampVertical 0, 25
		assert.are.equal 0, e\y!

		e\clampVertical 0, 20
		assert.are.equal -5, e\y!

		e\clampVertical 0, 25
		assert.are.equal 0, e\y!

		e\clampVertical 5, 30
		assert.are.equal 5, e\y!

		e\clampVertical 5, 20
		assert.are.equal -5, e\y!

	it "centerGraphicInRect", ->
		g = Graphic!
		e = with Entity!
			.width = 10
			.height = 10
			\graphic g

		assert.are_not.equal e\halfWidth!, e\graphic!.x
		assert.are_not.equal e\halfHeight!, e\graphic!.y
		e\centerGraphicInRect!
		assert.are.equal e\halfWidth!, e\graphic!.x
		assert.are.equal e\halfHeight!, e\graphic!.y


shows paths between tiles

shows paths between tiles

export ^

require "tile"
require "rooms/rock"
require "rooms/corridor"
shapes = require "rooms/shape"

class Spacebase
    kBASE_SIZE: 20  -- in tile²
    totalSize: ->
        return Spacebase.kBASE_SIZE * Tile.kTILE_SIZE

    new: =>
        @tileGrid = {}
        for i=1, @kBASE_SIZE
            table.insert(@tileGrid, {})
            for j=1, @kBASE_SIZE
                table.insert(@tileGrid[i], nil)
        @rooms = {}
        @mousePosition = {0, 0} -- updated by external class
        @floatingRoom = nil

        -- build the first room
        @addRoom(Rock(shapes.dot, {@kBASE_SIZE / 2 - 1, @kBASE_SIZE/2}, 0, true))
        @addRoom(Rock(shapes.dot, {@kBASE_SIZE / 2 + 1, @kBASE_SIZE/2}, 0, true))
        @addRoom(Rock(shapes.dot, {@kBASE_SIZE / 2, @kBASE_SIZE/2 - 1}, 0, true))
        @addRoom(Corridor(shapes.t, {@kBASE_SIZE / 2, @kBASE_SIZE/2}, 1, true))

    update: (dt) =>
        if @floatingRoom
            mx, my = @mousePosition[1], @mousePosition[2]
            -- world position
            {wx, wy} = @screenToTileCoordinates(mx, my)
            @floatingRoom\updatePosition(wx, wy)

    draw: =>
        tileSize = Tile.kTILE_SIZE
        totalSize = Spacebase.totalSize()
        for i=1, @kBASE_SIZE + 1
            love.graphics.setColor(255, 255, 255, 150)
            love.graphics.setLineWidth(1)
            i -= 1 -- begins at 0
            love.graphics.line(0, i * tileSize, totalSize, i * tileSize)
            love.graphics.line(i * tileSize, 0, i * tileSize, totalSize)
        for room in *@rooms
            @drawRoom(room)
        if @floatingRoom
            @drawRoom(@floatingRoom)
        -- debug: see neighbors
        love.graphics.setLineWidth(2)
        love.graphics.setColor({50,50,50})
        neighbors = {Tile.kRIGHT, Tile.kLEFT, Tile.kUP, Tile.kDOWN}
        for i=1,@kBASE_SIZE
            for j=1,@kBASE_SIZE
                for n in *neighbors
                    if @tileGrid[i][j] and @tileGrid[i][j].neighbors[n]
                        neigh = @tileGrid[i][j].neighbors[n]
                        {x1, y1} = @tileCenterCoordinates(i, j)
                        {x2, y2} = @tileCenterCoordinates(neigh.x, neigh.y)
                        love.graphics.line(x1, y1, x2, y2)

    drawRoom: (room) =>
            i = room.x - 1
            j = room.y - 1
            love.graphics.push()
            love.graphics.translate(i * Tile.kTILE_SIZE, j * Tile.kTILE_SIZE)
            room\draw()
            love.graphics.pop()

    placeFloatingRoom: =>
        -- TODO: assert that floating room is valid
        @floatingRoom\finalize()
        @addRoom(@floatingRoom)
        @floatingRoom = nil

    addRoom: (room) =>
        table.insert(@rooms, room)
        for tile in *room.tiles
            @tileGrid[tile.x][tile.y] = tile
        @updateNeighbors()

    screenToTileCoordinates: (x, y) =>
        i = math.ceil(x / Tile.kTILE_SIZE)
        j = math.ceil(y / Tile.kTILE_SIZE)
        return {i, j}

    tileCenterCoordinates: (i, j) =>
        x = (i - 1) * Tile.kTILE_SIZE + 0.5 * Tile.kTILE_SIZE
        y = (j - 1) * Tile.kTILE_SIZE + 0.5 * Tile.kTILE_SIZE
        return {x, y}

    updateNeighbors: =>
        for i=1,@kBASE_SIZE
            for j=1,@kBASE_SIZE
                tile = @tileGrid[i][j]
                if tile
                    if i < @kBASE_SIZE
                        tile.neighbors[Tile.kRIGHT] = @tileGrid[i + 1][j]
                    if i > 1
                        tile.neighbors[Tile.kLEFT] = @tileGrid[i - 1][j]
                    if j > 1
                        tile.neighbors[Tile.kUP] = @tileGrid[i][j - 1]
                    if j < @kBASE_SIZE
                        tile.neighbors[Tile.kDOWN] = @tileGrid[i][j + 1]

export ^

require "tile"
require "rooms/rock"
require "rooms/corridor"
shapes = require "rooms/shape"

class Spacebase
    kBASE_SIZE: 20  -- in tile²
    totalSize: ->
        return Spacebase.kBASE_SIZE * Tile.kTILE_SIZE

    new: =>
        @tileGrid = {}
        for i=1, @kBASE_SIZE
            table.insert(@tileGrid, {})
            for j=1, @kBASE_SIZE
                table.insert(@tileGrid[i], nil)
        @rooms = {}
        @mousePosition = {0, 0} -- updated by external class
        @floatingRoom = nil

        -- build the first room
        @addRoom(Rock(shapes.dot, {@kBASE_SIZE / 2 - 1, @kBASE_SIZE/2}, 0, true))
        @addRoom(Rock(shapes.dot, {@kBASE_SIZE / 2 + 1, @kBASE_SIZE/2}, 0, true))
        @addRoom(Rock(shapes.dot, {@kBASE_SIZE / 2, @kBASE_SIZE/2 - 1}, 0, true))
        @addRoom(Corridor(shapes.t, {@kBASE_SIZE / 2, @kBASE_SIZE/2}, 1, true))

    update: (dt) =>
        if @floatingRoom
            mx, my = @mousePosition[1], @mousePosition[2]
            -- world position
            {wx, wy} = @screenToTileCoordinates(mx, my)
            @floatingRoom\updatePosition(wx, wy)

    draw: =>
        tileSize = Tile.kTILE_SIZE
        totalSize = Spacebase.totalSize()
        for i=1, @kBASE_SIZE + 1
            love.graphics.setColor(255, 255, 255, 150)
            love.graphics.setLineWidth(1)
            i -= 1 -- begins at 0
            love.graphics.line(0, i * tileSize, totalSize, i * tileSize)
            love.graphics.line(i * tileSize, 0, i * tileSize, totalSize)
        for room in *@rooms
            @drawRoom(room)
        if @floatingRoom
            @drawRoom(@floatingRoom)

    drawRoom: (room) =>
            i = room.x - 1
            j = room.y - 1
            love.graphics.push()
            love.graphics.translate(i * Tile.kTILE_SIZE, j * Tile.kTILE_SIZE)
            room\draw()
            love.graphics.pop()

    placeFloatingRoom: =>
        -- TODO: assert that floating room is valid
        @floatingRoom\finalize()
        @addRoom(@floatingRoom)
        @floatingRoom = nil

    addRoom: (room) =>
        table.insert(@rooms, room)
        for tile in *room.tiles
            @tileGrid[tile.x][tile.y] = tile
        @updateNeighbors()

    screenToTileCoordinates: (x, y) =>
        i = math.ceil(x / Tile.kTILE_SIZE)
        j = math.ceil(y / Tile.kTILE_SIZE)
        return {i, j}

    mousepressed: (x, y, button) =>
        {i, j} = @screenToTileCoordinates(x, y)
        room = Rock(shapes.random(), {i, j}, 1)
        @addRoom(room)

    updateNeighbors: =>
        for i=1,@kBASE_SIZE
            for j=1,@kBASE_SIZE
                tile = @tileGrid[i][j]
                if tile
                    if i < @kBASE_SIZE
                        tile.neighbors[Tile.kRIGHT] = @tileGrid[i + 1][j]
                    if i > 1
                        tile.neighbors[Tile.kLEFT] = @tileGrid[i - 1][j]
                    if j > 1
                        tile.neighbors[Tile.kUP] = @tileGrid[i][j - 1]
                    if j < @kBASE_SIZE
                        tile.neighbors[Tile.kDOWN] = @tileGrid[i][j + 1]


lib/irc: Document what happens in ping timeout

--- IRC client class
-- @classmod IRCClient

re = require "re"
socket = require 'cqueues.socket'
errno = require 'cqueues.errno'
Logger = require 'logger'

moonscript = {
	errors: require 'moonscript.errors'
}

escapers =  {['s']: ' ', ['r']: '\r', ['n']: '\n', [';']: ';'}

local IRCClient, ContextTable, priority

priority = {
	HIGH: 1,
	DEFAULT: 2,
	LOW: 3,
}

class ContextTable
	get: (name, opts = {})=>
		:multiple, :index, :uses_priority, :current = opts
		multiple = true if multiple == nil
		uses_priority = true if uses_priority == nil

		local output
		output = current or {} if multiple
		for context, items in pairs(self)
			-- context
			if multiple
				-- add all that exist
				if uses_priority
					for p=priority.HIGH, priority.LOW
						if items[p][name]
							for element in *items[p][name]
								table.insert output, element
				else
					if items[name]
						for element in *items[name]
							table.insert output, element
			else
				-- return the singular if it exists
				if uses_priority
					for p=priority.HIGH, priority.LOW
						return items[p][name] if items[p][name] ~= nil
				else
					return items[name] if items[name] ~= nil

		if multiple and not index
			return output
		if index
			return index\get name, :multiple, :uses_priority, current: output

	remove: (name)=>
		for context_name, tbl in pairs(self)
			tbl[name] = nil

class IRCClient
	commands: ContextTable!
	hooks: ContextTable!
	handlers: ContextTable!
	senders: ContextTable!

	__tostring: => "IRCClient"

	default_config = {
		prefix: "!"
	}

	line_pattern = re.compile [[
		-- tags, command, args
		line <- {|
			{:tags: {| (tags sp)? |} :}
			{:prefix: {| (prefix sp)? |} :}
			{:command: (command / numeric) :}
			{:args: {| (sp arg)* |} :} |}
		tags <- '@' tag (';' tag)*
		tag <- {|
			{:is_client: {'+'} :}? -- check: if tag.is_client
			{:vendor: {[^/ ]+} '/' :}?
			{:key: {[^=; ]+} -> esc_tag :}
			{:value: ('=' {[^; ]*} -> esc_tag) :}?
		|}
		prefix <- ':' (
			{:nick: {[^ !]+} :} '!'
			{:user: {[^ @]+} :} '@'
			{:host: {[^ ]+} :} /
			{:nick: {[^ ]+} :})
		command <- [A-Za-z]+
		numeric <- %d^+3^-4 -- at most four digits, at least three
		arg <- ':' {.+} / {[^ ]+}
		sp <- %s
	]], esc_tag: (tag)-> tag\gsub "\\(.)", setmetatable({
		[":"]: ";"
		s: " "
		r: "\r"
		n: "\n"
	}, __index: (t, k) -> k)

	--- Get a tag based on search parameters (is_client IS NOT matched by key)
	-- @tparam table tags Tag from IRC parser
	-- @tparam table opts Options to search for (is_client: bool, key: string)
	get_tag: (tags, opts)=>
		:is_client, :key = opts
		for tag in *tags
			continue if is_client ~= nil and tag.is_client ~= is_client
			if key ~= nil
				continue if tag.vendor ~= nil and key ~= "#{tag.vendor}/#{tag.key}"
				continue if tag.vendor == nil and key ~= tag.key
			return tag

		return {}

	--- Generate a new IRCClient
	-- @tparam string server IRC server name
	-- @tparam number port IRC port number
	-- @tparam table config Default configuration
	new: (server, port=6697, config)=>
		assert server
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs default_config
			@config[k] = v
		if config
			for k, v in pairs config
				@config[k] = v

		@commands = ContextTable!
		@hooks    = ContextTable!
		@handlers = ContextTable!
		@senders  = ContextTable!
		@server   = {}

	unpack = unpack or table.unpack

	get_priority = (options, fn)->
		return fn and options.priority or priority.DEFAULT

	handle_options: (options, fn)=>
		unless fn
			fn = options
			options = {}

		if options.async
			return (...)->
				args = {...}
				require("queue")\wrap ->
					@pcall_bare fn, unpack args

		if options.wrap_iter
			tmp_fn = coroutine.wrap fn
			tmp_fn!
			return tmp_fn

		fn

	assert_context: (context_table)=>
		assert @context, "Missing @with_context"
		return context_table[assert @context, "Missing @with_context"]

	--- Change the module context for the current command
	-- @tparam string context module context (typically, the name)
	-- @tparam function fn code to run under context
	with_context: (context, fn)=>
		assert @context == nil, "Already in context: #{@context}"
		@context = context

		self["senders"][context] = {} -- no priority for senders, only one
		for key in *{"hooks", "handlers", "commands"}
			self[key][context] = [{} for p=priority.HIGH, priority.LOW]

		fn self, context
		@context = nil

	--- Add an IRC bot command
	-- @tparam string name Bot command name
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function command Function for handling command
	add_command: (name, options, command)=>
		final_command = command
		commands = @assert_context @commands

		-- `options` is passed, which fills in `command`
		if command
			-- is async, send typing until completed
			if options.async
				-- async, send @+draft/typing
				final_command = (prefix, target, tags, ...)=>
					@vars.typing_counter = 0 if not @vars.typing_counter
					@vars.typing_counter += 1
					@send 'TAGMSG', target, "+draft/typing": 'active'
					@pcall command, prefix, target, tags, ...
					@vars.typing_counter -= 1
					if @vars.typing_counter == 0
						@send 'TAGMSG', target, "+draft/typing": 'done'
					else
						@send 'TAGMSG', target, "+draft/typing": 'active'

		p = get_priority options, final_command
		commands[p][name] = @handle_options options, final_command

	--- Add a client processing hook
	-- @tparam string name Name of event to hook into
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function hook Processor for hook event
	add_hook: (name, options, hook)=>
		hooks = @assert_context @hooks

		p = get_priority options, hook

		unless hooks[p][name]
			hooks[p][name] = {@handle_options options, hook}
		else
			table.insert hooks[p][name], @handle_options(options, hook)

	--- Add an IRC command handler
	-- @tparam string id IRC command ID (numerics MUST be strings)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function handler IRC command processor
	add_handler: (id, options, handler)=>
		handlers = @assert_context @handlers

		p = get_priority options, handler

		unless handlers[p][id]
			handlers[p][id] = {@handle_options options, handler}
		else
			table.insert hooks[p][name], @handle_options(options, handler)

	--- Add an IRC command sending handler, does NOT take a priority
	-- @tparam string id IRC command ID ("PRIVMSG", "JOIN", etc.)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function sender Function to handle message to be sent
	add_sender: (id, options, sender)=>
		senders = @assert_context @senders
		senders[id] = @handle_options(options, sender)

	--- Reset all modules in the IRCClient
	clear_modules: ()=>
		@senders = {}
		@handlers = {}
		@hooks = {}
		@commands = {}

	--- Connect to the IRC server specified in the configuration
	connect: ()=>
		-- Clear internally managed state
		@__is_pinging_timeout = nil

		-- Reset old connection
		if @socket
			Logger.debug "Shutting down socket: #{tostring(@socket)}"
			@socket\shutdown!

		host = @config.server
		port = @config.port
		ssl  = @config.ssl
		debug_msg = ('Connecting... {host: "%s", port: "%s"}')\format host, port

		@config.nick = 'Moon-Moon' if not @config.nick
		@config.username = 'Mooooon' if not @config.username
		@config.realname = 'Moon Moon: MoonScript IRC Bot' if not @config.realname

		Logger.debug debug_msg, Logger.level.warn .. '--- Connecting...'
		@socket = assert socket.connect{:host, :port}
		if ssl
			Logger.debug 'Starting TLS exchange...'
			@socket\starttls!
			Logger.debug 'Started TLS exchange'
		Logger.print Logger.level.okay .. '--- Connected'
		@fire_hook 'CONNECT'
		nick = @config.nick
		user = @config.username
		real = @config.realname
		pass = @config.password
		Logger.print Logger.level.warn .. '--- Sending authentication data'
		if pass and ssl
			Logger.debug '*** Sending password'
			@send_raw 'PASS', pass
		elseif pass
			Logger.print Logger.level.error .. '*** Not sending password: TLS not enabled ***'
		@send_raw 'NICK', nick
		@send_raw 'USER', user, '*', '*', real
		debug_msg = ('Sent authentication data: {nickname: %s, username: %s, realname: %s}')\format nick, user, real
		Logger.debug debug_msg, Logger.level.okay .. '--- Sent authentication data'

	--- Disconnect from the current IRC server
	disconnect: ()=>
		@socket\shutdown! if @socket
		@fire_hook 'DISCONNECT'

	tag_escapes = {
		";": ":"
		" ": "s"
		"\r": "r"
		"\n": "n"
	}

	--- Serialize a value for being sent in a tag
	serialize_tag_string = (key, value)->
		return key if type(value) == "boolean"
		return key .. "=" .. value\gsub "([: \r\n])", (value)-> "\\#{tbl[value]}"

	--- Serialize client->client tags for sending to the server
	-- @param tags Table of tags to serialize
	serialize_tags = (tags)->
		output = [serialize_tag_string(k, v) for k, v in pairs tags]
		return "@#{table.concat output, ';'}"

	--- Send a raw line to the currently connected IRC server
	-- @param ... List of strings, concatenated using spaces
	send_raw: (...)=>
		output = {...}
		arg_count = select("#", ...)
		opts_table = select(arg_count, ...)
		if type(opts_table) == "table"
			-- we have tags, server accepts tags
			if opts_table.tags and next(opts_table.tags) and @server.ircv3_caps["message-tags"]
				table.insert output, 1, serialize_tags opts_table.tags
			-- remove opts_table regardless of caps
			table.remove output, #output
		arg_count = #output
		if output[arg_count]\find " "
			output[arg_count] = ":#{output[arg_count]}"
		@socket\write table.concat(output, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat output, ' '

	--- Send a command using a builtin sender configured with @add_sender
	-- @tparam string name Name of sender to use
	-- @param ... List of arguments to be passed to sender
	-- @see IRCClient\add_sender
	send: (name, ...)=>
		sender = @senders\get name, multiple: false, index: IRCClient.senders, uses_priority: false
		input = sender self, ...
		@socket\write input if input

	date_pattern: "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+).(%d+)Z"

	--- Parse the time of an ISO 8601 compliant string
	-- @tparam string date ISO 8601 compliant YYYY-MM-DDThh:mm:ssZ date
	-- @treturn number Seconds from Epoch from configured date
	parse_time: (date)=>
		year, month, day, hour, min, sec, mil = date\match @date_pattern
		return os.time(:year, :month, :day, :hour, :min, :sec) + tonumber(mil) / 1000

	--- parse an IRC command using line_pattern
	parse: (line)=> line_pattern\match line

	--- Activate hooks configured for the event name
	-- @tparam string hook_name Name of event to "fire"
	-- @treturn boolean True if no errors were encountered, false otherwise
	-- @treturn table Table containing list of errors from hooks
	fire_hook: (hook_name, ...)=>
		Logger.debug "#{Logger.level.warn}--- Running hooks for #{hook_name}"
		has_run = false
		has_errors = false
		errors = {}

		for hook in *(@hooks\get hook_name, index: IRCClient.hooks)
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running global hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)

		-- for _, hook in pairs IRCClient.hooks\get hook_name
		---	has_run = true unless has_run
		---	Logger.debug Logger.level.warn .. '--- Running global hook: ' .. hook_name
		---	ok, err = @pcall hook, ...
		---	if not ok
		---		has_errors = true if not has_errors
		---		table.insert(errors, err)
		-- for _, hook in pairs @hooks\get hook_name
		---	has_run = true unless has_run
		---	Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
		---	ok, err = @pcall hook, ...
		---	if not ok
		---		has_errors = true if not has_errors
		---		table.insert(errors, err)

		-- Logger.debug Logger.level.error .. "*** Hook not found for #{hook_name}" unless has_run

		return has_errors, errors, has_run

	process_line: (line, opts={})=>
		{:tags, :prefix, :command, :args} = @parse line
		opts.line = line
		@process prefix, command, args, tags, opts

	--- Run handlers for an unparsed command
	-- @tparam table prefix Prefix argument from server (nick, user, host)
	-- @tparam table args Argument list from server, including trailing
	-- @tparam table tags Tags list from server
	-- @tparam table opts Optional arguments, in_batch: IRCv3 batching
	process: (prefix, command, args, tags, opts={})=>
		{:in_batch} = opts
		{:nick, :user, :host} = prefix
		if opts.line
			Logger.debug Logger.level.warn .. '--- | Line: ' .. opts.line
			Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
			if nick and user and host
				Logger.debug "#{Logger.level.okay}--- |\\ User: #{nick}!#{user}@#{host}"
			elseif nick
				Logger.debug "#{Logger.level.okay}--- |\\ Nick: #{nick}"
			if #args > 0
				Logger.debug "#{Logger.level.okay}--- |\\ Arguments: #{table.concat args, ' '}"

		has_run = false

		if not in_batch and tags.batch
			-- will return _, _, true if a hook run; this is necessary because
			-- the protocol might send batches even if we don't support them.
			return unless select(2, @fire_hook "BATCH.#{tags.batch}", prefix,
				args, tags, opts)

		for handler in *@handlers\get command, index: IRCClient.handlers
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		-- for handler in *IRCClient.handlers\get command
		---	has_run = true unless has_run
		---	@pcall handler, prefix, args, tags, opts

		-- for handler in *@handlers\get command
		---	has_run = true unless has_run
		---	@pcall handler, prefix, args, tags, opts

		Logger.debug Logger.level.error .. "*** Handler not found for #{command}" unless has_run

	--- Iterate over lines from a server and handle errors appropriately
	loop: ()=>
		for received_line, err in @socket\xlines(nil, nil, 180) do
			-- if we get a non-timeout error, rethrow
			assert received_line or err == errno.ETIMEDOUT, errno[err]
			if err == errno.ETIMEDOUT -- No input from server in n seconds
				-- we have ping timed out, throw the timeout, which will
				-- trigger a reconnect (thus clearing @__is_pinging_timeout)
				assert not @__is_pinging_timeout, errno[err]
				@send_raw 'PING', 'test_ping_timeout'
				@__is_pinging_timeout = true
			@pcall @process_line, received_line, in_batch: false

	--- Call a function and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, self, ...
		if not ok
			Logger.print "Arguments:"
			for arg in *{...}
				Logger.print tostring arg
		return ok, err

	--- Call a function without self and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall_bare: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, ...
		if not ok
			Logger.print "Arguments:"
			Logger.print tostring arg for arg in *{...}
		return ok, err

	--- Print a traceback using the internal logging mechanism
	-- @see IRCClient\log
	log_traceback: (err)=>
		err = tostring err
		Logger.print moonscript.errors.rewrite_traceback debug.traceback!, err
		Logger.print "#{Logger.level.error} ---"
		return err

	--- Log message from IRC server (used in plugins)
	-- @tparam string input Line to print, IRC color formatted
	-- @see logger
	log: (input)=>
		for line in input\gmatch "[^\r\n]+"
			server_name = @config.server_name or @server.caps['NETWORK'] or @config.server
			Logger.print '\00311(\003' .. server_name ..  "\00311)\003 #{line}"

return {:IRCClient, :priority}

--- IRC client class
-- @classmod IRCClient

re = require "re"
socket = require 'cqueues.socket'
errno = require 'cqueues.errno'
Logger = require 'logger'

moonscript = {
	errors: require 'moonscript.errors'
}

escapers =  {['s']: ' ', ['r']: '\r', ['n']: '\n', [';']: ';'}

local IRCClient, ContextTable, priority

priority = {
	HIGH: 1,
	DEFAULT: 2,
	LOW: 3,
}

class ContextTable
	get: (name, opts = {})=>
		:multiple, :index, :uses_priority, :current = opts
		multiple = true if multiple == nil
		uses_priority = true if uses_priority == nil

		local output
		output = current or {} if multiple
		for context, items in pairs(self)
			-- context
			if multiple
				-- add all that exist
				if uses_priority
					for p=priority.HIGH, priority.LOW
						if items[p][name]
							for element in *items[p][name]
								table.insert output, element
				else
					if items[name]
						for element in *items[name]
							table.insert output, element
			else
				-- return the singular if it exists
				if uses_priority
					for p=priority.HIGH, priority.LOW
						return items[p][name] if items[p][name] ~= nil
				else
					return items[name] if items[name] ~= nil

		if multiple and not index
			return output
		if index
			return index\get name, :multiple, :uses_priority, current: output

	remove: (name)=>
		for context_name, tbl in pairs(self)
			tbl[name] = nil

class IRCClient
	commands: ContextTable!
	hooks: ContextTable!
	handlers: ContextTable!
	senders: ContextTable!

	__tostring: => "IRCClient"

	default_config = {
		prefix: "!"
	}

	line_pattern = re.compile [[
		-- tags, command, args
		line <- {|
			{:tags: {| (tags sp)? |} :}
			{:prefix: {| (prefix sp)? |} :}
			{:command: (command / numeric) :}
			{:args: {| (sp arg)* |} :} |}
		tags <- '@' tag (';' tag)*
		tag <- {|
			{:is_client: {'+'} :}? -- check: if tag.is_client
			{:vendor: {[^/ ]+} '/' :}?
			{:key: {[^=; ]+} -> esc_tag :}
			{:value: ('=' {[^; ]*} -> esc_tag) :}?
		|}
		prefix <- ':' (
			{:nick: {[^ !]+} :} '!'
			{:user: {[^ @]+} :} '@'
			{:host: {[^ ]+} :} /
			{:nick: {[^ ]+} :})
		command <- [A-Za-z]+
		numeric <- %d^+3^-4 -- at most four digits, at least three
		arg <- ':' {.+} / {[^ ]+}
		sp <- %s
	]], esc_tag: (tag)-> tag\gsub "\\(.)", setmetatable({
		[":"]: ";"
		s: " "
		r: "\r"
		n: "\n"
	}, __index: (t, k) -> k)

	--- Get a tag based on search parameters (is_client IS NOT matched by key)
	-- @tparam table tags Tag from IRC parser
	-- @tparam table opts Options to search for (is_client: bool, key: string)
	get_tag: (tags, opts)=>
		:is_client, :key = opts
		for tag in *tags
			continue if is_client ~= nil and tag.is_client ~= is_client
			if key ~= nil
				continue if tag.vendor ~= nil and key ~= "#{tag.vendor}/#{tag.key}"
				continue if tag.vendor == nil and key ~= tag.key
			return tag

		return {}

	--- Generate a new IRCClient
	-- @tparam string server IRC server name
	-- @tparam number port IRC port number
	-- @tparam table config Default configuration
	new: (server, port=6697, config)=>
		assert server
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs default_config
			@config[k] = v
		if config
			for k, v in pairs config
				@config[k] = v

		@commands = ContextTable!
		@hooks    = ContextTable!
		@handlers = ContextTable!
		@senders  = ContextTable!
		@server   = {}

	unpack = unpack or table.unpack

	get_priority = (options, fn)->
		return fn and options.priority or priority.DEFAULT

	handle_options: (options, fn)=>
		unless fn
			fn = options
			options = {}

		if options.async
			return (...)->
				args = {...}
				require("queue")\wrap ->
					@pcall_bare fn, unpack args

		if options.wrap_iter
			tmp_fn = coroutine.wrap fn
			tmp_fn!
			return tmp_fn

		fn

	assert_context: (context_table)=>
		assert @context, "Missing @with_context"
		return context_table[assert @context, "Missing @with_context"]

	--- Change the module context for the current command
	-- @tparam string context module context (typically, the name)
	-- @tparam function fn code to run under context
	with_context: (context, fn)=>
		assert @context == nil, "Already in context: #{@context}"
		@context = context

		self["senders"][context] = {} -- no priority for senders, only one
		for key in *{"hooks", "handlers", "commands"}
			self[key][context] = [{} for p=priority.HIGH, priority.LOW]

		fn self, context
		@context = nil

	--- Add an IRC bot command
	-- @tparam string name Bot command name
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function command Function for handling command
	add_command: (name, options, command)=>
		final_command = command
		commands = @assert_context @commands

		-- `options` is passed, which fills in `command`
		if command
			-- is async, send typing until completed
			if options.async
				-- async, send @+draft/typing
				final_command = (prefix, target, tags, ...)=>
					@vars.typing_counter = 0 if not @vars.typing_counter
					@vars.typing_counter += 1
					@send 'TAGMSG', target, "+draft/typing": 'active'
					@pcall command, prefix, target, tags, ...
					@vars.typing_counter -= 1
					if @vars.typing_counter == 0
						@send 'TAGMSG', target, "+draft/typing": 'done'
					else
						@send 'TAGMSG', target, "+draft/typing": 'active'

		p = get_priority options, final_command
		commands[p][name] = @handle_options options, final_command

	--- Add a client processing hook
	-- @tparam string name Name of event to hook into
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function hook Processor for hook event
	add_hook: (name, options, hook)=>
		hooks = @assert_context @hooks

		p = get_priority options, hook

		unless hooks[p][name]
			hooks[p][name] = {@handle_options options, hook}
		else
			table.insert hooks[p][name], @handle_options(options, hook)

	--- Add an IRC command handler
	-- @tparam string id IRC command ID (numerics MUST be strings)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function handler IRC command processor
	add_handler: (id, options, handler)=>
		handlers = @assert_context @handlers

		p = get_priority options, handler

		unless handlers[p][id]
			handlers[p][id] = {@handle_options options, handler}
		else
			table.insert hooks[p][name], @handle_options(options, handler)

	--- Add an IRC command sending handler, does NOT take a priority
	-- @tparam string id IRC command ID ("PRIVMSG", "JOIN", etc.)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function sender Function to handle message to be sent
	add_sender: (id, options, sender)=>
		senders = @assert_context @senders
		senders[id] = @handle_options(options, sender)

	--- Reset all modules in the IRCClient
	clear_modules: ()=>
		@senders = {}
		@handlers = {}
		@hooks = {}
		@commands = {}

	--- Connect to the IRC server specified in the configuration
	connect: ()=>
		-- Clear internally managed state
		@__is_pinging_timeout = nil

		-- Reset old connection
		if @socket
			Logger.debug "Shutting down socket: #{tostring(@socket)}"
			@socket\shutdown!

		host = @config.server
		port = @config.port
		ssl  = @config.ssl
		debug_msg = ('Connecting... {host: "%s", port: "%s"}')\format host, port

		@config.nick = 'Moon-Moon' if not @config.nick
		@config.username = 'Mooooon' if not @config.username
		@config.realname = 'Moon Moon: MoonScript IRC Bot' if not @config.realname

		Logger.debug debug_msg, Logger.level.warn .. '--- Connecting...'
		@socket = assert socket.connect{:host, :port}
		if ssl
			Logger.debug 'Starting TLS exchange...'
			@socket\starttls!
			Logger.debug 'Started TLS exchange'
		Logger.print Logger.level.okay .. '--- Connected'
		@fire_hook 'CONNECT'
		nick = @config.nick
		user = @config.username
		real = @config.realname
		pass = @config.password
		Logger.print Logger.level.warn .. '--- Sending authentication data'
		if pass and ssl
			Logger.debug '*** Sending password'
			@send_raw 'PASS', pass
		elseif pass
			Logger.print Logger.level.error .. '*** Not sending password: TLS not enabled ***'
		@send_raw 'NICK', nick
		@send_raw 'USER', user, '*', '*', real
		debug_msg = ('Sent authentication data: {nickname: %s, username: %s, realname: %s}')\format nick, user, real
		Logger.debug debug_msg, Logger.level.okay .. '--- Sent authentication data'

	--- Disconnect from the current IRC server
	disconnect: ()=>
		@socket\shutdown! if @socket
		@fire_hook 'DISCONNECT'

	tag_escapes = {
		";": ":"
		" ": "s"
		"\r": "r"
		"\n": "n"
	}

	--- Serialize a value for being sent in a tag
	serialize_tag_string = (key, value)->
		return key if type(value) == "boolean"
		return key .. "=" .. value\gsub "([: \r\n])", (value)-> "\\#{tbl[value]}"

	--- Serialize client->client tags for sending to the server
	-- @param tags Table of tags to serialize
	serialize_tags = (tags)->
		output = [serialize_tag_string(k, v) for k, v in pairs tags]
		return "@#{table.concat output, ';'}"

	--- Send a raw line to the currently connected IRC server
	-- @param ... List of strings, concatenated using spaces
	send_raw: (...)=>
		output = {...}
		arg_count = select("#", ...)
		opts_table = select(arg_count, ...)
		if type(opts_table) == "table"
			-- we have tags, server accepts tags
			if opts_table.tags and next(opts_table.tags) and @server.ircv3_caps["message-tags"]
				table.insert output, 1, serialize_tags opts_table.tags
			-- remove opts_table regardless of caps
			table.remove output, #output
		arg_count = #output
		if output[arg_count]\find " "
			output[arg_count] = ":#{output[arg_count]}"
		@socket\write table.concat(output, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat output, ' '

	--- Send a command using a builtin sender configured with @add_sender
	-- @tparam string name Name of sender to use
	-- @param ... List of arguments to be passed to sender
	-- @see IRCClient\add_sender
	send: (name, ...)=>
		sender = @senders\get name, multiple: false, index: IRCClient.senders, uses_priority: false
		input = sender self, ...
		@socket\write input if input

	date_pattern: "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+).(%d+)Z"

	--- Parse the time of an ISO 8601 compliant string
	-- @tparam string date ISO 8601 compliant YYYY-MM-DDThh:mm:ssZ date
	-- @treturn number Seconds from Epoch from configured date
	parse_time: (date)=>
		year, month, day, hour, min, sec, mil = date\match @date_pattern
		return os.time(:year, :month, :day, :hour, :min, :sec) + tonumber(mil) / 1000

	--- parse an IRC command using line_pattern
	parse: (line)=> line_pattern\match line

	--- Activate hooks configured for the event name
	-- @tparam string hook_name Name of event to "fire"
	-- @treturn boolean True if no errors were encountered, false otherwise
	-- @treturn table Table containing list of errors from hooks
	fire_hook: (hook_name, ...)=>
		Logger.debug "#{Logger.level.warn}--- Running hooks for #{hook_name}"
		has_run = false
		has_errors = false
		errors = {}

		for hook in *(@hooks\get hook_name, index: IRCClient.hooks)
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running global hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)

		-- for _, hook in pairs IRCClient.hooks\get hook_name
		---	has_run = true unless has_run
		---	Logger.debug Logger.level.warn .. '--- Running global hook: ' .. hook_name
		---	ok, err = @pcall hook, ...
		---	if not ok
		---		has_errors = true if not has_errors
		---		table.insert(errors, err)
		-- for _, hook in pairs @hooks\get hook_name
		---	has_run = true unless has_run
		---	Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
		---	ok, err = @pcall hook, ...
		---	if not ok
		---		has_errors = true if not has_errors
		---		table.insert(errors, err)

		-- Logger.debug Logger.level.error .. "*** Hook not found for #{hook_name}" unless has_run

		return has_errors, errors, has_run

	process_line: (line, opts={})=>
		{:tags, :prefix, :command, :args} = @parse line
		opts.line = line
		@process prefix, command, args, tags, opts

	--- Run handlers for an unparsed command
	-- @tparam table prefix Prefix argument from server (nick, user, host)
	-- @tparam table args Argument list from server, including trailing
	-- @tparam table tags Tags list from server
	-- @tparam table opts Optional arguments, in_batch: IRCv3 batching
	process: (prefix, command, args, tags, opts={})=>
		{:in_batch} = opts
		{:nick, :user, :host} = prefix
		if opts.line
			Logger.debug Logger.level.warn .. '--- | Line: ' .. opts.line
			Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
			if nick and user and host
				Logger.debug "#{Logger.level.okay}--- |\\ User: #{nick}!#{user}@#{host}"
			elseif nick
				Logger.debug "#{Logger.level.okay}--- |\\ Nick: #{nick}"
			if #args > 0
				Logger.debug "#{Logger.level.okay}--- |\\ Arguments: #{table.concat args, ' '}"

		has_run = false

		if not in_batch and tags.batch
			-- will return _, _, true if a hook run; this is necessary because
			-- the protocol might send batches even if we don't support them.
			return unless select(2, @fire_hook "BATCH.#{tags.batch}", prefix,
				args, tags, opts)

		for handler in *@handlers\get command, index: IRCClient.handlers
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		-- for handler in *IRCClient.handlers\get command
		---	has_run = true unless has_run
		---	@pcall handler, prefix, args, tags, opts

		-- for handler in *@handlers\get command
		---	has_run = true unless has_run
		---	@pcall handler, prefix, args, tags, opts

		Logger.debug Logger.level.error .. "*** Handler not found for #{command}" unless has_run

	--- Iterate over lines from a server and handle errors appropriately
	loop: ()=>
		for received_line, err in @socket\xlines(nil, nil, 180) do
			assert(received_line or err == errno.ETIMEDOUT, errno[err])
			if err == errno.ETIMEDOUT -- No input from server in n seconds
				assert not @__is_pinging_timeout, errno[err]
				@send_raw 'PING', 'test_ping_timeout'
				@__is_pinging_timeout = true
			@pcall @process_line, received_line, in_batch: false

	--- Call a function and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, self, ...
		if not ok
			Logger.print "Arguments:"
			for arg in *{...}
				Logger.print tostring arg
		return ok, err

	--- Call a function without self and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall_bare: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, ...
		if not ok
			Logger.print "Arguments:"
			Logger.print tostring arg for arg in *{...}
		return ok, err

	--- Print a traceback using the internal logging mechanism
	-- @see IRCClient\log
	log_traceback: (err)=>
		err = tostring err
		Logger.print moonscript.errors.rewrite_traceback debug.traceback!, err
		Logger.print "#{Logger.level.error} ---"
		return err

	--- Log message from IRC server (used in plugins)
	-- @tparam string input Line to print, IRC color formatted
	-- @see logger
	log: (input)=>
		for line in input\gmatch "[^\r\n]+"
			server_name = @config.server_name or @server.caps['NETWORK'] or @config.server
			Logger.print '\00311(\003' .. server_name ..  "\00311)\003 #{line}"

return {:IRCClient, :priority}