------------------------------------------------------------------------------
--
--  LGI Helpers for custom FFI definitions
--
--  Copyright (c) 2012 Pavel Holejsovsky
--  Licensed under the MIT license:
--  http://www.opensource.org/licenses/mit-license.php
--
------------------------------------------------------------------------------

local pairs, ipairs, setmetatable, getmetatable
   = pairs, ipairs, setmetatable, getmetatable

local math = require 'math'

local core = require 'LuaGObject.core'
local gi = core.gi
local component = require 'LuaGObject.component'
local enum = require 'LuaGObject.enum'
local record = require 'LuaGObject.record'

local ffi = {}

local gobject = assert(gi.require('GObject'))
local glib = assert(gi.require('GLib'))

-- Gather all basic types.  We have to 'steal' them from well-known
-- declarations, because girepository API does not allow synthesizing
-- GIBaseInfo instances from the air.
ffi.types = {
   void = gobject.Callback.return_type,
   boolean = glib.Variant.methods.get_boolean.return_type,
   int8 = gobject.ParamSpecChar.fields.minimum.typeinfo,
   uint8 = gobject.ParamSpecUChar.fields.minimum.typeinfo,
   int16 = glib.Variant.methods.get_int16.return_type,
   uint16 = glib.Variant.methods.get_uint16.return_type,
   int32 = glib.Variant.methods.get_int32.return_type,
   uint32 = glib.Variant.methods.get_uint32.return_type,
   int64 = glib.Variant.methods.get_int64.return_type,
   uint64 = glib.Variant.methods.get_uint64.return_type,

   int = gobject.ParamSpecEnum.fields.default_value.typeinfo,
   uint = gobject.ParamSpecFlags.fields.default_value.typeinfo,
   long = gobject.ParamSpecLong.fields.default_value.typeinfo,
   ulong = gobject.ParamSpecULong.fields.default_value.typeinfo,

   float = gobject.ParamSpecFloat.fields.default_value.typeinfo,
   double = gobject.ParamSpecDouble.fields.default_value.typeinfo,

   utf8 = gobject.ParamSpecString.fields.default_value.typeinfo,
   filename = glib.file_get_contents.args[1].typeinfo,

   GType = gobject.ParamSpecGType.fields.is_a_type.typeinfo,

   GStrv = glib.KeyFile.methods.get_groups.return_type,

   GDestroyNotify = glib.Hook.fields.destroy.typeinfo,

   ptr = glib.CompareDataFunc.args[1].typeinfo,
}

for name, alias in pairs {
   char = 'int8', uchar = 'uint8', short = 'int16', ushort = 'uint16'
} do
   ffi.types[name] = ffi.types[alias]
end

-- Gets gtype from specified resolved and _get_type function name.
function ffi.load_gtype(resolver, get_type_name)
   local addr = resolver[get_type_name]
   if not addr then return nil, ("`%s' not found"):format(get_type_name) end
   local get_gtype = core.callable.new(
      { name = get_type_name, addr = addr, ret = ffi.types.GType })
   return get_gtype()
end

-- Creates new enum/flags table with all values from specified gtype.
function ffi.load_enum(gtype, name)
   local GLib, GObject = core.repo.GLib, core.repo.GObject
   local is_flags = GObject.Type.is_a(gtype, GObject.Type.FLAGS)
   local enum_component = component.create(
      gtype, is_flags and enum.bitflags_mt or enum.enum_mt, name)
   local type_class
   -- GLib >= 2.86 deprecates GObject.TypeClass.ref() in favour of .get()
   if GLib.check_version(2, 86, 0) then
      type_class = GObject.TypeClass.ref(gtype)
   else
      type_class = GObject.TypeClass.get(gtype)
   end
   local enum_class = core.record.cast(
      type_class, is_flags and GObject.FlagsClass or GObject.EnumClass)
   for i = 0, enum_class.n_values - 1 do
      local val = core.record.fromarray(enum_class.values, i)
      enum_component[core.upcase(val.value_nick):gsub('%-', '_')] = val.value
   end
   -- For GLib versions below 2.86, type_class was ref'd and needs to be unref'd
   if GLib.check_version(2, 86, 0) then
      type_class:unref()
   end
   return enum_component
end

-- Aligns offset to specified alignment.
local function align(offset, align)
   return math.modf((offset + align - 1) / align) * align
end

-- Creates record from the table of the field definitions.
function ffi.load_fields(rec, defs)
   rec._field = {}
   local offset = 0
   local max_align = 1
   local max_size = 0
   for _, def in ipairs(defs) do
      local field = {}

      -- Get size and alignment of this field.
      local size, alignment
      if gi.isinfo(def[2]) then
	 field[2] = 0
	 if def[2].tag == 'interface' then
	    local ii = def[2].interface
	    if ii.type == 'enum' or ii.type == 'flags' then
	       size, alignment = core.marshal.typeinfo(ii.typeinfo)
	    elseif ii.type == 'struct' or ii.type == 'union' then
	       size, alignment = core.marshal.typeinfo(ffi.types.ptr)
	       if not ii.is_pointer then
		  -- Alignment is tricky; ideally we should go through
		  -- the record and find alignment according to the
		  -- largest alignments of the fields, but now we just
		  -- punt and use 'ptr' as alignment.  But this might
		  -- be incorrect in case that doubles are used on 32b
		  -- platform.

		  -- Get size from the record descriptor.
		  size = core.repotype(ii)._size
	       end
	    end
	 else
	    -- Basic type.
	    size, alignment = core.marshal.typeinfo(def[2])
	 end
      else
	 -- Either record or enum, decide according to repotable.
	 local repotype = getmetatable(def[2])._type
	 if repotype == 'struct' or repotype == 'union' then
	    field[2] = 1
	    size, alignment = core.marshal.typeinfo(ffi.types.ptr)
	    if not def.ptr then
	       field[2] = 2
	       size = def[2]._size
	    end
	 elseif repotype == 'enum' or repotype == 'flags' then
	    field[2] = 3
	    field[4] = def.type or ffi.types.int
	    size, alignment = core.marshal.typeinfo(field[4])
	 end
      end

      -- Adjust offset according to the alignment.
      offset = align(offset, alignment)
      max_align = math.max(max_align, alignment)

      -- Create and add field definition.
      field[1] = offset
      field[3] = def[2]
      rec._field[def[1]] = field

      if getmetatable(rec)._type == 'union' then
	 -- Remember largest size as the size of the union.
	 max_size = math.max(max_size, align(size, alignment))
      else
	 -- Move offset after the field.
	 offset = offset + size
      end
   end

   -- Store the total size of the record.
   rec._size = ((getmetatable(rec)._type == 'union') and max_size
	     or align(offset, max_align))
end

return ffi
