/*
 * BRLTTY - A background process providing access to the console screen (when in
 *          text mode) for a blind person using a refreshable braille display.
 *
 * Copyright (C) 1995-2024 by The BRLTTY Developers.
 *
 * BRLTTY 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>.
 */

/* apitest provides a small test utility for BRLTTY's API */

#include "prologue.h"

#include <stdio.h>
#include <string.h>
#include <signal.h>
#ifdef __MINGW32__
#include "win_pthread.h"
#else
#include <pthread.h>
#endif

#include "cmdline.h"
#include "pid.h"
#include "brl_cmds.h"
#include "brl_dots.h"
#include "cmd.h"
#include "cmd_brlapi.h"
#include "async_wait.h"

#define BRLAPI_NO_DEPRECATED
#include "brlapi.h"

static brlapi_connectionSettings_t settings;
static char *opt_host;
static char *opt_auth;

static int opt_showName;
static int opt_showModelIdentifier;
static int opt_showSize;

static int opt_showDots;
static int opt_showKeyCodes;
static int opt_learnMode;
static int opt_parameters;

static int opt_suspendMode;
static int opt_threadMode;

BEGIN_OPTION_TABLE(programOptions)
  { .word = "brlapi",
    .letter = 'b',
    .argument = "[host][:port]",
    .setting.string = &opt_host,
    .description = "BrlAPIa host and/or port to connect to."
  },

  { .word = "auth",
    .letter = 'a',
    .argument = "scheme+...",
    .setting.string = &opt_auth,
    .description = "BrlAPI authorization/authentication schemes."
  },

  { .word = "name",
    .letter = 'n',
    .setting.flag = &opt_showName,
    .description = "Show the name of the braille driver."
  },

  { .word = "model",
    .letter = 'm',
    .setting.flag = &opt_showModelIdentifier,
    .description = "Show the model identifier of the braille device."
  },

  { .word = "window",
    .letter = 'w',
    .setting.flag = &opt_showSize,
    .description = "Show the dimensions of the braille window."
  },

  { .word = "dots",
    .letter = 'd',
    .setting.flag = &opt_showDots,
    .description = "Show dot pattern."
  },

  { .word = "keycodes",
    .letter = 'k',
    .setting.flag = &opt_showKeyCodes,
    .description = "Enter interactive keycode learn mode."
  },

  { .word = "learn",
    .letter = 'l',
    .setting.flag = &opt_learnMode,
    .description = "Enter interactive command learn mode."
  },

  { .word = "parameters",
    .letter = 'p',
    .setting.flag = &opt_parameters,
    .description = "Test parameters"
  },

  { .word = "suspend",
    .letter = 's',
    .setting.flag = &opt_suspendMode,
    .description = "Suspend the braille driver (press ^C or send SIGUSR1 to resume)."
  },

  { .word = "thread",
    .letter = 't',
    .setting.flag = &opt_threadMode,
    .description = "Exercise threaded use"
  },
END_OPTION_TABLE(programOptions)

static void showDisplaySize(void)
{
  unsigned int x, y;
  fprintf(stderr,"Getting display size: ");
  if (brlapi_getDisplaySize(&x, &y)<0) {
    brlapi_perror("failed");
    exit(PROG_EXIT_FATAL);
  }
  fprintf(stderr, "%dX%d\n", x, y);
}

static void showDriverName(void)
{
  char name[30];
  fprintf(stderr, "Getting driver name: ");
  if (brlapi_getDriverName(name, sizeof(name))<0) {
    brlapi_perror("failed");
    exit(PROG_EXIT_FATAL);
  }
  fprintf(stderr, "%s\n", name);
}

static void showModelIdentifier(void)
{
  char identifier[30];
  fprintf(stderr, "Getting model identifier: ");
  if (brlapi_getModelIdentifier(identifier, sizeof(identifier))<0) {
    brlapi_perror("failed");
    exit(PROG_EXIT_FATAL);
  }
  fprintf(stderr, "%s\n", identifier);
}

#define DOTS_TEXT "dots: "
#define DOTS_TEXTLEN (strlen(DOTS_TEXT))
#define DOTS_LEN 8
#define DOTS_TOTALLEN (DOTS_TEXTLEN + DOTS_LEN)
static void showDots(void)
{
  unsigned int size;

  {
    unsigned int columns, rows;

    if (brlapi_getDisplaySize(&columns, &rows) < 0) {
      brlapi_perror("failed");
      exit(PROG_EXIT_FATAL);
    }

    size = columns * rows;
    unsigned int minimum = DOTS_TOTALLEN;

    if (size < minimum) {
      fprintf(stderr, "can't show dots on a braille display with less than %u cells\n", minimum);
      exit(PROG_EXIT_SEMANTIC);
    }
  }

  if (brlapi_enterTtyMode(-1, NULL) < 0) {
    brlapi_perror("enterTtyMode");
    exit(PROG_EXIT_FATAL);
  }

  {
    fprintf(stderr, "Showing dot patterns\n");

    char text[size + 1];
    memset(text, ' ', size);
    text[size] = 0;
    memcpy(text, DOTS_TEXT, DOTS_TEXTLEN);

    unsigned char or[size];
    memset(or, 0, size);
    or[DOTS_TEXTLEN+0] = BRL_DOT_1;
    or[DOTS_TEXTLEN+1] = BRL_DOT_2;
    or[DOTS_TEXTLEN+2] = BRL_DOT_3;
    or[DOTS_TEXTLEN+3] = BRL_DOT_4;
    or[DOTS_TEXTLEN+4] = BRL_DOT_5;
    or[DOTS_TEXTLEN+5] = BRL_DOT_6;
    or[DOTS_TEXTLEN+6] = BRL_DOT_7;
    or[DOTS_TEXTLEN+7] = BRL_DOT_8;

    brlapi_writeArguments_t wa = BRLAPI_WRITEARGUMENTS_INITIALIZER;
    wa.regionBegin = 1;
    wa.regionSize = size;
    wa.text = text;
    wa.orMask = or;

    if (brlapi_write(&wa) < 0) {
      brlapi_perror("brlapi_write");
      exit(PROG_EXIT_FATAL);
    }
  }

  {
    brlapi_keyCode_t key;
    brlapi_readKey(1, &key);
  }
}

static char *getKeyName (brlapi_keyCode_t key)
{
  return brlapi_getParameterAlloc(BRLAPI_PARAM_KEY_SHORT_NAME, (key & ~BRLAPI_DRV_KEY_PRESS), BRLAPI_PARAMF_GLOBAL, NULL);
}

static void listKeys(void)
{
  size_t length;
  brlapi_param_driverKeycode_t *keys = brlapi_getParameterAlloc(BRLAPI_PARAM_DEVICE_KEY_CODES, 0, BRLAPI_PARAMF_GLOBAL, &length);

  if (keys) {
    length /= sizeof(*keys);
    printf("%zu keys\n", length);

    for (int i=0; i<length; i+=1) {
      printf("key %04"BRLAPI_PRIxKEYCODE":", keys[i]);

      {
        char *name = getKeyName(keys[i]);

        if (name) {
          printf(" name %s", name);
          free(name);
        }
      }

      printf("\n");
    }

    free(keys);
  }
}

static void showKeyCodes(void)
{
  char buf[0X100];

  fprintf(stderr, "Entering keycode learn mode\n");

  if (brlapi_getDriverName(buf, sizeof(buf))==-1) {
    brlapi_perror("getDriverName");
    return;
  }

  if (brlapi_enterTtyMode(-1, buf)<0) {
    brlapi_perror("enterTtyMode");
    return;
  }

  if (brlapi_acceptAllKeys()==-1) {
    brlapi_perror("acceptAllKeys");
    return;
  }

  if (brlapi_writeText(BRLAPI_CURSOR_OFF, "showing key codes")<0) {
    brlapi_perror("brlapi_writeText");
    exit(PROG_EXIT_FATAL);
  }

  int res;
  brlapi_keyCode_t key;

  while ((res = brlapi_readKeyWithTimeout(10000, &key)) > 0) {
    const char *action = (key & BRLAPI_DRV_KEY_PRESS)? "press": "release";
    size_t length = snprintf(buf, sizeof(buf), "%04" BRLAPI_PRIxKEYCODE " (%" BRLAPI_PRIuKEYCODE ") %s", key, key, action);

    {
      char *name = getKeyName(key);

      if (name) {
        snprintf(&buf[length], (sizeof(buf) - length), ": %s", name);
        free(name);
      }
    }

    brlapi_writeText(BRLAPI_CURSOR_OFF, buf);
    fprintf(stderr, "%s\n", buf);
  }

  if (res < 0) brlapi_perror("brlapi_readKey");
}

static void enterLearnMode(void)
{
  int res;
  brlapi_keyCode_t code;
  int cmd;
  char buf[0X100], *val;

  fprintf(stderr,"Entering command learn mode\n");
  if (brlapi_enterTtyMode(-1, NULL)<0) {
    brlapi_perror("enterTtyMode");
    return;
  }

  if (brlapi_writeText(BRLAPI_CURSOR_OFF, "command learn mode")<0) {
    brlapi_perror("brlapi_writeText");
    exit(PROG_EXIT_FATAL);
  }

  while ((res = brlapi_readKey(1, &code)) != -1) {
    fprintf(stderr, "got key %016"BRLAPI_PRIxKEYCODE"\n",code);
    cmd = cmdBrlapiToBrltty(code);
    describeCommand(buf, sizeof(buf), cmd,
                    (CDO_IncludeName | CDO_IncludeOperand));
    brlapi_writeText(BRLAPI_CURSOR_OFF, buf);
    fprintf(stderr, "%s\n", buf);
    val = brlapi_getParameterAlloc(BRLAPI_PARAM_COMMAND_LONG_NAME, cmd, BRLAPI_PARAMF_GLOBAL, NULL);
    fprintf(stderr, "%s\n", val);
    free(val);
    if (cmd==BRL_CMD_LEARN) return;
  }
  brlapi_perror("brlapi_readKey");
}

static void brailleRetainDotsChanged(brlapi_param_t parameter, brlapi_param_subparam_t subparam, brlapi_param_flags_t flags, void *priv, const void *data, size_t len)
{
  const brlapi_param_retainDots_t *d = data;
  if (parameter != BRLAPI_PARAM_RETAIN_DOTS)
  {
    printf("handler called for %x, another parameter than retaindot parameter?!\n", parameter);
    return;
  }
  printf("new retain dots %zd: %d\n", len, *d);
}

static void testParameters(void)
{
  brlapi_param_retainDots_t val;
  if (brlapi_getParameter(BRLAPI_PARAM_RETAIN_DOTS, 0, BRLAPI_PARAMF_LOCAL, &val, sizeof(val)) < 0) {
    brlapi_perror("getParameter");
  }
  printf("retain dots was %d\n", val);
  printf("now watching retain dots parameter\n");
  if (brlapi_watchParameter(BRLAPI_PARAM_RETAIN_DOTS, 0, BRLAPI_PARAMF_LOCAL, brailleRetainDotsChanged, NULL, NULL, 0) == 0) {
    brlapi_perror("watchParameter");
  }
  val = 0;
  printf("setting retain dots parameter to %d\n", val);
  if (brlapi_setParameter(BRLAPI_PARAM_RETAIN_DOTS, 0, BRLAPI_PARAMF_LOCAL, &val, sizeof(val)) < 0) {
    brlapi_perror("setParameter");
  }
  if (brlapi_getParameter(BRLAPI_PARAM_RETAIN_DOTS, 0, BRLAPI_PARAMF_LOCAL, &val, sizeof(val)) < 0) {
    brlapi_perror("getParameter");
  }
  printf("retain dots now %d\n", val);
  val = 1;
  printf("setting retain dots parameter to %d\n", val);
  if (brlapi_setParameter(BRLAPI_PARAM_RETAIN_DOTS, 0, BRLAPI_PARAMF_LOCAL, &val, sizeof(val)) < 0) {
    brlapi_perror("setParameter");
  }
  if (brlapi_getParameter(BRLAPI_PARAM_RETAIN_DOTS, 0, BRLAPI_PARAMF_LOCAL, &val, sizeof(val)) < 0) {
    brlapi_perror("getParameter");
  }
  printf("retain dots now %d\n", val);

  listKeys();
}

#ifdef SIGUSR1
static void emptySignalHandler(int sig) { }
#endif /* SIGUSR1 */

static void suspendDriver(void)
{
  char driver[30];
  fprintf(stderr, "Getting driver name: ");

  if (brlapi_getDriverName(driver, sizeof(driver))<0) {
    brlapi_perror("failed");
    exit(PROG_EXIT_FATAL);
  }
  fprintf(stderr, "%s\n", driver);

  fprintf(stderr, "Suspending driver\n");
  if (brlapi_suspendDriver(driver)) {
    brlapi_perror("suspend");
  } else {
#ifdef SIGUSR1
    signal(SIGUSR1,emptySignalHandler);
#endif /* SIGUSR1 */

    {
      ProcessIdentifier pid = getProcessIdentifier();
      fprintf(stderr, "Waiting (to resume, send SIGUSR1 to process %"PRIpid")\n", pid);
    }

    brlapi_pause(-1);

#ifdef SIGUSR1
    signal(SIGUSR1,SIG_DFL);
#endif /* SIGUSR1 */

    fprintf(stderr, "Resuming driver\n");
    if (brlapi_resumeDriver()) {
      brlapi_perror("resumeDriver");
    }
  }
}

volatile int thread_done;
static void *thread_fun(void *foo)
{
  brlapi_keyCode_t code;
  do {
    brlapi_readKey(1, &code);
    printf("got key %"PRIx64"\n", code);
    if (brlapi_readKeyWithTimeout(1000, &code) != 1) {
      printf("didn't get a key within the 1s delay\n");
    } else {
      printf("got key %"PRIx64" within the 1s delay\n", code);
    }
  } while (code != (BRLAPI_KEY_TYPE_CMD | BRL_CMD_HOME));
  thread_done = 1;
  return NULL;
}

static void exerciseThreads(void)
{
  pthread_t thread;
  unsigned x, y;
  unsigned i = 0;
  if (brlapi_getDisplaySize(&x, &y)<0) {
    brlapi_perror("failed");
    exit(PROG_EXIT_FATAL);
  }
  if (brlapi_enterTtyMode(-1, NULL)<0) {
    brlapi_perror("enterTtyMode");
    exit(PROG_EXIT_FATAL);
  }
  pthread_create(&thread, NULL, thread_fun, NULL);
  while (!thread_done) {
    char buf[x+1];
    snprintf(buf, sizeof(buf), "counting %d", i);
    buf[x] = 0;
    brlapi_writeText(BRLAPI_CURSOR_OFF, buf);
    asyncWait(1000);
    i++;
  }
  pthread_join(thread, NULL);
}

int
main (int argc, char *argv[]) {
  ProgramExitStatus exitStatus = PROG_EXIT_SUCCESS;
  brlapi_fileDescriptor fd;

  {
    const CommandLineDescriptor descriptor = {
      .options = &programOptions,
      .applicationName = "apitest",

      .usage = {
        .purpose = strtext("Test BrlAPI functions."),
      }
    };

    PROCESS_OPTIONS(descriptor, argc, argv);
  }

  settings.host = opt_host;
  settings.auth = opt_auth;
  fprintf(stderr, "Connecting to BrlAPI... ");
  if ((fd=brlapi_openConnection(&settings, &settings)) != (brlapi_fileDescriptor)(-1)) {
    fprintf(stderr, "done (fd=%"PRIfd")\n", fd);
    fprintf(stderr,"Connected to %s using auth %s\n", settings.host, settings.auth);

    if (opt_showName) {
      showDriverName();
    }

    if (opt_showModelIdentifier) {
      showModelIdentifier();
    }

    if (opt_showSize) {
      showDisplaySize();
    }

    if (opt_showDots) {
      showDots();
    }

    if (opt_showKeyCodes) {
      showKeyCodes();
    }

    if (opt_learnMode) {
      enterLearnMode();
    }

    if (opt_parameters) {
      testParameters();
    }

    if (opt_suspendMode) {
      suspendDriver();
    }

    if (opt_threadMode) {
      exerciseThreads();
    }

    brlapi_closeConnection();
    fprintf(stderr, "Disconnected\n");
  } else {
    fprintf(stderr, "failed to connect to %s using auth %s",settings.host, settings.auth);
    brlapi_perror("");
    exitStatus = PROG_EXIT_FATAL;
  }
  return exitStatus;
}
