-- elua core utilities used in other modules

local ffi  = require("ffi")
local cast = ffi.cast
local new  = ffi.new
local copy = ffi.copy
local str  = ffi.string
local gc   = ffi.gc

local C = ffi.C

local M = {}

local getmetatable, setmetatable = getmetatable, setmetatable
local dgetmt = debug.getmetatable

-- multiple inheritance index with depth-first search
local proto_lookup = function(protos, name)
    if not protos then return nil end
    for i = 1, #protos do
        local proto = protos[i]
        local v = proto[name]
        if v ~= nil then
            return v
        end
    end
end

local Object_MT = {
    __index = function(self, name)
        local v = proto_lookup(self.__mixins, name)
        if v == nil then
            return proto_lookup(self.__protos, name)
        end
    end,

    __tostring = function(self)
        local  f = self["__tostring"]
        if not f then
            return ("Object: %s"):format(self.name or "unnamed")
        end
        return f(self)
    end,

    __call = function(self, ...)
        return self["__call"](self, ...)
    end
}

local obj_gc = function(px)
    local obj = dgetmt(px).__obj
    local dtor = obj and obj.__dtor or nil
    if dtor then dtor(obj) end
end

M.Object = {
    __enable_dtor = false,

    __call = function(self, ...)
        local r = self:clone()
        if self.__enable_dtor then
            local px = newproxy(true)
            local pxmt = dgetmt(px)
            r.__gcproxy = px
            pxmt.__gc = obj_gc
            pxmt.__obj = r
        end
        if self.__ctor then return r, self.__ctor(r, ...) end
        return r
    end,

    clone = function(self, o)
        o = o or {}
        o.__protos, o.__mixins = { self }, {}
        setmetatable(o, Object_MT)
        return o
    end,

    is_a = function(self, base)
        if self == base then return true end
        local protos = self.__protos
        for i = 1, #protos do
            if protos[i]:is_a(base) then
                return true
            end
        end
        return false
    end,

    add_parent = function(self, parent)
        local protos = self.__protos
        protos[#protos + 1] = parent
    end,

    add_mixin = function(self, mixin)
        local mixins = self.__mixins
        mixins[#mixins + 1] = mixin
    end
}

local newproxy = newproxy

local robj_gc = function(px)
    local dtor = px.__dtor
    if dtor then dtor(px) end
end

M.Readonly_Object = M.Object:clone {}
M.Readonly_Object.__call = function(self, ...)
    local r = newproxy(true)
    local rmt = getmetatable(r)
    rmt.__index = self
    rmt.__tostring = Object_MT.__tostring
    rmt.__metatable = false
    if self.__enable_dtor then
        rmt.__gc = robj_gc
    end
    if self.__ctor then return r, self.__ctor(r, rmt, ...) end
    return r
end

local loaded_libs = {}
local loaded_libc = {}

local load_lib_win = function(libname, ev)
    local succ, v
    if not ev or ev == "" then
        succ, v = pcall(ffi.load, libname)
        if not succ then
            succ, v = pcall(ffi.load, "lib" .. libname)
        end
    else
        succ, v = pcall(ffi.load, ev .. "\\" .. libname .. ".dll")
        if not succ then
            succ, v = pcall(ffi.load, ev .. "\\lib" .. libname .. ".dll")
        end
    end
    return succ, v
end

local load_lib = function(libname, ev)
    local succ, v
    if ffi.os == "Windows" then
        succ, v = load_lib_win(libname, ev)
    elseif not ev or ev == "" then
        succ, v = pcall(ffi.load, libname)
    else
        local ext = (ffi.os == "OSX") and ".dylib" or ".so"
        succ, v = pcall(ffi.load, ev .. "/lib" .. libname .. ext)
    end
    return succ, v
end

-- makes sure we only keep one handle for each lib
-- reference counted
M.lib_load = function(libname)
    local  lib = loaded_libs[libname]
    if not lib then
        local ev = os.getenv("ELUA_" .. libname:upper() .. "_LIBRARY_PATH")
        local succ, v = load_lib(libname, ev)
        if not succ then
            error(v, 2)
        end
        lib = v
        loaded_libs[libname] = lib
        loaded_libc[libname] = 0
    end
    loaded_libc[libname] = loaded_libc[libname] + 1
    return lib
end

M.lib_unload = function(libname)
    local  cnt = loaded_libc[libname]
    if not cnt then return end
    if cnt == 1 then
        loaded_libs[libname], loaded_libc[libname] = nil, nil
    else
        loaded_libc[libname] = cnt - 1
    end
end

-- string fmt

ffi.cdef [[
    typedef struct _Str_Buf {
        char  *buf;
        size_t len, cap;
    } Str_Buf;

    void *malloc(size_t);
    void    free(void*);
    size_t  strlen(const char *str);

    int isalnum(int c);
    int isdigit(int c);
]]

local fmt   = string.format
local pcall = pcall
local error = error
local type  = type
local tostr = tostring

local bytes = { ("cdeEfgGiopuxXsq"):byte(1, #("cdeEfgGiopuxXsq")) }
for i, v in ipairs(bytes) do bytes[v] = true end

local Str_Buf = ffi.metatype("Str_Buf", {
    __new = function(self)
        local r = new("Str_Buf")
        r.buf = C.malloc(8)
        r.len = 0
        r.cap = 8
        gc(r, self.free)
        return r
    end,
    __tostring = function(self)
        return str(self.buf, self.len)
    end,
    __index = {
        free = function(self)
            C.free(self.buf)
            self.buf = nil
            self.len = 0
            self.cap = 0
        end,
        clear = function(self)
            self.len = 0
        end,
        grow = function(self, newcap)
            local oldcap = self.cap
            if oldcap >= newcap then return end
            local buf = C.malloc(newcap)
            if self.len ~= 0   then copy(buf, self.buf, self.len) end
            if self.buf ~= nil then C.free(self.buf) end
            self.buf = buf
            self.cap = newcap
        end,
        append_char = function(self, c)
            local len = self.len
            self:grow (len + 1)
            self.buf  [len] = c
            self.len = len + 1
        end,
        append_str = function(self, str, strlen)
            if type(str) == "string" then strlen = strlen or #str end
            local strp = cast("const char*", str)
            strlen = strlen or C.strlen(strp)
            local len = self.len
            self:grow(len + strlen)
            for i = 0, strlen - 1 do
                self.buf[len + i] = strp[i]
            end
            self.len = len + strlen
        end
    }
})

local fmterr = function(idx, msg, off)
    local argerr = (type(idx) == "number")
        and ("#" .. idx)
         or ("'" .. idx .. "'")
    error("bad argument " .. argerr .. " to '%' (" .. msg .. ")",
        3 + (off or 0))
end

-- simulates lua's coercion
local checktype = function(c, idx, val)
    if c == 115 or c == 112 then -- s, p
        return val
    end
    local tv = type(val)
    if c == 113 then -- q
        if tv ~= "string" or tv ~= "number" then
            fmterr(idx, "string expected, got " .. tv, 1)
        end
        return val
    end
    if tv == "number" then return val end
    if tv == "string" then
        local v = tonumber(val)
        if v then return v end
    end
    fmterr(idx, "number expected, got " .. tv, 1)
end

getmetatable("").__mod = function(fmts, params)
    if not fmts then return nil end
    if type(params) ~= "table" then
        params = { params }
    end

    local buf, nbuf = Str_Buf(), Str_Buf()
    local s         = cast("const char*", fmts)
    local c, s      = s[0], s + 1
    local argn      = 1

    while c ~= 0 do
        if c == 37 then -- %
            c, s = s[0], s + 1
            if c ~= 37 then
                while c ~= 0 and C.isalnum(c) ~= 0 do
                    nbuf:append_char(c)
                    c, s = s[0], s + 1
                end
                if c == 36 then -- $
                    c, s = s[0], s + 1
                    local n = tostr(nbuf)
                    nbuf:clear()
                    while C.isdigit(c) ~= 0 or c == 45 or c == 46 do -- -, .
                        nbuf:append_char(c)
                        c, s = s[0], s + 1
                    end
                    if not bytes[c] then
                        buf:append_str(n)
                        buf:append_char(36) -- $
                        buf:append_char(c)
                    else
                        nbuf:append_char(c)
                        local idx = tonumber(n) or n
                        if type(idx) == "number" and idx > #params then
                            fmterr(idx, "no value")
                        end
                        local v = params[idx]
                        v = checktype(c, idx, v)
                        buf:append_str(("%" .. tostr(nbuf)):format(v))
                        nbuf:clear()
                    end
                else
                    local fmtmark = (nbuf.len > 0) and nbuf.buf[0] or nil
                    if not fmtmark then
                        while c ~= 0 and (C.isdigit(c) ~= 0 or c == 45
                        or c == 46) do
                            nbuf:append_char(c)
                            c, s = s[0], s + 1
                        end
                        if bytes[c] then fmtmark = c end
                    end
                    if fmtmark then
                        if argn > #params then
                            fmterr(argn, "no value")
                        end
                        local v = params[argn]
                        v = checktype(fmtmark, argn, v)
                        buf:append_str(("%" .. tostr(nbuf)):format(v))
                        nbuf:clear()
                        argn = argn + 1
                    else
                        buf:append_str(nbuf.buf, nbuf.len)
                        nbuf:clear()
                    end
                    if c ~= 0 then buf:append_char(c) end
                end
            else
                buf:append_char(c)
            end
        else
            buf:append_char(c)
        end
        c, s = s[0], s + 1
    end
    nbuf:free()
    local ret = tostr(buf)
    buf:free()
    return ret
end

-- file utils

M.find_file = function(fname, paths)
    for i, path in ipairs(paths) do
        local actual_path
        if path:match(".*/") then
            actual_path = path .. fname
        else
            actual_path = path .. "/" .. fname
        end
        local f = io.open(actual_path)
        if f then
            f:close()
            return actual_path
        end
    end
end

-- table utils

table.uniq = function(tbl)
    local ret  = {}
    local used = {}
    for i, v in ipairs(tbl) do
        if not used[v] then
            ret[#ret + 1], used[v] = v, true
        end
    end
    return ret
end

M.get_namespace = function(M, nspaces)
    local last_m = M
    for i, v in ipairs(nspaces) do
        local nsp = M[v]
        if not nsp then
            nsp = {}
            last_m[v] = nsp
        end
        last_m = nsp
    end
    return last_m
end

return M
