get_offset = 0x80

-- allows descriptor to use shorthand without stringifying
void = 'ii_void'
s8 = 'ii_s8'
u8 = 'ii_u8'
s16 = 'ii_s16'
s16V = 'ii_s16V'
s16ms = 'ii_s16ms'
u16 = 'ii_u16'
float = 'ii_float'
s32T = 'ii_s32T'

header = [[
// THIS FILE IS AUTOGENERATED //
// DO NOT EDIT THIS MANUALLY //

#pragma once

]]

types_and_commands = [[
typedef enum{ ii_void
            , ii_u8
            , ii_s8
            , ii_u16
            , ii_s16
            , ii_s16V
            , ii_s16ms
            , ii_float   // 32bit (for crow to crow comm'n)
            , ii_s32T
} ii_Type_t;

typedef struct{
    uint8_t cmd;
    uint8_t args;
    ii_Type_t return_type;
    ii_Type_t argtype[];
} ii_Cmd_t;

]]

function make_help(files)
    local c = 'const char* ii_module_list =\n'
           .. '\"--- ii: supported modules\\n\\r"\n'
    for _,f in ipairs(files) do
        c = c .. '"' .. f.lua_name .. '\\t-- ' .. f.module_name .. '\\n\\r"\n'
    end
    return c .. '"\\n\\r"\n'
             .. '"--- See a module\'s commands with \'ii.<module>.help()\'\\n\\r"\n'
             .. '"ii.jf.help()\\n\\r"\n'
             .. ';\n\n'
end

function make_commandlist(files)
    local function c_cmds(f)
        local c = ''

        local function make_a_cmd( lua_name, ix, cmd, args, retval )
            local s = 'const ii_Cmd_t ' .. lua_name .. ix .. ' = {' .. cmd .. ','
            if args == nil then
                s = s .. '0,' .. retval .. ',{}'
            elseif type(args[1]) == 'table' then -- >1 arg
                s = s .. #args .. ',' .. retval .. ',{'
                for i=1,#args-1 do
                    s = s .. args[i][2] .. ','
                end
                s = s .. args[#args][2] .. '}'
            elseif type(args[1]) == 'string' then -- 1 arg
                s = s .. '1,' .. retval .. ',{' .. args[2] .. '}'
            end
            return s .. '};\n'
        end

        local i = 0
        -- setters
        if f.commands then
            for _,v in ipairs( f.commands ) do
                c = c .. make_a_cmd( f.lua_name, i, v.cmd, v.args, void )
                i = i + 1
            end
        end

        local function get_last_argtype( args )
            if type(args[1]) == 'table' then -- >1 arg
                return args[#args][2]
            elseif type(args[1]) == 'string' then -- 1 arg
                return args[2]
            end
            return void
        end

        local function drop_last_arg( args )
            if type(args[1]) == 'table' then -- >1 arg
                local r = {}
                for i=1,#args-1 do
                    table.insert( r, args[i] )
                end
                return r
            else -- 1 or none
                return
            end
        end

        -- auto getters
        if f.commands then
            for _,v in ipairs( f.commands ) do
                if v.get == true then
                    c = c .. make_a_cmd( f.lua_name
                                       , i
                                       , v.cmd + get_offset
                                       , drop_last_arg( v.args )
                                       , get_last_argtype( v.args )
                                       )
                    i = i + 1
                end
            end
        end
        -- explicit getters
        if f.getters ~= nil then
            for _,v in ipairs( f.getters ) do
                c = c .. make_a_cmd( f.lua_name
                                   , i
                                   , v.cmd
                                   , v.args
                                   , v.retval[2]
                                   )
                i = i + 1
            end
        end
        return c
    end

    local c = ''
    for _,f in ipairs(files) do
        c = c .. c_cmds(f)
    end
    return c .. '\n'
end

function build_cases(address, name, indent)
    local s = ''
    if type(address) == 'table' then
        for k,v in ipairs(address) do
            s = s .. indent .. 'case ' .. v .. ':'
            if k==#address then s = s .. '{' end
            if k==1 then s = s .. ' // '.. name end
            s = s .. '\n'
        end
    else
        s = s .. indent .. 'case ' .. address .. ':{ // '.. name..'\n'
    end
    return s
end


function c_switch(files)
    local s = 'const ii_Cmd_t* ii_find_command( uint8_t address, uint8_t cmd ){\n'
           .. '\tswitch( address ){\n'
    for _,f in ipairs(files) do

        s = s .. build_cases(f.i2c_address, f.module_name, '\t\t')

        s = s .. '\t\t\tswitch( cmd ){\n'
        local ix = 0
        if f.commands then
            for _,v in ipairs( f.commands ) do
                -- setters
                s = s .. '\t\t\t\tcase ' .. v.cmd .. ': return &'
                      .. f.lua_name .. ix .. ';\n'
                ix = ix + 1
            end
            for _,v in ipairs( f.commands ) do
                if v.get == true then
                    -- implicit getter
                    s = s .. '\t\t\t\tcase ' .. (v.cmd + get_offset) .. ': return &'
                          .. f.lua_name .. ix .. ';\n'
                    ix = ix + 1
                end
            end
        end
        if f.getters ~= nil then
            for _,v in ipairs( f.getters ) do
                s = s .. '\t\t\t\tcase ' .. v.cmd .. ': return &'
                      .. f.lua_name .. ix .. ';\n'
                ix = ix + 1
            end
        end
        s = s .. '\t\t\t\tdefault: return NULL; // unknown command\n'
              .. '\t\t\t}\n'
              .. '\t\t}\n'
    end
    s = s .. '\t\tdefault: return NULL; // unknown address\n'
          .. '\t}\n'
          .. '}\n\n'
    return s
end

-- generate a printable c-string describing the module's commands
function generate_prototypes( d )
    if not d.commands then return '' end
    local prototypes = '"-- commands\\n\\r"\n'
    local proto_prefix = 'ii.' .. d.lua_name .. '.'
    for _,v in ipairs( d.commands ) do
        local s = '"' .. proto_prefix .. v.name .. '( '
        if not v.args then -- no args
            -- do nothing
        elseif type(v.args[1]) == 'table' then -- more than 1 arg
            local arg_count = #(v.args)
            for i=1,arg_count do
                s = s .. v.args[i][1]
                if i ~= arg_count then s = s .. ', ' end
            end
        else -- only 1 arg
            s = s .. v.args[1]
        end
        s = s .. ' )\\n\\r"\n'
        prototypes = prototypes .. s
    end
    return prototypes
end

function generate_getters( d )
    local getters = ''
    local get_prefix = '"ii.' .. d.lua_name .. '.get( '

    -- commands that have standard getters
    if d.commands then
        for _,v in ipairs( d.commands ) do
            if v.get == true then
                local s = get_prefix .. '\'' .. v.name .. '\''
                if v.args ~= nil then
                    if type(v.args[1]) == 'table' then -- >1 arg
                        for i=1,#(v.args)-1 do -- ignore last
                            s = s .. ', ' .. v.args[i][1]
                        end
                    end -- if only 1 arg it is ignored!
                end
                s = s .. ' )\\n\\r"\n'
                getters = getters .. s
            end
        end
    end

    -- get-only commands, or custom getters
    if d.getters ~= nil then
        for _,v in ipairs( d.getters ) do
            local s = get_prefix
            if type(v.name) == 'number' then
                s = s .. v.name
            else
                s = s .. '\'' .. v.name .. '\''
            end
            if v.args ~= nil then
                if type(v.args[1]) == 'table' then -- multiple args
                    for i=1,#(v.args)-1 do -- ignore last
                        s = s .. ', ' .. v.args[i][1]
                    end
                else s = s .. ', ' .. v.args[1] end
            end
            s = s .. ' )\\n\\r"\n'
            getters = getters .. s
        end
    end
    return getters
end

function generate_events( d )
    local has_get_cmd = function(d)
        if not d.commands then return false end
        for _,v in ipairs( d.commands ) do
            if v.get == true then return true end
        end
        return false
    end

    local events = '"ii.' .. d.lua_name .. '.event = function( e, value )\\n\\r"\n'

    local overloaded = has_get_cmd(d)
    if overloaded then
        local heading = true
        for _,v in ipairs( d.commands ) do
            if v.get == true then
                local vn = type(v.name) == 'number' and v.name or '\'' .. v.name .. '\''
                if heading then
                    events = events .. '"\tif e.name == ' .. vn .. ' then\\n\\r"\n'
                           .. '"\t\t-- handle ' .. v.name .. ' response\\n\\r"\n'
                           .. '"\t\t\t-- e.arg: first argument, ie channel\\n\\r"\n'
                           .. '"\t\t\t-- e.device: index of device\\n\\r"\n'
                    heading = false
                else
                    events = events .. '"\telseif e.name == ' .. vn .. ' then\\n\\r"\n'
                end
            end
        end
    else
        local gn = type(d.getters[1].name) == 'number' and d.getters[1].name
                                                       or '\'' .. d.getters[1].name .. '\''
        events = events .. '"\tif e.name == ' .. gn .. ' then\\n\\r"\n'
               .. '"\t\t-- handle ' .. d.getters[1].name .. ' response\\n\\r"\n'
               .. '"\t\t\t-- e.arg: first argument, ie channel\\n\\r"\n'
               .. '"\t\t\t-- e.device: index of device\\n\\r"\n'
    end

    if d.getters ~= nil then
        for i=(overloaded and 1 or 2),#(d.getters) do
            local gn = type(d.getters[i].name) == 'number' and d.getters[i].name
                                                           or '\'' .. d.getters[i].name .. '\''
            events = events .. '"\telseif e.name == ' .. gn .. ' then\\n\\r"\n'
        end
    end
    events = events .. '"\tend\\n\\r"\n'
                    .. '"end\\n\\r"\n'
    return events
end

function make_helpstrings(files)

    local function make_mhelp(f)
        local function has_getters( d )
            if d.getters ~= nil then return true
            else
                for _,v in ipairs( d.commands ) do
                    if v.get == true then return true end
                end
            end
            return false
        end

        local h = generate_prototypes(f) .. '"\\n\\r"\n'
        if has_getters(f) then
            h = h .. '"-- request params\\n\\r"\n'
                  .. generate_getters(f) .. '"\\n\\r"\n'
                  .. '"-- then receive\\n\\r"\n'
                  .. generate_events(f)
        end
        return h
    end

    local c = ''
    for _,f in ipairs(files) do
        c = c .. 'const char* ' .. f.lua_name .. '_help=\n' .. make_mhelp(f) .. ';\n\n'
    end
    return c
end

function make_helpget(files)
    local c = 'const char* ii_list_commands( uint8_t address )\n'
           .. '{\n'
           .. '\tswitch( address ){\n'
    for _,f in ipairs(files) do
        local ii = f.i2c_address
        if type(ii) == 'table' then ii = ii[1] end
        c = c .. '\t\tcase ' .. ii .. ': return ' .. f.lua_name .. '_help;\n'
    end
    return c .. '\t\tdefault: return "module not found\\n\\r";\n'
             .. '\t}\n'
             .. '}\n\n'
end

function c_pickle(files)
    local s = 'static void ii_pickle( uint8_t* address\n'
           .. '                     , uint8_t* data\n'
           .. '                     , uint8_t* byte_count\n'
           .. '                     ){\n'
           .. '\tswitch( *address ){\n'
    for _,f in ipairs(files) do
        if f.pickle then
            s = s .. build_cases(f.i2c_address, f.module_name, '\t\t')
                  .. '\t\t\t' .. string.gsub(f.pickle,"\n","\n\t\t\t")
                  .. '\n\t\t\tbreak;}\n'
        end
    end
    s = s .. '\t\tdefault: return; // no custom pickler\n'
          .. '\t}\n'
          .. '}\n\n'
    return s
end

function c_unpickle(files)
    local s = 'static void ii_unpickle( uint8_t* address\n'
           .. '                       , uint8_t* command\n'
           .. '                       , uint8_t* data\n'
           .. '                       ){\n'
           .. '\tswitch( *address ){\n'
    for _,f in ipairs(files) do
        if f.unpickle then
            s = s .. build_cases(f.i2c_address, f.module_name, '\t\t')
                  .. '\t\t\t' .. string.gsub(f.unpickle,"\n","\n\t\t\t")
                  .. '\n\t\t\tbreak;}\n'
        end
    end
    s = s .. '\t\tdefault: return; // no custom pickler\n'
          .. '\t}\n'
          .. '}\n\n'
    return s
end

function make_c(files)
    return header
        .. types_and_commands
        .. make_help(files)
        .. make_commandlist(files)
        .. c_switch(files)
        .. make_helpstrings(files)
        .. make_helpget(files)
        .. c_pickle(files)
        .. c_unpickle(files)
end

local in_file_dir = arg[1]
local out_file = arg[2]

do
    local dir = io.popen('/bin/ls ' .. in_file_dir)
    local files = {}
    for filename in dir:lines() do
        table.insert(files, dofile('lua/ii/' .. filename))
    end

    local c = io.open( out_file, 'w' )
    c:write(make_c(files))
    c:close()
end

-- example usage:
-- lua util/ii_c_layer.lua lua/ii/ build/ii_c_layer.h
