/*
 * libbrlapi - A library providing access to braille terminals for applications.
 *
 * Copyright (C) 2006-2024 by Dave Mielke <dave@mielke.cc>
 *
 * libbrlapi comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
 *
 * Web Page: http://brltty.app/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

#define BRLAPI_NO_SINGLE_SESSION 1
#include "prologue.h"
#include <brlapi.h>
#include <brl_cmds.h>
#include <errno.h>
#include <lua.h>
#include <lauxlib.h>

static const char *handle_t = "brlapi";
#define checkhandle(L, arg) (brlapi_handle_t *)luaL_checkudata(L, (arg), handle_t)

static void error(lua_State *L) {
  lua_pushstring(L, brlapi_strerror(&brlapi_error));
  lua_error(L);
}

static int openConnection(lua_State *L) {
  const brlapi_connectionSettings_t desiredSettings = {
    .host = luaL_optstring(L, 1, NULL), .auth = luaL_optstring(L, 2, NULL)
  };
  brlapi_connectionSettings_t actualSettings;
  brlapi_handle_t *const handle = (brlapi_handle_t *)lua_newuserdatauv(
    L, brlapi_getHandleSize(), 0
  );
  luaL_setmetatable(L, handle_t);

  if (brlapi__openConnection(handle, &desiredSettings, &actualSettings) == -1)
    error(L);

  lua_pushstring(L, actualSettings.host);
  lua_pushstring(L, actualSettings.auth);    

  return 3;
}

static int getFileDescriptor(lua_State *L) {
  const int fileDescriptor = brlapi__getFileDescriptor(checkhandle(L, 1));

  if (fileDescriptor == BRLAPI_INVALID_FILE_DESCRIPTOR) error(L);

  lua_pushinteger(L, fileDescriptor);

  return 1;
}

static int closeConnection(lua_State *L) {
  brlapi__closeConnection(checkhandle(L, 1));

  return 0;
}

static int getDriverName(lua_State *L) {
  char name[BRLAPI_MAXNAMELENGTH + 1];

  if (brlapi__getDriverName(checkhandle(L, 1), name, sizeof(name)) == -1)
    error(L);

  lua_pushstring(L, name);

  return 1;
}

static int getModelIdentifier(lua_State *L) {
  brlapi_handle_t *const handle = checkhandle(L, 1);
  char ident[BRLAPI_MAXNAMELENGTH + 1];

  if (brlapi__getModelIdentifier(handle, ident, sizeof(ident)) == -1) error(L);

  lua_pushstring(L, ident);

  return 1;
}

static int getDisplaySize(lua_State *L) {
  unsigned int x, y;

  if (brlapi__getDisplaySize(checkhandle(L, 1), &x, &y) == -1) error(L);

  lua_pushinteger(L, x), lua_pushinteger(L, y);

  return 2;
}

static int enterTtyMode(lua_State *L) {
  const int result = brlapi__enterTtyMode(checkhandle(L, 1),
    luaL_optinteger(L, 2, BRLAPI_TTY_DEFAULT), luaL_optstring(L, 3, NULL)
  );

  if (result == -1) error(L);

  lua_pushinteger(L, result);

  return 1;
}

static int leaveTtyMode(lua_State *L) {
  if (brlapi__leaveTtyMode(checkhandle(L, 1)) == -1) error(L);

  return 0;
}

static int setFocus(lua_State *L) {
  if (brlapi__setFocus(checkhandle(L, 1), luaL_checkinteger(L, 2)) == -1)
    error(L);

  return 0;
}

static int writeText(lua_State *L) {
  brlapi_handle_t *const handle = checkhandle(L, 1);
  const int cursor = luaL_checkinteger(L, 2);
  const char *const text = luaL_checkstring(L, 3);

  if (brlapi__writeText(handle, cursor, text) == -1) error(L);

  return 0;
}

static int writeDots(lua_State *L) {
  brlapi_handle_t *const handle = checkhandle(L, 1);
  const unsigned char *const dots = (const unsigned char *)luaL_checkstring(L, 2);

  if (brlapi__writeDots(handle, dots) == -1) error(L);

  return 0;
}

static int readKey(lua_State *L) {
  brlapi_handle_t *const handle = checkhandle(L, 1);
  const int wait = lua_toboolean(L, 2);
  brlapi_keyCode_t keyCode;
  int result;

  do {
    result = brlapi__readKey(handle, wait, &keyCode);
  } while (result == -1 &&
           brlapi_errno == BRLAPI_ERROR_LIBCERR && brlapi_libcerrno == EINTR);

  switch (result) {
  case -1:
    error(L);
    break; /* never reached */
  case 0:
    break;
  case 1:
    lua_pushinteger(L, (lua_Integer)keyCode);
   
    return 1;
  }

  return 0;
}

static int readKeyWithTimeout(lua_State *L) {
  brlapi_handle_t *const handle = checkhandle(L, 1);
  const int timeout_ms = luaL_checkinteger(L, 2);
  brlapi_keyCode_t keyCode;
  int result;

  do {
    result = brlapi__readKeyWithTimeout(handle, timeout_ms, &keyCode);
  } while (result == -1 &&
           brlapi_errno == BRLAPI_ERROR_LIBCERR && brlapi_libcerrno == EINTR);

  switch (result) {
  case -1:
    error(L);
    break; /* never reached */
  case 0:
    break;
  case 1:
    lua_pushinteger(L, (lua_Integer)keyCode);
 
    return 1;
  }

  return 0;
}

static int expandKeyCode(lua_State *L) {
  brlapi_keyCode_t keyCode = (brlapi_keyCode_t)luaL_checkinteger(L, 1);
  brlapi_expandedKeyCode_t expansion;

  brlapi_expandKeyCode(keyCode, &expansion);
  lua_pushinteger(L, expansion.type),
  lua_pushinteger(L, expansion.command),
  lua_pushinteger(L, expansion.argument),
  lua_pushinteger(L, expansion.flags);

  return 4;
}

static int describeKeyCode(lua_State *L) {
  brlapi_keyCode_t keyCode = (brlapi_keyCode_t)luaL_checkinteger(L, 1);
  brlapi_describedKeyCode_t description;

  brlapi_describeKeyCode(keyCode, &description);

  lua_pushstring(L, description.type),
  lua_pushstring(L, description.command),
  lua_pushinteger(L, description.argument);
  lua_createtable(L, description.flags, 0);
  for (size_t i = 0; i < description.flags; i += 1) {
    lua_pushstring(L, description.flag[i]);
    lua_rawseti(L, -2, i + 1);
  }

  return 4;
}

static int enterRawMode(lua_State *L) {
  brlapi_handle_t *const handle = checkhandle(L, 1);
  const char *const driverName = luaL_checkstring(L, 2);

  if (brlapi__enterRawMode(handle, driverName) == -1) error(L);

  return 0;
}

static int leaveRawMode(lua_State *L) {
  brlapi_handle_t *const handle = checkhandle(L, 1);

  if (brlapi__leaveRawMode(handle) == -1) error(L);

  return 0;
}

static int changeKeys(
  lua_State *L,
  int BRLAPI_STDCALL (*change) (brlapi_handle_t *, brlapi_rangeType_t, const brlapi_keyCode_t[], unsigned int)
) {
  brlapi_handle_t *const handle = checkhandle(L, 1);
  static const char *rangeNames[] = {
    "all", "code", "command", "key", "type", NULL
  };
  static const brlapi_rangeType_t rangeTypes[] = {
    brlapi_rangeType_all,
    brlapi_rangeType_code,
    brlapi_rangeType_command,
    brlapi_rangeType_key,
    brlapi_rangeType_type
  };
  const brlapi_rangeType_t range = rangeTypes[
    luaL_checkoption(L, 2, NULL, rangeNames)
  ];

  if (range == brlapi_rangeType_all) {
    if (change(handle, range, NULL, 0) == -1) error(L);

    return 0;
  }

  luaL_checktype(L, 3, LUA_TTABLE);
  lua_len(L, 3);

  {
    const size_t count = lua_tointeger(L, -1);
    brlapi_keyCode_t keys[count];
    lua_pop(L, 1);

    for (size_t i = 0; i < count; i += 1) {
      lua_geti(L, 3, i + 1);
      keys[i] = (brlapi_keyCode_t)luaL_checkinteger(L, -1);
      lua_pop(L, 1);
    }

    if (change(handle, range, keys, count) == -1) error(L);
  }
 
  return 0;
}

static int acceptKeys(lua_State *L) {
  return changeKeys(L, brlapi__acceptKeys);
}

static int ignoreKeys(lua_State *L) {
  return changeKeys(L, brlapi__ignoreKeys);
}

static int acceptAllKeys(lua_State *L) {
  if (brlapi__acceptAllKeys(checkhandle(L, 1)) == -1) error(L);

  return 0;
}

static int ignoreAllKeys(lua_State *L) {
  if (brlapi__ignoreAllKeys(checkhandle(L, 1)) == -1) error(L);

  return 0;
}

static int suspendDriver(lua_State *L) {
  brlapi_handle_t *const handle = checkhandle(L, 1);
  const char *const driverName = luaL_checkstring(L, 2);

  if (brlapi__suspendDriver(handle, driverName) == -1) error(L);

  return 0;
}

static int resumeDriver(lua_State *L) {
  if (brlapi__resumeDriver(checkhandle(L, 1)) == -1) error(L);

  return 0;
}

static int pause_(lua_State *L) {
  brlapi_handle_t *const handle = checkhandle(L, 1);
  const int timeout_ms = luaL_checkinteger(L, 2);
  int result;

  do {
    result = brlapi__pause(handle, timeout_ms);
  } while (timeout_ms == -1 && result == -1 &&
           brlapi_errno == BRLAPI_ERROR_LIBCERR && brlapi_libcerrno == EINTR);

  if (result == -1) error(L);

  return 0;
}

static int getBooleanParameter(lua_State *L, brlapi_param_t param) {
  brlapi_param_bool_t value;
  const int result = brlapi__getParameter(checkhandle(L, 1),
    param, 0, BRLAPI_PARAMF_GLOBAL, &value, sizeof(value)
  );

  if (result == -1) error(L);

  lua_pushboolean(L, value);

  return 1;
}

static int setBooleanParameter(lua_State *L, brlapi_param_t param) {
  brlapi_handle_t *const handle = checkhandle(L, 1);
  const brlapi_param_flags_t flags = BRLAPI_PARAMF_GLOBAL;
  const brlapi_param_bool_t value = lua_toboolean(L, 2);

  if (brlapi__setParameter(handle, param, 0, flags, &value, sizeof(value)) == -1)
    error(L);

  return 0;
}

static int getStringParameter(
  lua_State *L,
  brlapi_param_t param, brlapi_param_subparam_t subparam
) {
  size_t count;
  char *string = brlapi__getParameterAlloc(checkhandle(L, 1),
    param, subparam, BRLAPI_PARAMF_GLOBAL, &count
  );

  if (string == NULL) error(L);

  lua_pushlstring(L, string, count);
  free(string);

  return 1;
}

static int setStringParameter(
  lua_State *L,
  brlapi_param_t param, brlapi_param_subparam_t subparam
) {
  brlapi_handle_t *const handle = checkhandle(L, 1);
  size_t length;
  const brlapi_param_flags_t flags = BRLAPI_PARAMF_GLOBAL;
  const char *string = luaL_checklstring(L, 2, &length);

  if (brlapi__setParameter(handle, param, subparam, flags, string, length) == -1)
    error(L);

  return 0;
}

static int getDriverCode(lua_State *L) {
  return getStringParameter(L, BRLAPI_PARAM_DRIVER_CODE, 0);
}

static int getDriverVersion(lua_State *L) {
  return getStringParameter(L, BRLAPI_PARAM_DRIVER_VERSION, 0);
}

static int getDeviceOnline(lua_State *L) {
  return getBooleanParameter(L, BRLAPI_PARAM_DEVICE_ONLINE);
}

static int getAudibleAlerts(lua_State *L) {
  return getBooleanParameter(L, BRLAPI_PARAM_AUDIBLE_ALERTS);
}

static int setAudibleAlerts(lua_State *L) {
  return setBooleanParameter(L, BRLAPI_PARAM_AUDIBLE_ALERTS);
}

static int getClipboardContent(lua_State *L) {
  return getStringParameter(L, BRLAPI_PARAM_CLIPBOARD_CONTENT, 0);
}

static int setClipboardContent(lua_State *L) {
  return setStringParameter(L, BRLAPI_PARAM_CLIPBOARD_CONTENT, 0);
}

static int getComputerBrailleTable(lua_State *L) {
  return getStringParameter(L, BRLAPI_PARAM_COMPUTER_BRAILLE_TABLE, 0);
}

static int setComputerBrailleTable(lua_State *L) {
  return setStringParameter(L, BRLAPI_PARAM_COMPUTER_BRAILLE_TABLE, 0);
}

static int getLiteraryBrailleTable(lua_State *L) {
  return getStringParameter(L, BRLAPI_PARAM_LITERARY_BRAILLE_TABLE, 0);
}

static int setLiteraryBrailleTable(lua_State *L) {
  return setStringParameter(L, BRLAPI_PARAM_LITERARY_BRAILLE_TABLE, 0);
}

static int getMessageLocale(lua_State *L) {
  return getStringParameter(L, BRLAPI_PARAM_MESSAGE_LOCALE, 0);
}

static int setMessageLocale(lua_State *L) {
  return setStringParameter(L, BRLAPI_PARAM_MESSAGE_LOCALE, 0);
}

static int getBoundCommandKeycodes(lua_State *L) {
  size_t count;
  brlapi_keyCode_t *codes = brlapi__getParameterAlloc(checkhandle(L, 1),
    BRLAPI_PARAM_BOUND_COMMAND_KEYCODES, 0, BRLAPI_PARAMF_GLOBAL, &count
  );

  if (codes == NULL) error(L);

  count /= sizeof(brlapi_keyCode_t);

  lua_newtable(L);
  for (size_t i = 0; i < count; i += 1) {
    lua_pushinteger(L, (lua_Integer)codes[i]);
    lua_rawseti(L, -2, i + 1);
  }

  free(codes);

  return 1;
}

static int getCommandKeycodeName(lua_State *L) {
  const brlapi_keyCode_t keyCode = (brlapi_keyCode_t)luaL_checkinteger(L, 2);
  return getStringParameter(L, BRLAPI_PARAM_COMMAND_KEYCODE_NAME, keyCode);
}

static int getCommandKeycodeSummary(lua_State *L) {
  const brlapi_keyCode_t keyCode = (brlapi_keyCode_t)luaL_checkinteger(L, 2);
  return getStringParameter(L, BRLAPI_PARAM_COMMAND_KEYCODE_SUMMARY, keyCode);
}

static int getDefinedDriverKeycodes(lua_State *L) {
  size_t count;
  brlapi_keyCode_t *codes = brlapi__getParameterAlloc(checkhandle(L, 1),
    BRLAPI_PARAM_DEFINED_DRIVER_KEYCODES, 0, BRLAPI_PARAMF_GLOBAL, &count
  );

  if (codes == NULL) error(L);

  count /= sizeof(brlapi_keyCode_t);

  lua_newtable(L);
  for (size_t i = 0; i < count; i += 1) {
    lua_pushinteger(L, (lua_Integer)codes[i]);
    lua_rawseti(L, -2, i + 1);
  }

  free(codes);

  return 1;
}

static int getDriverKeycodeName(lua_State *L) {
  const brlapi_keyCode_t keyCode = (brlapi_keyCode_t)luaL_checkinteger(L, 2);
  return getStringParameter(L, BRLAPI_PARAM_DRIVER_KEYCODE_NAME, keyCode);
}

static int getDriverKeycodeSummary(lua_State *L) {
  const brlapi_keyCode_t keyCode = (brlapi_keyCode_t)luaL_checkinteger(L, 2);
  return getStringParameter(L, BRLAPI_PARAM_DRIVER_KEYCODE_SUMMARY, keyCode);
}

static inline int versionGetField(lua_State *L, const char *name) {
  if (lua_getfield(L, 1, name) == LUA_TNUMBER) {
    if (lua_getfield(L, 2, name) == LUA_TNUMBER) {
      return 1;
    }
  }

  return 0;
}

static int versionCompare(lua_State *L, int op) {
  int result = 0;

  if (versionGetField(L, "major")) {
    if (lua_compare(L, -2, -1, LUA_OPLT)) {
      result = 1;
    } else if (lua_compare(L, -2, -1, LUA_OPEQ)) {
      if (versionGetField(L, "minor")) {
	if (lua_compare(L, -2, -1, LUA_OPLT)) {
	  result = 1;
	} else if (lua_compare(L, -2, -1, LUA_OPEQ)) {
	  if (versionGetField(L, "revision")) {
	    if (lua_compare(L, -2, -1, op)) {
	      result = 1;
	    }
	  }
	}
      }
    }
  }

  lua_pushboolean(L, result);
  return 1;
}

static int versionLT(lua_State *L) {
  return versionCompare(L, LUA_OPLT);
}

static int versionLE(lua_State *L) {
  return versionCompare(L, LUA_OPLE);
}

static int versionString(lua_State *L) {
  static const char *separator = ".";

  lua_getfield(L, 1, "major");
  lua_pushstring(L, separator);
  lua_getfield(L, 1, "minor");
  lua_pushstring(L, separator);
  lua_getfield(L, 1, "revision");

  lua_concat(L, 5);

  return 1;
}

static const luaL_Reg version_meta[] = {
  { "__lt", versionLT },
  { "__le", versionLE },
  { "__tostring", versionString },
  { NULL, NULL }
};

static const luaL_Reg meta[] = {
  { "__close", closeConnection },
  { NULL, NULL }
};

static const luaL_Reg funcs[] = {
  { "openConnection", openConnection },
  { "getFileDescriptor", getFileDescriptor },
  { "closeConnection", closeConnection },
  { "getDriverName", getDriverName },
  { "getModelIdentifier", getModelIdentifier },
  { "getDisplaySize", getDisplaySize },
  { "enterTtyMode", enterTtyMode },
  { "leaveTtyMode", leaveTtyMode },
  { "setFocus", setFocus },
  { "writeText", writeText },
  { "writeDots", writeDots },
  { "readKey", readKey },
  { "readKeyWithTimeout", readKeyWithTimeout },
  { "expandKeyCode", expandKeyCode },
  { "describeKeyCode", describeKeyCode },
  { "enterRawMode", enterRawMode },
  { "leaveRawMode", leaveRawMode },
  { "acceptKeys", acceptKeys },
  { "ignoreKeys", ignoreKeys },
  { "acceptAllKeys", acceptAllKeys },
  { "ignoreAllKeys", ignoreAllKeys },
  { "suspendDriver", suspendDriver },
  { "resumeDriver", resumeDriver },
  { "pause", pause_ },
  { "getDriverCode", getDriverCode },
  { "getDriverVersion", getDriverVersion },
  { "getDeviceOnline", getDeviceOnline },
  { "getAudibleAlerts", getAudibleAlerts },
  { "setAudibleAlerts", setAudibleAlerts },
  { "getClipboardContent", getClipboardContent },
  { "setClipboardContent", setClipboardContent },
  { "getComputerBrailleTable", getComputerBrailleTable },
  { "setComputerBrailleTable", setComputerBrailleTable },
  { "getLiteraryBrailleTable", getLiteraryBrailleTable },
  { "setLiteraryBrailleTable", setLiteraryBrailleTable },
  { "getMessageLocale", getMessageLocale },
  { "setMessageLocale", setMessageLocale },
  { "getBoundCommandKeycodes", getBoundCommandKeycodes },
  { "getCommandKeycodeName", getCommandKeycodeName },
  { "getCommandKeycodeSummary", getCommandKeycodeSummary },
  { "getDefinedDriverKeycodes", getDefinedDriverKeycodes },
  { "getDriverKeycodeName", getDriverKeycodeName },
  { "getDriverKeycodeSummary", getDriverKeycodeSummary },
  { NULL, NULL }
};

static void createcmdtable(lua_State *L) {
  lua_newtable(L);
#define CMD(name, value) lua_pushinteger(L, (lua_Integer)value), lua_setfield(L, -2, STRINGIFY(name));
#include "cmd.auto.h"
#undef CMD
}

int luaopen_brlapi(lua_State *L) {
  luaL_newmetatable(L, handle_t);
  luaL_setfuncs(L, meta, 0);
  luaL_newlib(L, funcs);

  {
    int major, minor, revision;

    brlapi_getLibraryVersion(&major, &minor, &revision);

    lua_createtable(L, 0, 3);
    lua_pushinteger(L, major), lua_setfield(L, -2, "major");
    lua_pushinteger(L, minor), lua_setfield(L, -2, "minor");
    lua_pushinteger(L, revision), lua_setfield(L, -2, "revision");
    luaL_newmetatable(L, "version");
    luaL_setfuncs(L, version_meta, 0);
    lua_setmetatable(L, -2);
    lua_setfield(L, -2, "version");
  }

  createcmdtable(L);
  lua_setfield(L, -2, "CMD");

  /* Set function table as metatable __index */
  lua_pushvalue(L, -1), lua_setfield(L, -3, "__index");

  return 1;
}
