lib/irc: Rename argument to actual value

--- IRC client class
-- @classmod IRCClient

re = require "re"
socket = require 'cqueues.socket'
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: ()=>
		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 #{command}" 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 in @socket\lines! do
			@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'
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, line, ...)=>
					@vars.typing_counter = 0 if not @vars.typing_counter
					@vars.typing_counter += 1
					@send 'TAGMSG', target, "+draft/typing": 'active'
					@pcall command, prefix, target, line, ...
					@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: ()=>
		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 #{command}" 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 in @socket\lines! do
			@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}


lib/std: Fix handlers for some errors

lib/std: Fix handlers for some errors

local Result, Option, Ok, Err

expect_class = (value, expected)->
	assert value.__class == expected,
		"expected type #{expected.__name}<#{expected}>, got " ..
		"#{value.__class.__name}<#{value.__class}>"

class Result
	Ok: 0
	Err: 1

	new: (variant, content)=>
		@variant = variant
		@content = content

	is_ok: => @variant == Ok
	is_err: => @variant == Err

	--- Unwrap a result, returning the content or otherwise erroring
	expect: (error_value)=>
		@content if @is_ok! else error "#{error_value}: #{@content}"

	--- Raise error if is_err!, otherwise return content
	unwrap: =>
		@content if @is_ok! else error @content

	--- Raises error if is_ok!, otherwise returns error value
	unwrap_err: =>
		@content if @is_err! else error @content

	--- Returns content if is_ok!, otherwise `fallback`
	-- @param fallback
	unwrap_or: (fallback)=>
		@content if @is_ok! else fallback

	--- Returns content if is_ok!, otherwise the called output of `fallback_fn`
	-- @tparam fallback_fn
	unwrap_or_else: (fallback_fn)=>
		@content if @is_ok! else fallback_fn!

	ok: => Option(@content) if @is_ok! else Option!
	err: => Option(@content) if @is_err! else Option!

	--- Map Result<T, E> to Result<U, E> - Error can be kept if is_err!
	-- @tparam function fn Function to use for mapping content
	map: (fn)=>
		Ok fn @content if @is_ok else self

	--- Map Result<T, E> to Result<T, F> - Content can be kept if is_ok!
	-- @tparam function fn Function to use for mapping error
	map_err: (fn)=>
		if @is_err
			return Err fn @content
		self

	--- Return `right` if the variant is `Ok`, otherwise return self's `Err`
	-- @param right
	and_also: (right)=>
		self if @is_error! else right

	--- Return evaluated output of `right_fn` if the variant is `Ok`, otherwise
	-- return self's `Err`
	-- @tparam function right_fn
	and_then: (right_fn)=>
		self if @is_error! else right_fn @content

	--- Return `right` if the variant is `Err`, otherwise return self's `Ok`
	-- @param right
	or_other: (right)=>
		self if @is_ok! else right

	--- Return evaluated output of  `right_fn` if the variant is `Err`,
	-- otherwise return self's `Ok`
	-- @tparam function right_fn
	or_else: (right_fn)=>
		self if @is_ok! else right_fn @content

	--- Transpose a Result of an Option into an Option of a Result
	transpose: =>
		expect_class @content, Result
		if @content\is_some!
			return @variant @content.data -- Some
		Option! -- None

import Ok, Err from Result

result_variant_mt = {
	__call: (...)=> Result self, ...
}

debug.setmetatable Ok, result_variant_mt
debug.setmetatable Err, result_variant_mt

class Option
	new: (some=nil)=>
		if some
			@data = some

	is_some: => @data != nil
	is_none: => @data == nil

	--- Transform an Option<Option<T>> into an Option<T>
	flatten: =>
		expect_class @data, Option
		Option @data\expect!

	--- Unwrap an option, returning the content or otherwise erroring
	-- @param error_value Value raised if variant is None
	expect: (error_value)=>
		@data if @is_some! else Err(error_value)\unwrap!

	--- Moves a value `v` out of `Option<T>` if it is a `Some(v)` variant
	unwrap: =>
		@expect "called Option.unwrap() on a None variant"

	--- Return the contained value if is_some! or a fallback value
	-- @param fallback
	unwrap_or: (fallback)=>
		@data if @is_some! else fallback

	--- Return the contained value if is_some!, otherwise the called output of
	-- `fallback_fn`
	-- @tparam function fallback_fn
	unwrap_or_else: (fallback_fn)=>
		@data if @is_some! else fallback_fn!

	--- Applies a function to the contained value
	-- @tparam function fn Function to use for mapping content
	map: (fn)=>
		Option fn @data if @is_some! else self

	--- Applies a function to the contained value, or returns a fallback
	-- @param fallback
	-- @tparam function fn Function to use for mapping content
	map_or: (fallback, fn)=>
		Option fn @data if @is_some! else fallback

	--- Applies a function to the contained value, or calls fallback function
	-- @tparam function fallback_fn
	-- @tparam function fn Function to use for mapping content
	map_or_else: (fallback_fn, fn)=>
		Option fn @data if @is_some! else fallback_fn!

	--- Transform an `Option<T>` into a `Result<T, E>`, mapping `Some(v)` to
	-- `Ok(v)` and `None` to `Err(error_value)`
	-- @param error_value
	ok_or: (error_value)=>
		if @is_some!
			Ok @data
		else -- None
			Err error_value

	--- Transform an `Option<T>` into a `Result<T, E>`, mapping `Some(v)` to
	-- `Ok(v)` and `None` to `Err(error_fn!)`
	-- @tparam function error_fn
	ok_or_else: (error_fn)=>
		if @is_some!
			Ok @data
		else -- None
			Err error_fn!

	--- Return `None` if already `None`, otherwise call `predicate` function
	-- and returns `Some(v)` if the predicate function returns true, otherwise
	-- None if `predicate` returns false
	-- @tparam function predicate
	filter: (predicate)=>
		if @is_some! and predicate @data
			return @data
		Option!

	--- Return `right` if the variant is `Some`
	-- @param right
	and_also: (right)=>
		self if @is_none! else right

	--- Return evaluated output of `right_fn` if the variant is `Some`,
	-- otherwise return None
	-- @tparam function right_fn
	and_then: (right_fn)=>
		self if @is_none! else right_fn @data

	--- Return `right` if the variant is `None`, else return self's `Some`
	-- @param right
	or_other: (right)=>
		self if @is_some! else right

	--- Returns evaluated output of `right_fn` if the variant is `None`,
	-- otherwise returns self's `Some`
	-- @tparam function right_fn
	or_else: (right_fn)=>
		self if @is_some! else right_fn!

	--- Returns a `Some(value)` if either the self or the `right` have a
	-- `Some(value)`, but not both
	xor: (right)=>
		if (@is_some! and right\is_none!) or (@is_none! and right\is_some!)
			return @or_other right
		Option! -- None

	--- Transpose an Option of a Result into a Result of an Option
	transpose: =>
		if @is_none!
			return Ok self -- Result(None)
		@data.variant Option(@data.content)

class Entry
	new: (source, key)=>
		@source = source
		@key = key

	--- Determine whether or not a `key` exists currently in the table
	exists: => @source[@key] != nil

	--- Create a `default` value in the source if the key doesn't have an
	-- associated value already
	-- @param default
	or_insert: (default)=>
		source, key = @source, @key
		source[key] = default unless source[key]
		source[key]

	--- Create a default value from the result of `default_fn` in the source if
	-- the key doesn't have an associated value already
	-- @tparam function default_fn
	or_insert_with: (default_fn)=>
		source, key = @source, @key
		source[key] = default_fn! unless source[key]
		source[key]

	--- Apply a function `fn` to the entry, if it exists; should be called
	-- before returning the value, and can be chained
	-- @tparam function fn
	and_modify: (fn)=>
		source, key = @source, @key
		if source[key]
			source[key] = fn source[key]
		self

class Map
	new: (data={})=>
		@data = data

	--- Return an `Entry` to the potential value in the Map, which can be used
	-- to add a default value. If a default value isn't available or desired,
	-- instead use get()
	-- @param name
	entry: (key)=> Entry @data, key

	--- Returns `true` if a key exists within the table, or otherwise `false`
	contains_key: (key)=> @data[key] != nil

	--- Return an `Option<T>` to the potential value in the Map, which can be
	-- used to see if a value exists. If you want to insert a default value in
	-- the case it doesn't exist, use entry()
	get: (key)=> Option @data[key]

	--- Return a value expected to exist in the Map
	expect: (key, message)=>
		@get(key)\expect message

	--- Set a `value` in the Map to an associated `key`
	-- @param key
	-- @param value
	set: (key, value)=>
		@data[key] = value

	--- Remove a `key` from a Map
	remove: (key)=> @data[key] = nil

	--- Iterate over the Map
	iter: => pairs @data

return :Result, :Ok, :Err, :Option, :Entry, :Map

local Result, Option, Ok, Err

expect_class = (value, expected)->
	assert value.__class == expected,
		"expected type #{expected.__name}<#{expected}>, got " ..
		"#{value.__class.__name}<#{value.__class}>"

class Result
	Ok: 0
	Err: 1

	new: (variant, content)=>
		@variant = variant
		@content = content

	is_ok: => @variant == Ok
	is_err: => @variant == Err

	--- Unwrap a result, returning the content or otherwise erroring
	expect: (error_value)=>
		@content if @is_ok! else error "#{error_value}: #{@content}"

	--- Raise error if is_err!, otherwise return content
	unwrap: =>
		@content if @is_ok! else error content

	--- Raises error if is_ok!, otherwise returns error value
	unwrap_err: =>
		@content if @is_err! else error content

	--- Returns content if is_ok!, otherwise `fallback`
	-- @param fallback
	unwrap_or: (fallback)=>
		@content if @is_ok! else fallback

	--- Returns content if is_ok!, otherwise the called output of `fallback_fn`
	-- @tparam fallback_fn
	unwrap_or_else: (fallback_fn)=>
		@content if @is_ok! else fallback_fn!

	ok: => Option(@content) if @is_ok! else Option!
	err: => Option(@content) if @is_err! else Option!

	--- Map Result<T, E> to Result<U, E> - Error can be kept if is_err!
	-- @tparam function fn Function to use for mapping content
	map: (fn)=>
		Ok fn @content if @is_ok else self

	--- Map Result<T, E> to Result<T, F> - Content can be kept if is_ok!
	-- @tparam function fn Function to use for mapping error
	map_err: (fn)=>
		if @is_err
			return Err fn @content
		self

	--- Return `right` if the variant is `Ok`, otherwise return self's `Err`
	-- @param right
	and_also: (right)=>
		self if @is_error! else right

	--- Return evaluated output of `right_fn` if the variant is `Ok`, otherwise
	-- return self's `Err`
	-- @tparam function right_fn
	and_then: (right_fn)=>
		self if @is_error! else right_fn @content

	--- Return `right` if the variant is `Err`, otherwise return self's `Ok`
	-- @param right
	or_other: (right)=>
		self if @is_ok! else right

	--- Return evaluated output of  `right_fn` if the variant is `Err`,
	-- otherwise return self's `Ok`
	-- @tparam function right_fn
	or_else: (right_fn)=>
		self if @is_ok! else right_fn @content

	--- Transpose a Result of an Option into an Option of a Result
	transpose: =>
		expect_class @content, Result
		if @content\is_some!
			return @variant @content.data -- Some
		Option! -- None

import Ok, Err from Result

result_variant_mt = {
	__call: (...)=> Result self, ...
}

debug.setmetatable Ok, result_variant_mt
debug.setmetatable Err, result_variant_mt

class Option
	new: (some=nil)=>
		if some
			@data = some

	is_some: => @data != nil
	is_none: => @data == nil

	--- Transform an Option<Option<T>> into an Option<T>
	flatten: =>
		expect_class @data, Option
		Option @data\expect!

	--- Unwrap an option, returning the content or otherwise erroring
	-- @param error_value Value raised if variant is None
	expect: (error_value)=>
		@data if @is_some! else Err(error_value)\unwrap!

	--- Moves a value `v` out of `Option<T>` if it is a `Some(v)` variant
	unwrap: =>
		@expect "called Option.unwrap() on a None variant"

	--- Return the contained value if is_some! or a fallback value
	-- @param fallback
	unwrap_or: (fallback)=>
		@data if @is_some! else fallback

	--- Return the contained value if is_some!, otherwise the called output of
	-- `fallback_fn`
	-- @tparam function fallback_fn
	unwrap_or_else: (fallback_fn)=>
		@data if @is_some! else fallback_fn!

	--- Applies a function to the contained value
	-- @tparam function fn Function to use for mapping content
	map: (fn)=>
		Option fn @data if @is_some! else self

	--- Applies a function to the contained value, or returns a fallback
	-- @param fallback
	-- @tparam function fn Function to use for mapping content
	map_or: (fallback, fn)=>
		Option fn @data if @is_some! else fallback

	--- Applies a function to the contained value, or calls fallback function
	-- @tparam function fallback_fn
	-- @tparam function fn Function to use for mapping content
	map_or_else: (fallback_fn, fn)=>
		Option fn @data if @is_some! else fallback_fn!

	--- Transform an `Option<T>` into a `Result<T, E>`, mapping `Some(v)` to
	-- `Ok(v)` and `None` to `Err(error_value)`
	-- @param error_value
	ok_or: (error_value)=>
		if @is_some!
			Ok @data
		else -- None
			Err error_value

	--- Transform an `Option<T>` into a `Result<T, E>`, mapping `Some(v)` to
	-- `Ok(v)` and `None` to `Err(error_fn!)`
	-- @tparam function error_fn
	ok_or_else: (error_fn)=>
		if @is_some!
			Ok @data
		else -- None
			Err error_fn!

	--- Return `None` if already `None`, otherwise call `predicate` function
	-- and returns `Some(v)` if the predicate function returns true, otherwise
	-- None if `predicate` returns false
	-- @tparam function predicate
	filter: (predicate)=>
		if @is_some! and predicate @data
			return @data
		Option!

	--- Return `right` if the variant is `Some`
	-- @param right
	and_also: (right)=>
		self if @is_none! else right

	--- Return evaluated output of `right_fn` if the variant is `Some`,
	-- otherwise return None
	-- @tparam function right_fn
	and_then: (right_fn)=>
		self if @is_none! else right_fn @data

	--- Return `right` if the variant is `None`, else return self's `Some`
	-- @param right
	or_other: (right)=>
		self if @is_some! else right

	--- Returns evaluated output of `right_fn` if the variant is `None`,
	-- otherwise returns self's `Some`
	-- @tparam function right_fn
	or_else: (right_fn)=>
		self if @is_some! else right_fn!

	--- Returns a `Some(value)` if either the self or the `right` have a
	-- `Some(value)`, but not both
	xor: (right)=>
		if (@is_some! and right\is_none!) or (@is_none! and right\is_some!)
			return @or_other right
		Option! -- None

	--- Transpose an Option of a Result into a Result of an Option
	transpose: =>
		if @is_none!
			return Ok self -- Result(None)
		@data.variant Option(@data.content)

class Entry
	new: (source, key)=>
		@source = source
		@key = key

	--- Determine whether or not a `key` exists currently in the table
	exists: => @source[@key] != nil

	--- Create a `default` value in the source if the key doesn't have an
	-- associated value already
	-- @param default
	or_insert: (default)=>
		source, key = @source, @key
		source[key] = default unless source[key]
		source[key]

	--- Create a default value from the result of `default_fn` in the source if
	-- the key doesn't have an associated value already
	-- @tparam function default_fn
	or_insert_with: (default_fn)=>
		source, key = @source, @key
		source[key] = default_fn! unless source[key]
		source[key]

	--- Apply a function `fn` to the entry, if it exists; should be called
	-- before returning the value, and can be chained
	-- @tparam function fn
	and_modify: (fn)=>
		source, key = @source, @key
		if source[key]
			source[key] = fn source[key]
		self

class Map
	new: (data={})=>
		@data = data

	--- Return an `Entry` to the potential value in the Map, which can be used
	-- to add a default value. If a default value isn't available or desired,
	-- instead use get()
	-- @param name
	entry: (key)=> Entry @data, key

	--- Returns `true` if a key exists within the table, or otherwise `false`
	contains_key: (key)=> @data[key] != nil

	--- Return an `Option<T>` to the potential value in the Map, which can be
	-- used to see if a value exists. If you want to insert a default value in
	-- the case it doesn't exist, use entry()
	get: (key)=> Option @data[key]

	--- Return a value expected to exist in the Map
	expect: (key)=> @get(key)\expect!

	--- Set a `value` in the Map to an associated `key`
	-- @param key
	-- @param value
	set: (key, value)=>
		@data[key] = value

	--- Remove a `key` from a Map
	remove: (key)=> @data[key] = nil

	--- Iterate over the Map
	iter: => pairs @data

return :Result, :Ok, :Err, :Option, :Entry, :Map


Now loads a secondary language backup translation

[=[
Class: Lang
A static class used for dealing with translated messages.

Revisions:
	4.0.0 - Initial.
]=]
class ulx.Lang
	@CurrentLanguage: nil
	@BackupLanguage: nil
	@Phrases: {}
	@Mutators: {}

	parseAndVerifyLangTxt = (txt using nil) ->
		phrases = util.JSONToTable txt
		if not phrases
			return false, "Unable to parse JSON"

		types = tableshape.types
		dictionary = types.map_of types.string, types.string
		valid, err = dictionary\check_value phrases

		if not valid
			return false, err

		return phrases

	loadPhrasesForLang = (lang using nil) ->
		langFiles = ulx.Directory.GetFiles("locale", lang .. ".json", true)
		phraseAccum = {}

		if #langFiles == 0
			return false, "Language files do not exist"

		for langFile in *langFiles
			txt = ulx.File.ReadAllText langFile
			phrases, err = parseAndVerifyLangTxt txt
			if not phrases
				print err
				--log.warn "invalid..." -- TODO
				continue
			ulx.TableX.UnionByKey phraseAccum, phrases, true
		return phraseAccum

	init = () ->
		configLang = ulx.CoreConfig.Language
		@.SetLanguage configLang
		return
	ulx.RegisterInitCallback init

	[=[
	Function: AddMutator
	TODO
	]=]
	@AddMutator: (name, func using nil) ->
		@Mutators[name] = func

	[=[
	Function: SetLanguage
	TODO
	]=]
	@SetLanguage: (language, backupLanguage="english" using nil) ->
		ulx.UtilX.CheckArg "Language.SetLanguage", 1, "string", language
		ulx.UtilX.CheckArg "Language.SetLanguage", 2, "string", backupLanguage

		@CurrentLanguage = language\lower!
		@BackupLanguage = backupLanguage\lower!

		primaryPhrases, errPrimary = loadPhrasesForLang @CurrentLanguage

		if @BackupLanguage ~= @CurrentLanguage
			secondaryPhrases, errSecondary = loadPhrasesForLang @BackupLanguage
			if secondaryPhrases
				primaryPhrases = ulx.TableX.UnionByKey secondaryPhrases, primaryPhrases or {}, true

		if not primaryPhrases
			return false, errPrimary

		@Phrases = primaryPhrases
		return true

	[=[
	Function: GetPhrase
	TODO
	]=]
	@GetPhrase: (phraseName using nil) ->
		@Phrases[phraseName]

	[=[
	Function: GetMutatedPhrase
	TODO
	]=]
	@GetMutatedPhrase: (phraseName, data using nil) ->
		phrase = @.GetPhrase phraseName
		mutatedPhrase = phrase\gsub "{.-}", (placeholder using nil) ->
			processPlaceholder(placeholder, data)
		ulx.UtilX.Trim mutatedPhrase

	-- Receives everything between and including the brackets "{DAMAGE|NonZero:for %i damage}"
	processPlaceholder = (placeholder, data using nil) ->
		curPos = placeholder\find("|", 3, true) or #placeholder -- next pipe or the end bracket
		placeholderName = placeholder\sub(2, curPos-1)

		mutatorFnsAndArgs = {}
		while placeholder\sub(curPos, curPos) == "|"
			nextPos = placeholder\find("|", curPos+2, true) or #placeholder -- next pipe or the end bracket
			functionBlock = placeholder\sub(curPos+1, nextPos-1)
			fn, args = getFnAndArgs functionBlock, mutated
			table.insert mutatorFnsAndArgs, {fn, args}
			curPos = nextPos

		replacement = data[placeholderName]
		mutated = pipeMutators replacement, mutatorFnsAndArgs
		if type(mutated) == "table"
			mutated = listPipeMutators replacement, mutatorFnsAndArgs

		return mutated

	-- Receives a mutator function and all its arguments "NonZero:for %i damage"
	getFnAndArgs = (functionBlock, txt) ->
		curPos = functionBlock\find(":", 2, true) or #functionBlock+1 -- next colon of end of string
		functionName = functionBlock\sub(1, curPos-1)
		args = {}

		while functionBlock\sub(curPos, curPos) == ":"
			nextPos = functionBlock\find(":", curPos+1, true) or #functionBlock+1 -- next colon of end of string
			arg = functionBlock\sub(curPos+1, nextPos-1)
			table.insert args, arg
			curPos = nextPos

		return @Mutators[functionName], args

	-- Make a string from a list while piping each item
	listPipeMutators = (list, ... using nil) ->
		str = ""
		for i=1, #list-2
			str ..= pipeMutators(list[i], ...) .. ", "

		if #list > 1
			mutated = pipeMutators(list[#list-1], ...)
			conjuction = " " .. @.GetPhrase("AND") .. " "
			str ..= mutated .. conjuction

		str ..= pipeMutators(list[#list], ...)
		return str

	-- Send a string through each mutator function with the specified arguments
	pipeMutators = (replacement, mutatorFnsAndArgs using nil) ->
		for mutatorFnAndArgs in *mutatorFnsAndArgs
			fn = mutatorFnAndArgs[1]
			args = mutatorFnAndArgs[2]
			replacement = fn(replacement, unpack(args))
		return replacement

[=[
Class: Lang
A static class used for dealing with translated messages.

Revisions:
	4.0.0 - Initial.
]=]
class ulx.Lang
	@CurrentLanguage: "english"
	@BackupLanguage: "english"
	@Phrases: {}
	@Mutators: {}

	parseAndVerifyLangTxt = (txt using nil) ->
		phrases = util.JSONToTable txt
		if not phrases
			return false, "unable to parse JSON"

		types = tableshape.types
		dictionary = types.map_of types.string, types.string
		valid, err = dictionary\check_value phrases

		if not valid
			return false, err

		return phrases

	init = () ->
		configLang = ulx.CoreConfig.Language
		@.SetLanguage configLang
		return
	ulx.RegisterInitCallback init

	[=[
	Function: AddMutator
	TODO
	]=]
	@AddMutator: (name, func using nil) ->
		@Mutators[name] = func

	[=[
	Function: SetLanguage
	TODO
	]=]
	@SetLanguage: (language using nil) ->
		language = language\lower!
		@CurrentLanguage = language
		langFiles = ulx.Directory.GetFiles("locale", language .. ".json", true)
		phraseAccum = {}

		if #langFiles == 0
			return false

		for langFile in *langFiles
			txt = ulx.File.ReadAllText langFile
			phrases, err = parseAndVerifyLangTxt txt
			if not phrases
				print err
				--log.warn "invalid..." -- TODO
				continue
			ulx.TableX.UnionByKey phraseAccum, phrases, true

		@Phrases = phraseAccum
		return true

	[=[
	Function: GetPhrase
	TODO
	]=]
	@GetPhrase: (phraseName using nil) ->
		@Phrases[phraseName]

	[=[
	Function: GetMutatedPhrase
	TODO
	]=]
	@GetMutatedPhrase: (phraseName, data using nil) ->
		phrase = @.GetPhrase phraseName
		mutatedPhrase = phrase\gsub "{.-}", (placeholder using nil) ->
			processPlaceholder(placeholder, data)
		ulx.UtilX.Trim mutatedPhrase

	-- Receives everything between and including the brackets "{DAMAGE|NonZero:for %i damage}"
	processPlaceholder = (placeholder, data using nil) ->
		curPos = placeholder\find("|", 3, true) or #placeholder -- next pipe or the end bracket
		placeholderName = placeholder\sub(2, curPos-1)

		mutatorFnsAndArgs = {}
		while placeholder\sub(curPos, curPos) == "|"
			nextPos = placeholder\find("|", curPos+2, true) or #placeholder -- next pipe or the end bracket
			functionBlock = placeholder\sub(curPos+1, nextPos-1)
			fn, args = getFnAndArgs functionBlock, mutated
			table.insert mutatorFnsAndArgs, {fn, args}
			curPos = nextPos

		replacement = data[placeholderName]
		mutated = pipeMutators replacement, mutatorFnsAndArgs
		if type(mutated) == "table"
			mutated = listPipeMutators replacement, mutatorFnsAndArgs

		return mutated

	-- Receives a mutator function and all its arguments "NonZero:for %i damage"
	getFnAndArgs = (functionBlock, txt) ->
		curPos = functionBlock\find(":", 2, true) or #functionBlock+1 -- next colon of end of string
		functionName = functionBlock\sub(1, curPos-1)
		args = {}

		while functionBlock\sub(curPos, curPos) == ":"
			nextPos = functionBlock\find(":", curPos+1, true) or #functionBlock+1 -- next colon of end of string
			arg = functionBlock\sub(curPos+1, nextPos-1)
			table.insert args, arg
			curPos = nextPos

		return @Mutators[functionName], args

	-- Make a string from a list while piping each item
	listPipeMutators = (list, ... using nil) ->
		str = ""
		for i=1, #list-2
			str ..= pipeMutators(list[i], ...) .. ", "

		if #list > 1
			mutated = pipeMutators(list[#list-1], ...)
			conjuction = " " .. @.GetPhrase("AND") .. " "
			str ..= mutated .. conjuction

		str ..= pipeMutators(list[#list], ...)
		return str

	-- Send a string through each mutator function with the specified arguments
	pipeMutators = (replacement, mutatorFnsAndArgs using nil) ->
		for mutatorFnAndArgs in *mutatorFnsAndArgs
			fn = mutatorFnAndArgs[1]
			args = mutatorFnAndArgs[2]
			replacement = fn(replacement, unpack(args))
		return replacement


Fix issue with has_data in xp manager

Fix issue with has_data in xp manager

-- Copyright (c) 2014 by Adam Hellberg <sharparam@sharparam.com>.
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.

NAME, T = ...

{
    comm_manager: comm
    event_manager: em
    localization: L
    :log
    misc:
        :fix_name, :is_any_nil
} = T

{
    :GetExpansionLevel, :GetRealmName, :GetNumSubgroupMembers
    :IsInGroup
    :LE_PARTY_CATEGORY_HOME
    :next
    :UnitLevel, :UnitName, :UnitXP, :UnitXPMax
} = _G

T.xp_manager =
    max_levels:
        [0]: 60  -- Classic/Vanilla
        [1]: 70  -- TBC
        [2]: 80  -- WotLK
        [3]: 85  -- Cataclysm
        [4]: 90  -- MoP
        [5]: 100 -- WoD
    max_level: 255 -- Placeholder/fallback value replaced at load
    data: {}

    has_data: =>
        next(@data) != nil

    update: (name, current_level, max_level, current_xp, max_xp) =>
        name = fix_name name
        current_level = tonumber current_level
        max_level = tonumber max_level
        current_xp = tonumber current_xp
        max_xp = tonumber max_xp

        if is_any_nil current_level, max_level, current_xp, max_xp
            return log\error L.xp.invalid_data, name

        unless @data[name]
            @data[name] = {}
            realm = name\match '-(.+)$'
            local_realm = GetRealmName!\gsub '%s', ''
            @data[name].friendly_name = (realm == local_realm) and name\match('(.+)-') or name

        data = with @data[name]
            .current_level = current_level
            .max_level = max_level
            .current_xp = current_xp
            .max_xp = max_xp
        em\fire 'SHAREXP_XP_UPDATED'
        log\debug L.xp_updated, data.friendly_name

    send_xp: =>
        lvl = UnitLevel 'player'
        xp = UnitXP 'player'
        max_xp = UnitXPMax 'player'
        comm\send 'PARTY', 'xpupdate', lvl, @max_level, xp, max_xp
        log\debug L.xp_sent_update

    request_xp: =>
        comm\send 'PARTY', 'xprequest'
        log\debug L.xp_sent_request

    check_group: =>
        num = GetNumSubgroupMembers LE_PARTY_CATEGORY_HOME
        group = {}
        for i = 1, num
            unit = 'party' .. i
            name, realm = UnitName unit
            name = fix_name name, realm
            group[name] = true

        for k, _ in pairs @data
            unless group[k]
                log\debug L.xp.removing, k
                @data[k] = nil
                em\fire 'SHAREXP_XP_UPDATED'

xp = T.xp_manager

comm\register_callback 'xpupdate', (kind, channel, sender, ...) ->
    -- ARGS:
    -- 1: current level
    -- 2: max level
    -- 3: current xp
    -- 4: max xp
    log\debug L.xp_received_update, channel, sender
    xp\update sender, ...

comm\register_callback 'xprequest', (kind, channel, sender) ->
    log\debug L.xp_received_request, channel, sender
    xp\send_xp!

T.PLAYER_ENTERING_WORLD = ->
    xp\send_xp!
    xp\request_xp! if IsInGroup!

T.PLAYER_XP_UPDATE = ->
    xp\send_xp!

T.GROUP_ROSTER_UPDATE = ->
    xp\send_xp!
    xp\check_group!

T.SHAREXP_LOADED = ->
    expansion_level = GetExpansionLevel!
    max_lvl = xp.max_levels[expansion_level]
    xp.max_level = max_lvl

-- Copyright (c) 2014 by Adam Hellberg <sharparam@sharparam.com>.
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.

NAME, T = ...

{
    comm_manager: comm
    event_manager: em
    localization: L
    :log
    misc:
        :fix_name, :is_any_nil
} = T

{
    :GetExpansionLevel, :GetRealmName, :GetNumSubgroupMembers
    :IsInGroup
    :LE_PARTY_CATEGORY_HOME
    :UnitLevel, :UnitName, :UnitXP, :UnitXPMax
} = _G

T.xp_manager =
    max_levels:
        [0]: 60  -- Classic/Vanilla
        [1]: 70  -- TBC
        [2]: 80  -- WotLK
        [3]: 85  -- Cataclysm
        [4]: 90  -- MoP
        [5]: 100 -- WoD
    max_level: 255 -- Placeholder/fallback value replaced at load
    data: {}

    has_data: =>
        #@data > 0

    update: (name, current_level, max_level, current_xp, max_xp) =>
        name = fix_name name
        current_level = tonumber current_level
        max_level = tonumber max_level
        current_xp = tonumber current_xp
        max_xp = tonumber max_xp

        if is_any_nil current_level, max_level, current_xp, max_xp
            return log\error L.xp.invalid_data, name

        unless @data[name]
            @data[name] = {}
            realm = name\match '-(.+)$'
            local_realm = GetRealmName!\gsub '%s', ''
            @data[name].friendly_name = (realm == local_realm) and name\match('(.+)-') or name

        data = with @data[name]
            .current_level = current_level
            .max_level = max_level
            .current_xp = current_xp
            .max_xp = max_xp
        em\fire 'SHAREXP_XP_UPDATED'
        log\debug L.xp_updated, data.friendly_name

    send_xp: =>
        lvl = UnitLevel 'player'
        xp = UnitXP 'player'
        max_xp = UnitXPMax 'player'
        comm\send 'PARTY', 'xpupdate', lvl, @max_level, xp, max_xp
        log\debug L.xp_sent_update

    request_xp: =>
        comm\send 'PARTY', 'xprequest'
        log\debug L.xp_sent_request

    check_group: =>
        num = GetNumSubgroupMembers LE_PARTY_CATEGORY_HOME
        group = {}
        for i = 1, num
            unit = 'party' .. i
            name, realm = UnitName unit
            name = fix_name name, realm
            group[name] = true

        for k, _ in pairs @data
            unless group[k]
                log\debug L.xp.removing, k
                @data[k] = nil
                em\fire 'SHAREXP_XP_UPDATED'

xp = T.xp_manager

comm\register_callback 'xpupdate', (kind, channel, sender, ...) ->
    -- ARGS:
    -- 1: current level
    -- 2: max level
    -- 3: current xp
    -- 4: max xp
    log\debug L.xp_received_update, channel, sender
    xp\update sender, ...

comm\register_callback 'xprequest', (kind, channel, sender) ->
    log\debug L.xp_received_request, channel, sender
    xp\send_xp!

T.PLAYER_ENTERING_WORLD = ->
    xp\send_xp!
    xp\request_xp! if IsInGroup!

T.PLAYER_XP_UPDATE = ->
    xp\send_xp!

T.GROUP_ROSTER_UPDATE = ->
    xp\send_xp!
    xp\check_group!

T.SHAREXP_LOADED = ->
    expansion_level = GetExpansionLevel!
    max_lvl = xp.max_levels[expansion_level]
    xp.max_level = max_lvl


Make it easier to capture errors

Make it easier to capture errors

Instead of capture_errors boilerplate, we now have a safe_route wrapper
which takes care of everything for us.

lapis = require "lapis"
config = (require "lapis.config").get!
content = require "content"

import after_dispatch from require "lapis.nginx.context"
import to_json from require "lapis.util"
import capture_errors, assert_error, yield_error from require "lapis.application"

class CSWeek extends lapis.Application
    layout: require "views.layout"

    error_handler: (context, code) =>
        code = code or 500
        context = context or self

        with context
            .m, err = content\get "error"
            if err then
                print "Could not load the error page (code #{code}): #{err}"
                .m = { }

            .m[code] = .m[code] or .m.default or { }
            .page_id = "error"
            .status = code
            .m.title = .m[code].title

        render: "error", status: code

    safe_route: (fn) =>
        captured_self = self
        capture_errors {
            on_error: =>
                @app\error_handler self

            =>
                fn captured_self
        }

    try_render: (template, context) =>
        context = context or self
        with context
            .nav = assert_error content\get "nav"
            .footer = assert_error content\get "footer"
        render: template

    @before_filter =>
        if #[n for n in *{'production-perftest', 'development-perftest'} when n == config._name] > 0
            after_dispatch ->
                print to_json(ngx.ctx.performance)

    "/": =>
        @app.safe_route self, ->
            @page_id = "home"
            @m = assert_error content\get "home"
            print @m.title
            @lecturers = assert_error content\get "lecturers"
            @app\try_render "home", self

    "/midza": =>
        @app.safe_route self, ->
            @html ->
                p "dobar chkode"

    handle_404: =>
        @errors = "Route `#{self.req.parsed_url.path or 'unknown'}` not found"
        @app\error_handler self, 404

lapis = require "lapis"
config = (require "lapis.config").get!
content = require "content"

import after_dispatch from require "lapis.nginx.context"
import to_json from require "lapis.util"
import capture_errors, assert_error, yield_error from require "lapis.application"

class CSWeek extends lapis.Application
    layout: require "views.layout"

    error_handler: (context, code) =>
        code = code or 500
        context = context or self

        with context
            .m, err = content\get "error"
            if err then
                print "Could not load the error page (code #{code}): #{err}"
                .m = { }

            .m[code] = .m[code] or .m.default or { }
            .page_id = "error"
            .status = code
            .title = .m[code].title

        render: "error", status: code

    try_render: (template, context) =>
        context = context or self
        with context
            .nav = assert_error content\get "nav"
            .footer = assert_error content\get "footer"
        render: template

    @before_filter =>

        if #[n for n in *{'production-perftest', 'development-perftest'} when n == config._name] > 0
            after_dispatch ->
                print to_json(ngx.ctx.performance)

    "/": capture_errors {
        on_error: =>
             @app\error_handler self

        =>
            @page_id = "home"
            @m = assert_error content\get "home"
            @lecturers = assert_error content\get "lecturers"
            @app\try_render "home", self
    }

    handle_404: =>
        @errors = "Route `#{self.req.parsed_url.path or 'unknown'}` not found"
        @app\error_handler self, 404