/*
 * 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>.
 */

#include "prologue.h"

#include <stdio.h>
#include <string.h>

#include "log.h"
#include "scr_terminal.h"
#include "msg_queue.h"
#include "utf8.h"
#include "hostcmd.h"
#include "parse.h"
#include "file.h"
#include "program.h"
#include "async_handle.h"
#include "async_io.h"
#include "embed.h"

typedef enum {
  PARM_DIRECTORY,
  PARM_EMULATOR,
  PARM_GROUP,
  PARM_HOME,
  PARM_PATH,
  PARM_SHELL,
  PARM_USER,
} ScreenParameters;

#define SCRPARMS "directory", "emulator", "group", "home", "path", "shell", "user"
#include "scr_driver.h"

static char *directoryParameter = NULL;
static char *emulatorParameter = NULL;
static char *groupParameter = NULL;
static char *homeParameter = NULL;
static char *pathParameter = NULL;
static char *shellParameter = NULL;
static char *userParameter = NULL;

static void
setParameter (char **variable, char **parameters, ScreenParameters parameter) {
  char *value = parameters[parameter];
  if (value && !*value) value = NULL;
  *variable = value;
}

static int
processParameters_TerminalEmulatorScreen (char **parameters) {
  setParameter(&directoryParameter, parameters, PARM_DIRECTORY);
  setParameter(&emulatorParameter, parameters, PARM_EMULATOR);
  setParameter(&groupParameter, parameters, PARM_GROUP);
  setParameter(&homeParameter, parameters, PARM_HOME);
  setParameter(&pathParameter, parameters, PARM_PATH);
  setParameter(&shellParameter, parameters, PARM_SHELL);
  setParameter(&userParameter, parameters, PARM_USER);
  return 1;
}

static void
handleException (const char *cause) {
  logMessage(LOG_CATEGORY(SCREEN_DRIVER), "stopping: %s", cause);
  brlttyInterrupt(WAIT_STOP);
}

static AsyncHandle emulatorMonitorHandle = NULL;
static FILE *emulatorStream = NULL;
static char *emulatorStreamBuffer = NULL;
static size_t emulatorStreamBufferSize = 0;

static const char *problemText = NULL;
static ScreenSegmentHeader *screenSegment = NULL;
static ScreenSegmentHeader *cachedSegment = NULL;

static int haveTerminalMessageQueue = 0;
static int terminalMessageQueue;
static int haveSegmentUpdatedHandler = 0;
static int haveEmulatorExitingHandler = 0;

static int
sendTerminalMessage (MessageType type, const void *content, size_t length) {
  if (!haveTerminalMessageQueue) return 0;
  return sendMessage(terminalMessageQueue, type, content, length, 0);
}

static void
messageHandler_segmentUpdated (const MessageHandlerParameters *parameters) {
  mainScreenUpdated();
}

static void
messageHandler_emulatorExiting (const MessageHandlerParameters *parameters) {
  handleException("emulator exiting");
}

static void
enableMessages (key_t key) {
  haveTerminalMessageQueue = getMessageQueue(&terminalMessageQueue, key);

  if (haveTerminalMessageQueue) {
    haveSegmentUpdatedHandler = startMessageReceiver(
      "screen-segment-updated-receiver",
      terminalMessageQueue, TERM_MSG_SEGMENT_UPDATED,
      0, messageHandler_segmentUpdated, NULL
    );

    haveEmulatorExitingHandler = startMessageReceiver(
      "terminal-emulator-exiting-receiver",
      terminalMessageQueue, TERM_MSG_EMULATOR_EXITING,
      0, messageHandler_emulatorExiting, NULL
    );
  }
}

static int
accessSegmentForPath (const char *path) {
  key_t key;

  if (makeTerminalKey(&key, path)) {
    if ((screenSegment = getScreenSegmentForKey(key))) {
      problemText = gettext("no screen cache");
      enableMessages(key);
      return 1;
    } else {
      problemText = gettext("screen not accessible");
    }
  }

  return 0;
}

static void
destruct_TerminalEmulatorScreen (void) {
  brlttyDisableInterrupt();

  if (emulatorMonitorHandle) {
    asyncCancelRequest(emulatorMonitorHandle);
    emulatorMonitorHandle = NULL;
  }

  if (emulatorStream) {
    fclose(emulatorStream);
    emulatorStream = NULL;
  }

  if (emulatorStreamBuffer) {
    free(emulatorStreamBuffer);
    emulatorStreamBuffer = NULL;
  }

  if (screenSegment) {
    detachScreenSegment(screenSegment);
    screenSegment = NULL;
  }

  if (cachedSegment) {
    free(cachedSegment);
    cachedSegment = NULL;
  }
}

static void
driverDirectiveHandler_path (const char *const *operands) {
  const char *path = operands[0];

  if (path) {
    if (!screenSegment) {
      accessSegmentForPath(path);
    }
  }
}

typedef void DriverDirectiveHandler (const char *const *operands);

typedef struct {
  const char *name;
  DriverDirectiveHandler *handler;
} DriverDirectiveEntry;

#define DRIVER_DIRECTIVE_ENTRY(directive) { \
  .name = #directive, \
  .handler = driverDirectiveHandler_ ## directive, \
}

static const DriverDirectiveEntry driverDirectiveTable[] = {
  DRIVER_DIRECTIVE_ENTRY(path),
};

static int
handleDriverDirective (const char *line) {
  char buffer[strlen(line) + 1];
  strcpy(buffer, line);

  const char *operandTable[9];
  unsigned int operandCount = 0;

  {
    char *string = buffer;

    while (operandCount < (ARRAY_COUNT(operandTable) - 1)) {
      static const char delimiters[] = " ";
      char *next = strtok(string, delimiters);
      if (!next) break;

      operandTable[operandCount++] = next;
      string = NULL;
    }

    operandTable[operandCount] = NULL;
  }

  if (operandCount > 0) {
    const char **operands = operandTable;
    const char *name = *operands++;

    const DriverDirectiveEntry *entry = driverDirectiveTable;
    const DriverDirectiveEntry *end = entry + ARRAY_COUNT(driverDirectiveTable);

    while (entry < end) {
      if (strcasecmp(name, entry->name) == 0) {
        entry->handler(operands);
        return 1;
      }

      entry += 1;
    }
  }

  return 0;
}

ASYNC_MONITOR_CALLBACK(emEmulatorMonitor) {
  const char *line;

  {
    int ok = readLine(
      emulatorStream, &emulatorStreamBuffer,
      &emulatorStreamBufferSize, NULL
    );

    if (!ok) {
      const char *cause =
        ferror(emulatorStream)? "emulator stream error":
        feof(emulatorStream)? "end of emulator stream":
        "emulator monitor failure";

      handleException(cause);
      return 0;
    }

    line = emulatorStreamBuffer;
  }

  if (!handleDriverDirective(line)) {
    logMessage(LOG_NOTICE, "%s", line);
  }

  return 1;
}

static char *
makeDefaultEmulatorPath (void) {
  typedef char *PathMaker (const char *name);

  static PathMaker *pathMakers[] = {
    makeProgramPath,
    makeCommandPath,
  };

  PathMaker **pathMaker = pathMakers;
  PathMaker **end = pathMaker + ARRAY_COUNT(pathMakers);

  while (pathMaker < end) {
    char *path = (*pathMaker)("brltty-pty");

    if (path) {
      if (testProgramPath(path)) {
        logMessage(LOG_CATEGORY(SCREEN_DRIVER),
          "default terminal emulator: %s", path
        );

        return path;
      }

      free(path);
    }

    pathMaker += 1;
  }

  logMessage(LOG_WARNING, "default terminal emulator not found");
  return NULL;
}

static int
startEmulator (void) {
  char *emulator = emulatorParameter;

  if (!emulator) {
    if (!(emulator = makeDefaultEmulatorPath())) {
      return 0;
    }
  }

  logMessage(LOG_CATEGORY(SCREEN_DRIVER),
    "terminal emulator command: %s", emulator
  );

  const char *arguments[13];
  unsigned int argumentCount = 0;

  arguments[argumentCount++] = emulator;
  arguments[argumentCount++] = "--driver-directives";

  if (userParameter) {
    arguments[argumentCount++] = "--user";
    arguments[argumentCount++] = userParameter;
  }

  if (groupParameter) {
    arguments[argumentCount++] = "--group";
    arguments[argumentCount++] = groupParameter;
  }

  if (directoryParameter) {
    arguments[argumentCount++] = "--working-directory";
    arguments[argumentCount++] = directoryParameter;
  }

  if (homeParameter) {
    arguments[argumentCount++] = "--home-directory";
    arguments[argumentCount++] = homeParameter;
  }

  arguments[argumentCount++] = "--";
  if (shellParameter) arguments[argumentCount++] = shellParameter;
  arguments[argumentCount++] = NULL;

  HostCommandOptions options;
  initializeHostCommandOptions(&options);
  options.asynchronous = 1;
  options.standardError = &emulatorStream;

  int exitStatus = runHostCommand(arguments, &options);
  if (emulator != emulatorParameter) free(emulator);
  emulator = NULL;

  if (!exitStatus) {
    detachStandardStreams();

    if (asyncMonitorFileInput(&emulatorMonitorHandle, fileno(emulatorStream), emEmulatorMonitor, NULL)) {
      return 1;
    }
  }

  return 0;
}

static int
construct_TerminalEmulatorScreen (void) {
  brlttyEnableInterrupt();

  emulatorMonitorHandle = NULL;
  emulatorStream = NULL;
  emulatorStreamBuffer = NULL;
  emulatorStreamBufferSize = 0;

  problemText = gettext("screen not available");
  screenSegment = NULL;
  cachedSegment = NULL;

  haveTerminalMessageQueue = 0;
  haveSegmentUpdatedHandler = 0;
  haveEmulatorExitingHandler = 0;

  if (pathParameter) {
    if (accessSegmentForPath(pathParameter)) return 1;
  } else if (startEmulator()) {
    problemText = gettext("screen not attached");
    return 1;
  } else {
    problemText = gettext("no screen emulator");
  }

  handleException("driver construction failure");
//destruct_TerminalEmulatorScreen();
  return 0;
}

static int
poll_TerminalEmulatorScreen (void) {
  return !haveSegmentUpdatedHandler;
}

static int
refresh_TerminalEmulatorScreen (void) {
  if (!screenSegment) return 0;
  size_t size = screenSegment->segmentSize;

  if (cachedSegment) {
    if (cachedSegment->segmentSize != size) {
      logMessage(LOG_CATEGORY(SCREEN_DRIVER), "deallocating old screen cache");
      free(cachedSegment);
      cachedSegment = NULL;
    }
  }

  if (!cachedSegment) {
    logMessage(LOG_CATEGORY(SCREEN_DRIVER), "allocating new screen cache");

    if (!(cachedSegment = malloc(size))) {
      logMallocError();
      return 0;
    }
  }

  memcpy(cachedSegment, screenSegment, size);
  return 1;
}

static ScreenSegmentHeader *
getSegment (void) {
  ScreenSegmentHeader *segment = cachedSegment;
  if (!segment) segment = screenSegment;
  return segment;
}

static int
currentVirtualTerminal_TerminalEmulatorScreen (void) {
  ScreenSegmentHeader *segment = getSegment();
  return segment? segment->screenNumber: 0;
}

static void
describe_TerminalEmulatorScreen (ScreenDescription *description) {
  ScreenSegmentHeader *segment = getSegment();

  if (!segment) {
    description->unreadable = problemText;
    description->cols = strlen(description->unreadable);
    description->rows = 1;
    description->posx = 0;
    description->posy = 0;
  } else {
    description->number = currentVirtualTerminal_TerminalEmulatorScreen();
    description->rows = segment->screenHeight;
    description->cols = segment->screenWidth;
    description->posy = segment->cursorRow;
    description->posx = segment->cursorColumn;
  }
}

static void
setScreenAttribute (ScreenAttributes *attributes, ScreenAttributes attribute, unsigned char level, int bright) {
  if (level >= 0X20) {
    *attributes |= attribute;
    if (bright && (level >= 0XD0)) *attributes |= SCR_ATTR_FG_BRIGHT;
  }
}

static int
readCharacters_TerminalEmulatorScreen (const ScreenBox *box, ScreenCharacter *buffer) {
  ScreenSegmentHeader *segment = getSegment();

  if (!segment) {
    setScreenMessage(box, buffer, problemText);
    return 1;
  }

  if (validateScreenBox(box, segment->screenWidth, segment->screenHeight)) {
    ScreenCharacter *target = buffer;

    for (unsigned int row=0; row<box->height; row+=1) {
      const ScreenSegmentCharacter *source =
        getScreenCharacter(segment, box->top + row, box->left, NULL);

      for (unsigned int column=0; column<box->width; column+=1) {
        target->text = source->text;

        {
          ScreenAttributes *attributes = &target->attributes;
          *attributes = 0;
          if (source->blink) *attributes |= SCR_ATTR_BLINK;

          setScreenAttribute(attributes, SCR_ATTR_FG_RED,   source->foreground.red,   1);
          setScreenAttribute(attributes, SCR_ATTR_FG_GREEN, source->foreground.green, 1);
          setScreenAttribute(attributes, SCR_ATTR_FG_BLUE,  source->foreground.blue,  1);

          setScreenAttribute(attributes, SCR_ATTR_BG_RED,   source->background.red,   0);
          setScreenAttribute(attributes, SCR_ATTR_BG_GREEN, source->background.green, 0);
          setScreenAttribute(attributes, SCR_ATTR_BG_BLUE,  source->background.blue,  0);
        }

        source += 1;
        target += 1;
      }
    }

    return 1;
  }

  return 0;
}

static int
insertKey_TerminalEmulatorScreen (ScreenKey key) {
  setScreenKeyModifiers(&key, 0);

  wchar_t character = key & SCR_KEY_CHAR_MASK;
  Utf8Buffer utf8;
  size_t length = convertWcharToUtf8(character, utf8);

  return sendTerminalMessage(TERM_MSG_INPUT_TEXT, utf8, length);
}

static void
scr_initialize (MainScreen *main) {
  initializeRealScreen(main);
  main->base.currentVirtualTerminal = currentVirtualTerminal_TerminalEmulatorScreen;
  main->base.describe = describe_TerminalEmulatorScreen;
  main->base.readCharacters = readCharacters_TerminalEmulatorScreen;
  main->base.insertKey = insertKey_TerminalEmulatorScreen;
  main->base.poll = poll_TerminalEmulatorScreen;
  main->base.refresh = refresh_TerminalEmulatorScreen;

  main->processParameters = processParameters_TerminalEmulatorScreen;
  main->construct = construct_TerminalEmulatorScreen;
  main->destruct = destruct_TerminalEmulatorScreen;
}
