/*
    See license.txt in the root of this project.
*/

# include "luametatex.h"

/*tex

    These are the supported callbacks (by name). This list must have the same size and order as the
    array in |lmtcallbacklib.h|! The structure and capabilities evolved a bit over time. There are 
    differences between \LUATEX\ and \LUAMETATEX\ in callbacks with the same name. 

*/

callback_state_info lmt_callback_state = {
    .index   = 0,
    .options = 0,
    .items   = {
        { .value = 0, .state = 0,                          .name = ""                     }, /*tex empty on purpose */
        { .value = 0, .state = 0,                          .name = "test_only"            },
        { .value = 0, .state = callback_state_fundamental, .name = "find_log_file"        },
        { .value = 0, .state = callback_state_fundamental, .name = "find_format_file"     },
        { .value = 0, .state = callback_state_fundamental, .name = "open_data_file"       },
        { .value = 0, .state = callback_state_fundamental, .name = "process_jobname"      },
        { .value = 0, .state = callback_state_fundamental, .name = "start_run"            },
        { .value = 0, .state = callback_state_fundamental, .name = "stop_run"             },
        { .value = 0, .state = callback_state_fundamental, .name = "define_font"          },
        { .value = 0, .state = callback_state_selective,   .name = "quality_font"         },
        { .value = 0, .state = callback_state_selective,   .name = "pre_output"           },
        { .value = 0, .state = 0,                          .name = "buildpage"            },
        { .value = 0, .state = callback_state_selective,   .name = "hpack"                },
        { .value = 0, .state = callback_state_selective,   .name = "vpack"                },
        { .value = 0, .state = callback_state_selective,   .name = "hyphenate"            },
        { .value = 0, .state = callback_state_selective,   .name = "ligaturing"           },
        { .value = 0, .state = callback_state_selective,   .name = "kerning"              },
        { .value = 0, .state = callback_state_selective,   .name = "glyph_run"            },
        { .value = 0, .state = 0,                          .name = "pre_linebreak"        },
        { .value = 0, .state = callback_state_selective,   .name = "linebreak"            },
        { .value = 0, .state = 0,                          .name = "post_linebreak"       },
        { .value = 0, .state = callback_state_selective,   .name = "append_to_vlist"      },
        { .value = 0, .state = callback_state_selective,   .name = "alignment"            },
        { .value = 0, .state = callback_state_selective,   .name = "local_box"            },
        { .value = 0, .state = callback_state_selective,   .name = "packed_vbox"          },
        { .value = 0, .state = callback_state_selective,   .name = "mlist_to_hlist"       },
        { .value = 0, .state = callback_state_fundamental, .name = "pre_dump"             },
        { .value = 0, .state = callback_state_fundamental, .name = "start_file"           },
        { .value = 0, .state = callback_state_fundamental, .name = "stop_file"            },
        { .value = 0, .state = callback_state_tracing,     .name = "intercept_tex_error"  },
        { .value = 0, .state = callback_state_tracing,     .name = "intercept_lua_error"  },
        { .value = 0, .state = callback_state_tracing,     .name = "show_error_message"   },
        { .value = 0, .state = callback_state_tracing,     .name = "show_warning_message" },
        { .value = 0, .state = callback_state_tracing,     .name = "hpack_quality"        },
        { .value = 0, .state = callback_state_tracing,     .name = "vpack_quality"        },
        { .value = 0, .state = callback_state_tracing,     .name = "linebreak_check"      },
        { .value = 0, .state = callback_state_tracing,     .name = "balance_check"        },
        { .value = 0, .state = callback_state_tracing,     .name = "show_vsplit"          },
        { .value = 0, .state = callback_state_tracing,     .name = "show_build"           },
        { .value = 0, .state = 0,                          .name = "insert_par"           },
        { .value = 0, .state = callback_state_selective,   .name = "append_adjust"        },
        { .value = 0, .state = callback_state_selective,   .name = "append_migrate"       },
        { .value = 0, .state = callback_state_selective,   .name = "append_line"          },
     /* { .value = 0, .state = callback_state_selective,   .name = "pre_line"             }, */
        { .value = 0, .state = 0,                          .name = "insert_distance"      },
     /* { .value = 0, .state = 0,                          .name = "fire_up_output"       }, */
        { .value = 0, .state = callback_state_fundamental, .name = "wrapup_run"           },
        { .value = 0, .state = 0,                          .name = "begin_paragraph"      },
        { .value = 0, .state = 0,                          .name = "paragraph_context"    },
     /* { .value = 0, .state = 0,                          .name = "get_math_char"        }, */
        { .value = 0, .state = 0,                          .name = "math_rule"            },
        { .value = 0, .state = 0,                          .name = "make_extensible"      },
        { .value = 0, .state = 0,                          .name = "register_extensible"  },
        { .value = 0, .state = callback_state_tracing,     .name = "show_whatsit"         },
        { .value = 0, .state = callback_state_tracing,     .name = "get_attribute"        },
        { .value = 0, .state = callback_state_tracing,     .name = "get_noad_class"       },
        { .value = 0, .state = callback_state_tracing,     .name = "get_math_dictionary"  },
        { .value = 0, .state = callback_state_tracing,     .name = "show_lua_call"        },
        { .value = 0, .state = callback_state_tracing,     .name = "trace_memory"         },
        { .value = 0, .state = callback_state_tracing,     .name = "handle_overload"      },
        { .value = 0, .state = callback_state_tracing,     .name = "missing_character"    },
        { .value = 0, .state = callback_state_selective,   .name = "process_character"    },
        { .value = 0, .state = callback_state_tracing,     .name = "linebreak_quality"    },
        { .value = 0, .state = callback_state_selective,   .name = "paragraph_pass"       },
        { .value = 0, .state = callback_state_selective,   .name = "handle_uleader"       },
        { .value = 0, .state = callback_state_selective,   .name = "handle_uinsert"       },
        { .value = 0, .state = 0,                          .name = "italic_correction"    },
        { .value = 0, .state = callback_state_tracing,     .name = "show_loners"          },
        { .value = 0, .state = callback_state_selective,   .name = "tail_append"          },
        { .value = 0, .state = 0,                          .name = "balance_boundary"     },
        { .value = 0, .state = 0,                          .name = "balance_insert"       },
    } 
};

/*tex

    This is the generic callback handler, inspired by the one described in the \LUA\ manual(s). It
    got adapted over time and can also handle some userdata arguments.

*/

static int callbacklib_aux_run(lua_State *L, int id, int special, const char *values, va_list vl, int top, int base)
{
    int narg = 0;
    int nres = 0;
    if (special == 2) {
        /*tex copy the enclosing table */
        lua_pushvalue(L, -2);
    }
    for (narg = 0; *values; narg++) {
        switch (*values++) {
            case callback_boolean_key:
                /*tex A boolean: */
                lua_pushboolean(L, va_arg(vl, int));
                break;
            case callback_charnum_key:
                /*tex A (8 bit) character: */
                {
                    char cs = (char) va_arg(vl, int);
                    lua_pushlstring(L, &cs, 1);
                }
                break;
            case callback_integer_key:
                /*tex An integer: */
                lua_pushinteger(L, va_arg(vl, int));
                break;
            case callback_line_key:
                /*tex A buffer section, with implied start: */
                lua_pushlstring(L, (char *) (lmt_fileio_state.io_buffer + lmt_fileio_state.io_first), (size_t) va_arg(vl, int));
                break;
            case callback_strnumber_key:
                /*tex A \TEX\ string (indicated by an index): */
                {
                    size_t len;
                    const char *s = tex_makeclstring(va_arg(vl, int), &len);
                    lua_pushlstring(L, s, len);
                }
                break;
            case callback_lstring_key:
                /*tex A \LUA\ string: */
                {
                    lstring *lstr = va_arg(vl, lstring *);
                    lua_pushlstring(L, (const char *) lstr->s, lstr->l);
                }
                break;
            case callback_node_key:
                /*tex A \TEX\ node: */
             // lmt_push_node_fast(L, va_arg(vl, int));
                lmt_push_node_to_callback(L, va_arg(vl, int));
                break;
            case callback_string_key:
                /*tex A \CCODE\ string: */
                lua_pushstring(L, va_arg(vl, char *));
                break;
            case '-':
                narg--;
                break;
            case '>':
                goto ENDARGS;
            default:
                ;
        }
    }
  ENDARGS:
    nres = (int) strlen(values);
    if (special == 1) {
        nres++;
    } else if (special == 2) {
        narg++;
    }
    lmt_lua_state.saved_callback_count++;
    {
        int i = lua_pcall(L, narg, nres, base);
        if (i) {
            /*tex
                We can't be more precise here as it could be called before \TEX\ initialization is
                complete.
            */
            lua_remove(L, top + 2);
            lmt_error(L, "run callback", id, (i == LUA_ERRRUN ? 0 : 1));
            lua_settop(L, top);
            return 0;
        }
    }
    if (nres == 0) {
        return 1;
    }
    nres = -nres;
    while (*values) {
        int t = lua_type(L, nres);
        switch (*values++) {
            case callback_boolean_key:
                switch (t) {
                    case LUA_TBOOLEAN:
                        *va_arg(vl, int *) = lua_toboolean(L, nres);
                        break;
                    case LUA_TNIL:
                        *va_arg(vl, int *) = 0;
                        break;
                    default:
                        return tex_formatted_error("callback", "boolean or nil expected, false or nil, not: %s\n", lua_typename(L, t));
                }
                break;
            /*
            case callback_charnum_key:
                break;
            */
            case callback_integer_key:
                switch (t) {
                    case LUA_TNUMBER:
                        *va_arg(vl, int *) = lmt_tointeger(L, nres);
                        break;
                    default:
                        return tex_formatted_error("callback", "number expected, not: %s\n", lua_typename(L, t));
                }
                break;
            case callback_line_key:
                switch (t) {
                    case LUA_TSTRING:
                        {
                            size_t len;
                            const char *s = lua_tolstring(L, nres, &len);
                            if (s && (len > 0)) {
                                int *bufloc = va_arg(vl, int *);
                                int ret = *bufloc;
                                if (tex_room_in_buffer(ret + (int) len)) {
                                    strncpy((char *) (lmt_fileio_state.io_buffer + ret), s, len);
                                    *bufloc += (int) len;
                                    /* while (len--) {  fileio_state.io_buffer[(*bufloc)++] = *s++; } */
                                    while ((*bufloc) - 1 > ret && lmt_fileio_state.io_buffer[(*bufloc) - 1] == ' ') {
                                        (*bufloc)--;
                                    }
                               } else {
                                    return 0;
                               }
                            }
                            /*tex We can assume no more arguments! */
                        }
                        break;
                    case LUA_TNIL:
                        /*tex We assume no more arguments! */
                        return 0;
                    default:
                        return tex_formatted_error("callback", "string or nil expected, not: %s\n", lua_typename(L, t));
                }
                break;
            case callback_strnumber_key:
                switch (t) {
                    case LUA_TSTRING:
                        {
                            size_t len;
                            const char *s = lua_tolstring(L, nres, &len);
                            if (s) {
                                *va_arg(vl, int *) = tex_maketexlstring(s, len);
                            } else {
                                /*tex |len| can be zero */
                                *va_arg(vl, int *) = 0;
                            }
                        }
                        break;
                    default:
                        return tex_formatted_error("callback", "string expected, not: %s\n", lua_typename(L, t));
               }
                break;
            case callback_lstring_key:
                switch (t) {
                    case LUA_TSTRING:
                        {
                            size_t len;
                            const char *s = lua_tolstring(L, nres, &len);
                            if (s && len > 0) {
                                lstring *lsret = lmt_memory_malloc(sizeof(lstring));
                                if (lsret) {
                                    lsret->s = lmt_memory_malloc((unsigned) (len + 1));
                                    if (lsret->s) {
                                        (void) memcpy(lsret->s, s, (len + 1));
                                        lsret->l = len;
                                        *va_arg(vl, lstring **) = lsret;
                                    } else {
                                        *va_arg(vl, int *) = 0;
                                    }
                                } else {
                                    *va_arg(vl, int *) = 0;
                                }
                            } else {
                                /*tex |len| can be zero */
                                *va_arg(vl, int *) = 0;
                            }
                        }
                        break;
                    default:
                        return tex_formatted_error("callback", "string expected, not: %s\n", lua_typename(L, t));
                }
                break;
            case callback_node_key:
                switch (t) {
                    case LUA_TUSERDATA:
                    case LUA_TNUMBER:
                     // *va_arg(vl, int *) = lmt_check_isnode(L, nres);
                        *va_arg(vl, int *) = lmt_pop_node_from_callback(L, nres);
                        break;
                    default:
                        *va_arg(vl, int *) = null;
                        break;
                }
                break;
            case callback_string_key:
                switch (t) {
                    case LUA_TSTRING:
                        {
                            size_t len;
                            const char *s = lua_tolstring(L, nres, &len);
                            if (s) {
                                char *ss = lmt_memory_malloc((unsigned) (len + 1));
                                if (ss) {
                                    memcpy(ss, s, (len + 1));
                                 }
                                *va_arg(vl, char **) = ss;
                            } else {
                                *va_arg(vl, char **) = NULL;
                             // *va_arg(vl, int *) = 0;
                            }
                        }
                        break;
                    default:
                        return tex_formatted_error("callback", "string expected, not: %s\n", lua_typename(L, t));
                }
                break;
            case callback_result_s_key:
                switch (t) {
                    case LUA_TNIL:
                        *va_arg(vl, int *) = 0;
                        break;
                    case LUA_TBOOLEAN:
                        if (lua_toboolean(L, nres) == 0) {
                            *va_arg(vl, int *) = 0;
                            break;
                        } else {
                            return tex_formatted_error("callback", "string, false or nil expected, not: %s\n", lua_typename(L, t));
                        }
                    case LUA_TSTRING:
                        {
                            size_t len;
                            const char *s = lua_tolstring(L, nres, &len);
                            if (s) {
                                char *ss = lmt_memory_malloc((unsigned) (len + 1));
                                if (ss) {
                                    memcpy(ss, s, (len + 1));
                                    *va_arg(vl, char **) = ss;
                                } else {
                                   *va_arg(vl, char **) = NULL;
                                // *va_arg(vl, int *) = 0;
                                }
                            } else {
                                *va_arg(vl, char **) = NULL;
                             // *va_arg(vl, int *) = 0;
                            }
                        }
                        break;
                    default:
                        return tex_formatted_error("callback", "string, false or nil expected, not: %s\n", lua_typename(L, t));
                }
                break;
            case callback_result_i_key:
                switch (t) {
                    case LUA_TNUMBER:
                        *va_arg(vl, int *) = lmt_tointeger(L, nres);
                        break;
                    default:
                     /* *va_arg(vl, int *) = 0; */ /*tex We keep the value! */
                        break;
                }
                break;
            default:
                return tex_formatted_error("callback", "invalid value type returned\n");
        }
        nres++;
    }
    return 1;
}

/*tex
    Especially the \IO\ related callbacks are registered once, for instance when a file is opened,
    and (re)used later. These are dealt with here.
*/

int lmt_run_saved_callback_close(lua_State *L, int r)
{
    int ret = 0;
    int stacktop = lua_gettop(L);
    lua_rawgeti(L, LUA_REGISTRYINDEX, r);
    lua_push_key(close);
    if (lua_rawget(L, -2) == LUA_TFUNCTION) {
        ret = lua_pcall(L, 0, 0, 0);
        if (ret) {
            return tex_formatted_error("lua", "error in close file callback") - 1;
        }
    }
    lua_settop(L, stacktop);
    return ret;
}

int lmt_run_saved_callback_line(lua_State *L, int r, int firstpos)
{
    int ret = -1; /* -1 is error, >= 0 is buffer length */
    int stacktop = lua_gettop(L);
    lua_rawgeti(L, LUA_REGISTRYINDEX, r);
    lua_push_key(reader);
    if (lua_rawget(L, -2) == LUA_TFUNCTION) {
        lua_pushvalue(L, -2);
        lmt_lua_state.file_callback_count++;
        ret = lua_pcall(L, 1, 1, 0);
        if (ret) {
            ret = tex_formatted_error("lua", "error in read line callback") - 1;
        } else if (lua_type(L, -1) == LUA_TSTRING) {
            size_t len;
            const char *s = lua_tolstring(L, -1, &len);
            if (s && len > 0) {
                while (len >= 1 && s[len-1] == ' ') {
                    len--;
                }
                if (len > 0) {
                    if (tex_room_in_buffer(firstpos + (int) len)) {
                        strncpy((char *) (lmt_fileio_state.io_buffer + firstpos), s, len);
                        ret = firstpos + (int) len;
                    } else {
                        tex_overflow_error("buffer", (int) len);
                        ret = 0;
                    }
                } else {
                    ret = 0;
                }
            } else {
                ret = 0;
            }
        } else {
            ret = -1;
        }
    }
    lua_settop(L, stacktop);
    return ret;
}

/*tex

    Many callbacks have a specific handler, so they don't use the previously mentioned generic one.
    The next bunch of helpers checks for them being set and deals invoking them as well as reporting
    errors.

*/

# define callbacks_trace 0

int lmt_callback_okay(lua_State *L, int i, int *top)
{
    *top = lua_gettop(L);
    lua_rawgeti(L, LUA_REGISTRYINDEX, lmt_callback_state.index);
    lua_pushcfunction(L, lmt_traceback); /* goes before function */
    if (lua_rawgeti(L, -2, i) == LUA_TFUNCTION) {
        if (lmt_callback_state.options & callback_option_trace) {
            /*tex Just a raw print because we don't want interference */
            printf("[callback %02i : %s]\n",i,lmt_callback_state.items[i].name);
        }
        lmt_lua_state.saved_callback_count++;
        return 1;
    } else {
        lua_pop(L, 3);
        return 0;
    }
}

void lmt_callback_error(lua_State *L, int top, int i)
{
    lua_remove(L, top + 2);
    lmt_error(L, "callback error", -1, (i == LUA_ERRRUN ? 0 : 1));
    lua_settop(L, top);
}

int lmt_run_and_save_callback(lua_State *L, int i, const char *values, ...)
{
    int top = 0;
    int ret = 0;
    if (lmt_callback_okay(L, i, &top)) {
        va_list args;
        va_start(args, values);
        ret = callbacklib_aux_run(L, i, 1, values, args, top, top + 2);
        va_end(args);
        if (ret > 0) {
            ret = lua_type(L, -1) == LUA_TTABLE ? luaL_ref(L, LUA_REGISTRYINDEX) : 0;
        }
        lua_settop(L, top);
    }
    return ret;
}

int lmt_run_callback(lua_State *L, int i, const char *values, ...)
{
    int top = 0;
    int ret = 0;
    if (lmt_callback_okay(L, i, &top)) {
        va_list args;
        va_start(args, values);
        ret = callbacklib_aux_run(L, i, 0, values, args, top, top + 2);
        va_end(args);
        lua_settop(L, top);
    }
    return ret;
}

void lmt_destroy_saved_callback(lua_State *L, int i)
{
    luaL_unref(L, LUA_REGISTRYINDEX, i);
}

static int callbacklib_found(lua_State *L)
{
    switch (lua_type(L, 1)) { 
        case LUA_TNUMBER: 
            {
                halfword cb = lmt_tohalfword(L, 1);
                return (cb > 0 && cb < total_callbacks) ? cb :  -1;
            }
        case LUA_TSTRING:   
            {
                const char *s = lua_tostring(L, 1);
                if (s) {
                    /* hm, why not start at 1 */
                    for (int cb = 0; cb < total_callbacks; cb++) {
                        if (strcmp(lmt_callback_state.items[cb].name, s) == 0) {
                            return cb;
                        }
                    }
                }
                return -1;
            } 
        default:
            return -1;
    }
}

static int callbacklib_register(lua_State *L)
{
    int cb = callbacklib_found(L);
    if (cb > 0) {
        if (lmt_callback_state.items[cb].state & callback_state_frozen) {
           /*tex Maybe issue a message. */
        } else {
            switch (lua_type(L, 2)) {
                case LUA_TFUNCTION:
                    lmt_callback_state.items[cb].value = cb;
                    lmt_callback_state.items[cb].state |= callback_state_set;
                    lmt_callback_state.items[cb].state |= callback_state_touched;
                    break;
                case LUA_TBOOLEAN:
                    if (lua_toboolean(L, 2)) {
                        goto BAD; /*tex Only |false| is valid. */
                    }
                    // fall through
                case LUA_TNIL:
                    lmt_callback_state.items[cb].value = -1;
                    lmt_callback_state.items[cb].state &= ~ callback_state_set;
                    lmt_callback_state.items[cb].state |= callback_state_touched;
                    break;
            }
            /*tex Push the callback table on the stack. */ 
            lua_rawgeti(L, LUA_REGISTRYINDEX, lmt_callback_state.index);
            /*tex Push the function or |nil|. */
            lua_pushvalue(L, 2);
            /*tex Update the value. */
            lua_rawseti(L, -2, cb);
            lua_pop(L, 1); /* was: lua_rawseti(L, LUA_REGISTRYINDEX, lmt_callback_state.metatable_index); */
            /*tex Return the callback id, which is a fixed value. */
            lua_pushinteger(L, cb);
            return 1;
        } 
    }
  BAD:
    lua_pushnil(L);
    return 1;
}

static int callbacklib_getstate(lua_State *L)
{
    int cb = callbacklib_found(L);
    if (cb > 0) {
        lua_pushinteger(L, lmt_callback_state.items[cb].state);
    } else {
        lua_pushnil(L);
    }
    return 1;
}

static int callbacklib_setstate(lua_State *L)
{
    int cb = callbacklib_found(L);
    if (cb > 0) {
        /*tex We can always enable and disable. */
        if (lua_type(L, 2) == LUA_TNUMBER) {
            lmt_callback_state.items[cb].state |= (lua_tointeger(L, 2) & 0xFFFF);
        } else { 
            lmt_callback_state.items[cb].state &= ~ callback_state_disabled;
        }
        lua_pushboolean(L, 1);
    } else {
        lua_pushboolean(L, 0);
    }
    return 1;
}

static int callbacklib_setoptions(lua_State *L)
{
    unsigned options = lmt_tounsigned(L, 1);
    int set = lua_type(L, 2) == LUA_TBOOLEAN ? lua_toboolean(L, 2) : 1;
    if (options & callback_option_direct) {
        if (lmt_callback_state.options & callback_option_direct) {
            tex_formatted_warning("callbacks", "direct node mode is enabled and can't be changed");
        } else if (set) { 
            lmt_callback_state.options |= callback_option_direct;
        }
    }
    if (options & callback_option_trace) {
        if (set) {
            lmt_callback_state.options |= callback_option_trace; 
        } else {
            lmt_callback_state.options &= ~ callback_option_trace;
        }
    }
    return 0;
}

static int callbacklib_getoptions(lua_State *L)
{
    lua_pushinteger(L, lmt_callback_state.options);
    return 1;
}

void lmt_run_memory_callback(const char* what, int success)
{
    lmt_run_callback(lmt_lua_state.lua_instance, trace_memory_callback, "Sb->", what, success);
    fflush(stdout);
}

/*tex

    The \LUA\ library that deals with callbacks has some diagnostic helpers that makes it possible
    to implement a higher level interface.

*/

static int callbacklib_find(lua_State *L)
{
    int cb = callbacklib_found(L);
    if (cb > 0 && ! (lmt_callback_state.items[cb].state & callback_state_private)) {
        lua_rawgeti(L, LUA_REGISTRYINDEX, lmt_callback_state.index);
        lua_rawgeti(L, -1, cb);
    } else { 
        lua_pushnil(L);
    }
    return 1;
}

static int callbacklib_known(lua_State *L)
{
    lua_pushboolean(L, callbacklib_found(L) > 0);
    return 1;
}

static int callbacklib_getindex(lua_State *L)
{
    int cb = callbacklib_found(L);
    if (cb > 0) {
        lua_pushinteger(L, cb);
    } else { 
        lua_pushnil(L);
    }
    return 1;
}

static int callbacklib_list(lua_State *L)
{
    lua_createtable(L, 0, total_callbacks);
    for (int cb = 1; cb < total_callbacks; cb++) {
        lua_pushstring(L, lmt_callback_state.items[cb].name);
        lua_pushboolean(L, lmt_callback_defined(cb));
        lua_rawset(L, -3);
    }
    return 1;
}

static int callbacklib_names(lua_State *L)
{
    lua_createtable(L, total_callbacks, 0);
    for (int cb = 1; cb < total_callbacks; cb++) {
        lua_pushstring(L, lmt_callback_state.items[cb].name);
        lua_rawseti(L, -2, cb);
    }
    return 1;
}

/* todo: language function calls */

void lmt_push_callback_usage(lua_State *L)
{
    lua_createtable(L, 0, 9);
    lua_push_integer_at_key(L, saved,    lmt_lua_state.saved_callback_count);
    lua_push_integer_at_key(L, file,     lmt_lua_state.file_callback_count);
    lua_push_integer_at_key(L, direct,   lmt_lua_state.direct_callback_count);
    lua_push_integer_at_key(L, function, lmt_lua_state.function_callback_count);
    lua_push_integer_at_key(L, value,    lmt_lua_state.value_callback_count);
    lua_push_integer_at_key(L, local,    lmt_lua_state.local_callback_count);
    lua_push_integer_at_key(L, bytecode, lmt_lua_state.bytecode_callback_count);
    lua_push_integer_at_key(L, message,  lmt_lua_state.message_callback_count);
    lua_push_integer_at_key(L, count,
        lmt_lua_state.saved_callback_count
      + lmt_lua_state.file_callback_count
      + lmt_lua_state.direct_callback_count
      + lmt_lua_state.function_callback_count
      + lmt_lua_state.value_callback_count
      + lmt_lua_state.local_callback_count
      + lmt_lua_state.bytecode_callback_count
      + lmt_lua_state.message_callback_count
    );
}

static int callbacklib_usage(lua_State *L)
{
    lmt_push_callback_usage(L);
    return 1;
}

static int callbacklib_testonly(lua_State *L)
{
    int cb = lmt_callback_defined(test_only_callback);
    if (cb > 0) {
        int top = 0;
        if (lmt_callback_okay(L, cb, &top)) {
            int i; 
            lua_pushinteger(L, lmt_callback_state.items[cb].state);
            i = lmt_callback_call(L, 1, 0, top);
            if (i) {
                lmt_callback_error(L, top, i);
            } else {
                lmt_callback_wrapup(L, top);
                lua_pushboolean(L, 1);
                return 1;
            }
        }
    }
    lua_pushboolean(L, 0);
    return 1;
}

static int callbacklib_getoptionvalues(lua_State *L)
{
    lua_createtable(L, 2, 0);
    lua_set_string_by_index(L, callback_option_direct, "direct");
    lua_set_string_by_index(L, callback_option_trace,  "trace");
    return 1;
}

static int callbacklib_getstatevalues(lua_State *L)
{
    lua_createtable(L, 2, 6);
    lua_set_string_by_index(L, callback_state_set,         "set");
    lua_set_string_by_index(L, callback_state_disabled,    "disabled");
    lua_set_string_by_index(L, callback_state_frozen,      "frozen");
    lua_set_string_by_index(L, callback_state_private,     "private");
    lua_set_string_by_index(L, callback_state_touched,     "touched");
    lua_set_string_by_index(L, callback_state_tracing,     "tracing");
    lua_set_string_by_index(L, callback_state_selective,   "selective");
    lua_set_string_by_index(L, callback_state_fundamental, "fundamental");
    return 1;
}

static const struct luaL_Reg callbacklib_function_list[] = {
    { "find",            callbacklib_find            },
    { "known",           callbacklib_known           },
    { "register",        callbacklib_register        },
    { "list",            callbacklib_list            },
    { "names",           callbacklib_names           },
    { "usage",           callbacklib_usage           },
    { "getindex",        callbacklib_getindex        },
    { "setstate",        callbacklib_setstate        },
    { "getstate",        callbacklib_getstate        },
    { "setoptions",      callbacklib_setoptions      },
    { "getoptions",      callbacklib_getoptions      },
    { "testonly",        callbacklib_testonly        },
    { "getoptionvalues", callbacklib_getoptionvalues },
    { "getstatevalues",  callbacklib_getstatevalues  },
    { NULL,              NULL                        },
};

int luaopen_callback(lua_State *L)
{
    lua_newtable(L);
    luaL_setfuncs(L, callbacklib_function_list, 0);
    lua_createtable(L, total_callbacks, 0);
    lmt_callback_state.index = luaL_ref(L, LUA_REGISTRYINDEX);
    return 1;
}
