-- [ʞ] sirsem.lua
--  ~ lexu hale <lexi@hale.su>
--  ? utility library with functionality common to
--    cortav.lua and its extensions
--    from Ranuir "software utility"
--  > local ss = require 'sirsem.lua'

local ss
do -- pull ourselves up by our own bootstraps
	local package = _G.package -- prevent namespace from being broken by env shenanigans
	local function namespace(name, tbl)
		local pkg = tbl or {}
		if package then
			package.loaded[name] = pkg
		end
		return pkg
	end
	ss = namespace 'sirsem'
	ss.namespace = namespace
end

function ss.map(fn, lst)
	local new = {}
	for k,v in pairs(lst) do
		table.insert(new, fn(v,k))
	end
	return new
end
function ss.reduce(fn, acc, lst)
	for i,v in ipairs(lst) do
		acc = fn(acc, v, i)
	end
	return acc
end
function ss.filter(list, fn)
	local new = {}
	for i, v in ipairs(list) do
		if fn(v,i) then table.insert(new, v) end
	end
	return new
end
function ss.kmap(fn, list)
	local new = {}
	for k, v in pairs(list) do
		local nk,nv = fn(k,v)
		new[nk or k] = nv or v
	end
	return new
end

function ss.kfilter(list, fn)
	local new = {}
	for k, v in pairs(list) do
		if fn(k,v) then new[k] = v end
	end
	return new
end

function ss.find(tbl, pred, ...)
	pred = pred or function(a,b)
		return a == b
	end
	for k,v in pairs(tbl) do
		if pred(v,k,...) then return k,v end
	end
	return nil
end

function ss.clone(tbl) -- performs a shallow copy
	if tbl.clone then return tbl:clone() end
	local new = {}
	for k,v in pairs(tbl) do
		new[k] = v
	end
	return new
end

function ss.copy(tbl) -- performs a deep copy
	local new = {}
	for k,v in pairs(tbl) do
		if type(v) == 'table' then
			if v.clone then
				new[k] = v:clone() -- this may or may not do a deep copy!
			else
				new[k] = ss.copy(v)
			end
		else
			new[k] = v
		end
	end
	return new
end

function ss.delegate(tbl,tpl) -- returns a table that looks up keys it lacks from
                              -- tbl (lightweight alternative to shallow copies)
	tpl = tpl or {}
	return setmetatable({}, {__index=tbl})
end

ss.str = {}

function ss.str.begins(str, pfx)
	return string.sub(str, 1, #pfx) == pfx
end

ss.str.enc = {
	utf8 = {
		char = utf8.char;
		codepoint = utf8.codepoint;
	};
	c6b = {};
	ascii = {};
}

function ss.str.enc.utf8.each(str, ascode)
	local pos = {
		code = 1;
		byte = 1;
	}
	return function()
		if pos.byte > #str then return nil end
		local thischar = utf8.codepoint(str, pos.byte)
		local lastpos = {
			code = pos.code;
			byte = pos.byte;
			next = pos;
		}
		if not ascode then
			thischar = utf8.char(thischar)
			pos.byte = pos.byte + #thischar
		else
			pos.byte = pos.byte + #utf8.char(thischar)
		end
		pos.code = pos.code + 1
		return thischar, lastpos
	end
end

ss.math = {}

function ss.math.lerp(t, a, b)
	return (1-t)*a + (t*b)
end

function ss.dump(o, state, path, depth)
	state = state or {tbls = {}}
	depth = depth or 0
	local pfx = string.rep('   ', depth)
	if type(o) == "table" then
		local str = ''
		for k,p in pairs(o) do
			local done = false
			local exp
			if type(p) == 'table' then
				if state.tbls[p] then
					exp = '<' .. state.tbls[p] ..'>'
					done = true
				else
					state.tbls[p] = path and string.format('%s.%s', path, k) or k
				end
			end
			if not done then
				local function dodump() return ss.dump(
					p, state,
					path and string.format("%s.%s", path, k) or k,
					depth + 1
				) end
				-- boy this is ugly
				if type(p) ~= 'table' or
					getmetatable(p) == nil or
					getmetatable(p).__tostring == nil then
					exp = dodump()
				end
				if type(p) == 'table' then
					exp = string.format('{\n%s%s}', exp, pfx)
					local meta = getmetatable(p)
					if meta then
						if meta.__tostring then
							exp = tostring(p)
						end
						if meta.__name then
							exp = meta.__name .. ' ' .. exp
						end
					end
				end
			end
			str = str .. pfx .. string.format("%s = %s\n", k, exp)
		end
		return str
	elseif type(o) == "string" then
		return string.format('“%s”', o)
	else
		return tostring(o)
	end
end

function ss.hexdump(s)
	local hexlines, charlines = {},{}
	for i=1,#s do
		local line = math.floor((i-1)/16) + 1
		hexlines[line] = (hexlines[line] or '') .. string.format("%02x ",string.byte(s, i))
		charlines[line] = (charlines[line] or '') .. ' ' .. string.gsub(string.sub(s, i, i), '[^%g ]', '\x1b[;35m·\x1b[36;1m') .. ' '
	end
	local str = ''
	for i=1,#hexlines do
		str = str .. '\x1b[1;36m' .. charlines[i] .. '\x1b[m\n' .. hexlines[i] .. '\n'
	end
	return str
end

function ss.declare(c)
	local cls = setmetatable({
		__name = c.ident;
	}, {
		__name = 'class';
		__tostring = function() return c.ident or '(class)' end;
	})

	cls.__call = c.call
	cls.__index = function(self, k)
		if c.default and c.default[k] then
			return c.default[k]
		end
		if k == 'clone' then
			return function(self)
				local new = cls.mk()
				for k,v in pairs(self) do
					new[k] = v
				end
				if c.clonesetup then
					c.clonesetup(new, self)
				end
				return new
			end
		elseif k == 'to' then
			return function(self, to, ...)
				if to == 'string' then return tostring(self)
				elseif to == 'number' then return tonumber(self)
				elseif to == 'int' then return math.floor(tonumber(self))
				elseif c.cast and c.cast[to] then
					return c.cast[to](self, ...)
				elseif type(to) == 'table' and getmetatable(to) and getmetatable(to).cvt and getmetatable(to).cvt[cls] then
				else error((c.ident or 'class') .. ' is not convertible to ' .. (type(to) == 'string' and to or tostring(to))) end
			end
		end
		if c.fns then return c.fns[k] end
	end

	if c.cast then
		if c.cast.string then
			cls.__tostring = c.cast.string
		end
		if c.cast.number then
			cls.__tonumber = c.cast.number
		end
	end

	cls.mk = function(...)
		local val = setmetatable(c.mk and c.mk(...) or {}, cls)
		if c.init then
			for k,v in pairs(c.init) do
				val[k] = v
			end
		end
		if c.construct then
			c.construct(val, ...)
		end
		return val
	end
	getmetatable(cls).__call = function(_, ...) return cls.mk(...) end
	cls.is = function(o) return getmetatable(o) == cls end
	return cls
end

-- tidy exceptions

ss.exn = ss.declare {
	ident = 'exn';
	mk = function(kind, ...)
		return {
			vars = {...};
			kind = kind;
		}
	end;
	cast = {
		string = function(me)
			return me.kind.report(table.unpack(me.vars))
		end;
	};
	fns = {
		throw = function(me) error(me) end;
	}
}
ss.exnkind = ss.declare {
	ident = 'exn-kind';
	mk = function(desc, report)
		return {
			desc = desc;
			report = report or function(msg,...)
				return string.format(msg,...)
			end;
		}
	end;
	call = function(me, ...)
		return ss.exn(me, ...)
	end;
}
ss.str.exn = ss.exnkind 'failure while string munging'

function ss.str.delimit(encoding, start, stop, s)
	local depth = 0
	encoding = encoding or ss.str.enc.utf8
	if not ss.str.begins(s, start) then return nil end
	for c,p in encoding.each(s) do
		if c == (encoding.escape or '\\') then
			p.next.byte = p.next.byte + #encoding.char(encoding.codepoint(s, p.next.byte))
			p.next.code = p.next.code + 1
		elseif c == start then
			depth = depth + 1
		elseif c == stop then
			depth = depth - 1
			if depth == 0 then
				return s:sub(1+#start, p.byte - #stop), p.byte -- FIXME
			elseif depth < 0 then
				ss.str.exn('out of place token “%s”', stop):throw()
			end
		end
	end

	ss.str.exn('token “%s” expected before end of line', stop):throw()
end

ss.version = ss.declare {
	name = 'version';
	mk = function(tbl) return tbl end;
	fns = {
		pre = function(self,other) end;
		post = function(self,other) end;
		string = function(self) return tostring(self) end;		
	};
	cast = {
		string = function(vers)
			if not(next(vers)) then return '0.0' end
			local str = ''
			for _,v in pairs(vers) do
				if type(v) == 'string' then
					if str ~= '' then str = str .. '-' end
					str = str .. v
				else
					if str ~= '' then str = str .. '.' end
					str = str .. tostring(v)
				end
			end
			return str
		end
	};
}

function ss.classinstance(o)
	local g = getmetatable(o)
	if not o then return nil end
	local mm = getmetatable(g)
	if not o then return nil end
	if mm.__name == 'class' then
		return g
	else
		return nil
	end
end

function ss.walk(o, key, ...)
	if o[key] then
		if select('#', ...) == 0 then
			return o[key]
		else
			return ss.walk(o[key], ...)
		end
	end
	return nil
end

function ss.coalesce(x, ...)
	if x ~= nil then
		return x
	elseif select('#', ...) == 0 then
		return nil
	else
		return ss.coalesce(...)
	end
end
