-- EFL LuaJIT bindings: Eo
-- For use with Elua

local ffi = require("ffi")

ffi.cdef [[
    typedef unsigned char Eina_Bool;
    typedef struct _Eina_Iterator Eina_Iterator;

    typedef struct _Eo_Opaque Eo;
    typedef Eo Eo_Class;

    typedef short Eo_Callback_Priority;

    typedef void (*eo_key_data_free_func)(void *);

    struct _Eo_Event_Description {
        const char *name;
        const char *doc;
        Eina_Bool   unfreezable;
    };
    typedef struct _Eo_Event_Description Eo_Event_Description;

    typedef Eina_Bool (*Eo_Event_Cb)(void *data, Eo *obj,
        const Eo_Event_Description *desc, void *event_info);

    struct _Eo_Callback_Array_Item {
        const Eo_Event_Description *desc;
        Eo_Event_Cb func;
    };
    typedef struct _Eo_Callback_Array_Item Eo_Callback_Array_Item;

    Eina_Bool eo_init(void);
    Eina_Bool eo_shutdown(void);

    Eina_Bool eo_isa(const Eo *obj, const Eo_Class *klass);

    const char *eo_class_name_get(const Eo_Class *klass);

    void eo_constructor(void);
    void eo_destructor(void);

    Eo *_eo_add_internal_start(const char *file, int line,
        const Eo_Class *klass_id, Eo *parent, Eina_Bool ref);

    Eina_Bool _eo_do_start(const Eo *obj, const Eo_Class *cur_klass,
        Eina_Bool is_super, const char *file, const char *func, int line);
    void      _eo_do_end  (const Eo **ojb);

    const Eo_Class *eo_class_get(const Eo *obj);

    void *eo_data_xref_internal(const char *file, int line, const Eo *obj,
        const Eo_Class *klass, const Eo *ref_obj);
    void eo_data_xunref_internal(const Eo *obj, void *data, const Eo *ref_obj);
    Eo *eo_xref_internal(const char *file, int line, Eo *obj, const Eo *ref_obj);
    void eo_xunref(Eo *obj, const Eo *ref_obj);
    Eo *eo_ref(const Eo *obj);
    void eo_unref(const Eo *obj);
    int eo_ref_get(const Eo *obj);
    void eo_wref_add(Eo **wref);
    void eo_wref_del(Eo **wref);

    void eo_del(const Eo *obj);

    void eo_manual_free_set(Eo *obj, Eina_Bool manual_free);
    Eina_Bool eo_manual_free(Eo *obj);
    Eina_Bool eo_destructed_is(const Eo *obj);

    Eina_Bool eo_composite_attach(Eo *comp_obj, Eo *parent);
    Eina_Bool eo_composite_detach(Eo *comp_obj, Eo *parent);
    Eina_Bool eo_composite_is(const Eo *comp_obj);

    void eo_parent_set(Eo *parent);
    Eo  *eo_parent_get(void);

    void eo_event_freeze(void);
    void eo_event_thaw(void);
    int eo_event_freeze_count_get(void);
    void eo_event_global_freeze(void);
    void eo_event_global_thaw(void);
    int eo_event_global_freeze_count_get(void);

    Eina_Bool eo_finalized_get(void);
    Eo *eo_finalize(void);

    void eo_event_callback_forwarder_add(const Eo_Event_Description *desc,
        Eo *new_obj);
    void eo_event_callback_forwarder_del(const Eo_Event_Description *desc,
        Eo *new_obj);
    void  eo_event_callback_priority_add(const Eo_Event_Description *desc,
        Eo_Callback_Priority priority, Eo_Event_Cb cb, const void *data);
    void eo_event_callback_del(const Eo_Event_Description *desc,
        Eo_Event_Cb func, const void *user_data);
    void eo_event_callback_array_priority_add(const Eo_Callback_Array_Item *array,
        Eo_Callback_Priority priority, const void *data);
    void eo_event_callback_array_del(const Eo_Callback_Array_Item *array,
        const void *user_data);
    Eina_Bool eo_event_callback_call(const Eo_Event_Description *desc,
        void *event_info);

    void eo_key_data_set(const char *key, const void *data,
        eo_key_data_free_func free_func);
    void *eo_key_data_get(const char *key);
    void  eo_key_data_del(const char *key);

    Eina_Iterator *eo_children_iterator_new(void);

    const Eo_Class *eo_base_class_get(void);

    extern const Eo_Event_Description _EO_BASE_EVENT_DEL;
]]

local addr_d = ffi.typeof("union { double d; const Eo *p; }")
local eo_obj_addr_get = function(x)
    local v = addr_d()
    v.p = x
    return tonumber(v.d)
end

local cutil = require("cutil")
local util  = require("util")

local M = {}

local eo

local classes = {}
local eo_classes = {}

-- event system

local eo_callbacks, eo_cbidcache = {}, {}

local eo_event_del, eo_event_cb

local eo_event_del_fun = function(data, obj, desc, einfo)
    local addr = eo_obj_addr_get(obj)
    if  eo_callbacks[addr] then
        eo_callbacks[addr], eo_cbidcache[addr] = nil, nil
    end
end

local eo_event_cb_fun = function(data, obj, desc, einfo)
    local  addr = eo_obj_addr_get(obj)
    local  cbs  = eo_callbacks[addr]
    assert(cbs)
    local cidx = tonumber(ffi.cast("intptr_t", data))
    local fun  = cbs[cidx]
    assert(fun)
    return fun() ~= false
end

local connect = function(self, ename, func, priority)
    local ev = self.__events[ename]
    if not ev then
        return false, "attempt to connect an invalid event '" .. ename .. "'"
    end
    local cl = eo_classes["Eo_Base"]
    -- add the callback to the respective array
    local cdel = false
    local addr = eo_obj_addr_get(self)
    local  cbs = eo_callbacks[addr]
    local  cbi = eo_cbidcache[addr]
    if not cbs then
        cbs, cbi = {}, {}
        eo_callbacks[addr], eo_cbidcache[addr] = cbs, cbi
        cdel = true
    end
    local cidx = #cbs + 1
    cbs[cidx], cbi[func] = func, cidx
    M.__do_start(self, cl)
    eo.eo_event_callback_priority_add(ev, priority or 0,
        eo_event_cb, ffi.cast("void *", cidx))
    eo.eo_event_callback_priority_add(eo._EO_BASE_EVENT_DEL, 0, eo_event_del,
        nil)
    M.__do_end()
    return true
end

local disconnect = function(self, ename, func)
    local ev = self.__events[ename]
    if not ev then
        return false, "attempt to disconnect an invalid event '" .. ename .. "'"
    end
    local cl = eo_classes["Eo_Base"]
    -- like connect, but the other way around
    local addr = eo_obj_addr_get(self)
    local  cbs = eo_callbacks[addr]
    if not cbs then
        return false
    end
    local  cbi = eo_cbidcache[addr]
    assert(cbi)
    local cidx = cbi[func]
    if not cidx then
        return false
    end
    cbs[cidx] = nil
    cbi[func] = nil
    M.__do_start(self, cl)
    eo.eo_event_callback_del(ev, eo_event_cb, ffi.cast("void *", cidx))
    M.__do_end()
    return true
end

local init = function()
    eo = util.lib_load("eo")
    eo.eo_init()
    local eocl = eo.eo_base_class_get()
    local addr = eo_obj_addr_get(eocl)
    classes["Eo_Base"] = util.Object:clone {
        connect = connect,
        disconnect = disconnect,
        __events = util.Object:clone {},
        __properties = util.Object:clone {}
    }
    classes[addr] = classes["Eo_Base"]
    eo_classes["Eo_Base"] = eocl
    eo_classes[addr] = eocl
    eo_event_del = ffi.cast("Eo_Event_Cb", eo_event_del_fun)
    eo_event_cb  = ffi.cast("Eo_Event_Cb", eo_event_cb_fun)
end

local shutdown = function()
    classes, eo_classes = {}, {}
    eo.eo_shutdown()
    eo_event_del:free()
    eo_event_cb:free()
    eo_event_del = nil
    eo_event_cb  = nil
    eo_callbacks = {}
    eo_cbidcache = {}
    util.lib_unload("eo")
end

local getinfo = debug.getinfo

local getfuncname = function(info)
    return info.name or "<" .. tostring(info.func) .. ">"
end

M.class_get = function(name)
    return classes[name]
end

M.eo_class_get = function(name)
    return eo_classes[name]
end

local inherit_meta = function(body, field, parents, mixins)
    local o = body[field]
    if o then
        o = parents[1][field]:clone(o)
        for i = 2, #parents do o:add_parent(parents[i][field]) end
        for i = 1, #mixins  do o:add_mixin (mixins [i][field]) end
        body[field] = o
    end
end

M.class_register = function(name, parents, mixins, body, eocl)
    -- map given names to objects
    local pars = {}
    for i = 1, #parents do pars[i] = classes[parents[i]] end
    -- for mixins, we need to check if it hasn't already been mixed in
    -- in some parent (doesn't matter how deep down the tree), because
    -- if it has, we need to skip it (for proper inheritance lookup order)
    local mins = {}
    local midx = 1
    for i = 1, #mixins do
        local mixin = mixins[i]
        local ck, hasmi = "__mixin_" .. mixin, false
        if mixin[ck] then
            for i = 1, #pars do
                if pars[i][ck] then
                    hasmi = true
                    break
                end
            end
        end
        if not hasmi then
            mins[midx] = mixin
            midx       = midx + 1
        end
    end

    inherit_meta(body, "__events"    , pars, mins)
    inherit_meta(body, "__properties", pars, mins)

    local lcl = pars[1]:clone(body)
    for i = 2, #pars do lcl:add_parent(pars[i]) end
    for i = 1, #mins do lcl:add_mixin (mins[i]) end

    local addr = eo_obj_addr_get(eocl)
    classes   [name], classes   [addr] = lcl , lcl
    eo_classes[name], eo_classes[addr] = eocl, eocl
end

M.class_unregister = function(name)
    local addr = eo_obj_addr_get(eo_classes[name])
    classes   [name], classes   [addr] = nil
    eo_classes[name], eo_classes[addr] = nil
end

local obj_gccb = function(obj)
    eo.eo_unref(obj)
end

M.__ctor_common = function(klass, parent, ctor, loff, ...)
    local info   = getinfo(2 + (loff or 0), "nlSf")
    local source = info.source
    local func   = getfuncname(info)
    local line   = info.currentline
    local ret    = eo._eo_add_internal_start(source, line, klass, parent, true)
    local retval
    if eo._eo_do_start(ret, nil, false, source, func, line) ~= 0 then
        eo.eo_constructor()
        if ctor then ctor(ret, ...) end
        ret = eo.eo_finalize()
        eo._eo_do_end(nil)
    end
    ffi.gc(ret, obj_gccb)
    return ret
end

M.__do_start = function(self, klass)
    if eo.eo_isa(self, klass) == 0 then
        error("method call on an invalid object", 3)
    end
    local info = getinfo(3, "nlSf")
    return eo._eo_do_start(self, nil, false, info.source,
        getfuncname(info), info.currentline) ~= 0
end

M.__do_end = function()
    eo._eo_do_end(nil) -- the parameter is unused and originally there
                       -- only for cleanup (dtor)
end

local get_obj_mt = function(obj)
    local cl = eo.eo_class_get(obj)
    if cl == nil then return nil end
    return classes[eo_obj_addr_get(cl)]
end

local prop_proxy_meta = {
    __index = function(self, key)
        local ngkeys = self.ngkeys
        if ngkeys <= 0 then
            error("property '" .. self.key .. "' has no getter keys", 2)
        end
        if ngkeys > 1 and type(key) == "table" then
            if self.ngvals > 1 then
                return { self(unpack(key)) }
            else
                return self(unpack(key))
            end
        else
            if self.ngvals > 1 then
                return { self(key) }
            else
                return self(key)
            end
        end
    end,

    __newindex = function(self, key, val)
        local nskeys = self.nskeys
        if nskeys <= 0 then
            error("property '" .. self.key .. "' has no setter keys", 2)
        end
        if nskeys > 1 then
            -- ultra slow path, q66 failed optimizing this
            -- if you ever get to touch this, increment this
            -- counter to let others know you failed too.
            --
            -- failures: 1
            --
            -- fortunately this one is not very commonly used.
            local atbl
            if type(key) == "table" then
                atbl = { unpack(key) }
            else
                atbl = { key }
            end
            if self.nsvals > 1 and type(val) == "table" then
                for i, v in ipairs(val) do atbl[nskeys + i] = v end
            else
                atbl[nskeys + 1] = val
            end
            self.mt[self.key .. "_set"](self.obj, unpack(atbl))
        else
            if self.nsvals > 1 and type(val) == "table" then
                -- somewhat less slow but still slow path
                self.mt[self.key .. "_set"](self.obj, key, unpack(val))
            else
                -- least slow, no unpacks, no temporaries, just proxy
                self.mt[self.key .. "_set"](self.obj, key, val)
            end
        end
    end,

    -- provides alt syntax for getters with keys
    __call = function(self, ...)
        return self.mt[self.key .. "_get"](self.obj, ...)
    end,

    -- locks out the proxy
    __metatable = false
}

-- each __properties field looks like this:
--
-- { NUM_GET_KEYS, NUM_SET_KEYS, NUM_GET_VALS, NUM_SET_VALS, GETTABLE, SETTABLE }
--
-- the last two are booleans (determining if the property can be get and set).
ffi.metatype("Eo", {
    -- handles property getting with no keys and also properties with keys
    __index = function(self, key)
        local mt = get_obj_mt(self)
        if mt == nil then return nil end
        local pt = mt.__properties
        local pp = pt[key]
        if not pp then
            return mt[key]
        end
        local ngkeys, nskeys = pp[1], pp[2]
        if ngkeys ~= 0 or nskeys ~= 0 then
            -- proxy - slow path, but no way around it
            -- basically the proxy is needed because we want nice syntax and
            -- lua can't do it by default. so we help ourselves a bit with this
            return setmetatable({ ngkeys = ngkeys, nskeys = nskeys,
                ngvals = pp[3], nsvals = pp[4], obj = self, key = key,
                mt = mt }, prop_proxy_meta)
        end
        if not pp[5] then
            error("property '" .. key .. "' is not gettable", 2)
        end
        if pp[3] > 1 then
            return { mt[key .. "_get"](self) }
        else
            return mt[key .. "_get"](self)
        end
    end,

    -- handles property setting with no keys
    __newindex = function(self, key, val)
        local mt = get_obj_mt(self)
        if mt == nil then return nil end
        local pt = mt.__properties
        local pp = pt[key]
        if not pp then
            error("no such property '" .. key .. "'", 2)
        end
        if not pp[6] then
            error("property '" .. key .. "' is not settable", 2)
        end
        local nkeys = pp[2]
        if nkeys ~= 0 then
            error("property '" .. key .. "' requires " .. nkeys .. " keys", 2)
        end
        local nvals = pp[4]
        if nvals > 1 and type(val) == "table" then
            mt[key .. "_set"](self, unpack(val))
        else
            mt[key .. "_set"](self, val)
        end
    end
})

cutil.init_module(init, shutdown)

return M
