/*
 * 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-2025 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 <string.h>
#include <errno.h>

#include "log.h"
#include "ctb_translate.h"
#include "brl_dots.h"
#include "file.h"
#include "parse.h"
#include "utf8.h"

static int
putExternalRequests (BrailleContractionData *bcd) {
  typedef enum {
    REQ_TEXT,
    REQ_NUMBER
  } ExternalRequestType;

  typedef struct {
    const char *name;
    ExternalRequestType type;

    union {
      struct {
        const wchar_t *start;
        size_t count;
      } text;

      unsigned int number;
    } value;
  } ExternalRequestEntry;

  const ExternalRequestEntry externalRequestTable[] = {
    { .name = "cursor-position",
      .type = REQ_NUMBER,
      .value.number = bcd->input.cursor? bcd->input.cursor-bcd->input.begin+1: 0
    },

    { .name = "expand-current-word",
      .type = REQ_NUMBER,
      .value.number = prefs.expandCurrentWord
    },

    { .name = "capitalization-mode",
      .type = REQ_NUMBER,
      .value.number = prefs.capitalizationMode
    },

    { .name = "maximum-length",
      .type = REQ_NUMBER,
      .value.number = getOutputCount(bcd)
    },

    { .name = "text",
      .type = REQ_TEXT,
      .value.text = {
        .start = bcd->input.begin,
        .count = getInputCount(bcd)
      }
    },

    { .name = NULL }
  };

  FILE *stream = bcd->table->data.external.standardInput;
  const ExternalRequestEntry *req = externalRequestTable;

  while (req->name) {
    if (fputs(req->name, stream) == EOF) goto outputError;
    if (fputc('=', stream) == EOF) goto outputError;

    switch (req->type) {
      case REQ_TEXT: {
        const wchar_t *character = req->value.text.start;
        const wchar_t *end = character + req->value.text.count;

        while (character < end) {
          Utf8Buffer utf8;
          size_t utfs = convertWcharToUtf8(*character++, utf8);

          if (!utfs) return 0;
          if (fputs(utf8, stream) == EOF) goto outputError;
        }

        break;
      }

      case REQ_NUMBER:
        if (fprintf(stream, "%u", req->value.number) == EOF) goto outputError;
        break;

      default:
        logMessage(LOG_WARNING, "unimplemented external contraction request property type: %s: %u (%s)", bcd->table->data.external.command, req->type, req->name);
        return 0;
    }

    if (fputc('\n', stream) == EOF) goto outputError;
    req += 1;
  }

  if (fflush(stream) == EOF) goto outputError;
  return 1;

outputError:
  logMessage(LOG_WARNING, "external contraction output error: %s: %s", bcd->table->data.external.command, strerror(errno));
  return 0;
}

static const unsigned char brfTable[0X40] = {
  /* 0X20   */ 0,
  /* 0X21 ! */ BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_6,
  /* 0X22 " */ BRL_DOT_5,
  /* 0X23 # */ BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X24 $ */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_4 | BRL_DOT_6,
  /* 0X25 % */ BRL_DOT_1 | BRL_DOT_4 | BRL_DOT_6,
  /* 0X26 & */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_6,
  /* 0X27 ' */ BRL_DOT_3,
  /* 0X28 ( */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X29 ) */ BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X2A * */ BRL_DOT_1 | BRL_DOT_6,
  /* 0X2B + */ BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_6,
  /* 0X2C , */ BRL_DOT_6,
  /* 0X2D - */ BRL_DOT_3 | BRL_DOT_6,
  /* 0X2E . */ BRL_DOT_4 | BRL_DOT_6,
  /* 0X2F / */ BRL_DOT_3 | BRL_DOT_4,
  /* 0X30 0 */ BRL_DOT_3 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X31 1 */ BRL_DOT_2,
  /* 0X32 2 */ BRL_DOT_2 | BRL_DOT_3,
  /* 0X33 3 */ BRL_DOT_2 | BRL_DOT_5,
  /* 0X34 4 */ BRL_DOT_2 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X35 5 */ BRL_DOT_2 | BRL_DOT_6,
  /* 0X36 6 */ BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_5,
  /* 0X37 7 */ BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X38 8 */ BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_6,
  /* 0X39 9 */ BRL_DOT_3 | BRL_DOT_5,
  /* 0X3A : */ BRL_DOT_1 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X3B ; */ BRL_DOT_5 | BRL_DOT_6,
  /* 0X3C < */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_6,
  /* 0X3D = */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X3E > */ BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_5,
  /* 0X3F ? */ BRL_DOT_1 | BRL_DOT_4 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X40 @ */ BRL_DOT_4,
  /* 0X41 A */ BRL_DOT_1,
  /* 0X42 B */ BRL_DOT_1 | BRL_DOT_2,
  /* 0X43 C */ BRL_DOT_1 | BRL_DOT_4,
  /* 0X44 D */ BRL_DOT_1 | BRL_DOT_4 | BRL_DOT_5,
  /* 0X45 E */ BRL_DOT_1 | BRL_DOT_5,
  /* 0X46 F */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_4,
  /* 0X47 G */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_4 | BRL_DOT_5,
  /* 0X48 H */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_5,
  /* 0X49 I */ BRL_DOT_2 | BRL_DOT_4,
  /* 0X4A J */ BRL_DOT_2 | BRL_DOT_4 | BRL_DOT_5,
  /* 0X4B K */  BRL_DOT_1 | BRL_DOT_3,
  /* 0X4C L */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_3,
  /* 0X4D M */ BRL_DOT_1 | BRL_DOT_3 | BRL_DOT_4,
  /* 0X4E N */ BRL_DOT_1 | BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_5,
  /* 0X4F O */ BRL_DOT_1 | BRL_DOT_3 | BRL_DOT_5,
  /* 0X50 P */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_4,
  /* 0X51 Q */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_5,
  /* 0X52 R */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_5,
  /* 0X53 S */ BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_4,
  /* 0X54 T */ BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_5,
  /* 0X55 U */ BRL_DOT_1 | BRL_DOT_3 | BRL_DOT_6,
  /* 0X56 V */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_6,
  /* 0X57 W */ BRL_DOT_2 | BRL_DOT_4 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X58 X */ BRL_DOT_1 | BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_6,
  /* 0X59 Y */ BRL_DOT_1 | BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X5A Z */ BRL_DOT_1 | BRL_DOT_3 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X5B [ */ BRL_DOT_2 | BRL_DOT_4 | BRL_DOT_6,
  /* 0X5C \ */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X5D ] */ BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_4 | BRL_DOT_5 | BRL_DOT_6,
  /* 0X5E ^ */ BRL_DOT_4 | BRL_DOT_5,
  /* 0X5F _ */ BRL_DOT_4 | BRL_DOT_5 | BRL_DOT_6
};

static int
handleExternalResponse_brf (BrailleContractionData *bcd, const char *value) {
  int useDot7 = prefs.capitalizationMode == CTB_CAP_DOT7;

  while (*value && (bcd->output.current < bcd->output.end)) {
    unsigned char brf = *value++ & 0XFF;
    unsigned char dots = 0;
    unsigned char superimpose = 0;

    if ((brf >= 0X60) && (brf <= 0X7F)) {
      brf -= 0X20;
    } else if ((brf >= 0X41) && (brf <= 0X5A)) {
      if (useDot7) superimpose |= BRL_DOT_7;
    }

    if ((brf >= 0X20) && (brf <= 0X5F)) dots = brfTable[brf - 0X20] | superimpose;
    *bcd->output.current++ = dots;
  }

  return 1;
}

static int
handleExternalResponse_consumedLength (BrailleContractionData *bcd, const char *value) {
  int length;

  if (!isInteger(&length, value)) return 0;
  if (length < 1) return 0;
  if (length > getInputCount(bcd)) return 0;

  bcd->input.current = bcd->input.begin + length;
  return 1;
}

static int
handleExternalResponse_outputOffsets (BrailleContractionData *bcd, const char *value) {
  if (bcd->input.offsets) {
    int previous = CTB_NO_OFFSET;
    unsigned int count = getInputCount(bcd);
    unsigned int index = 0;

    while (*value && (index < count)) {
      int offset;

      {
        char *delimiter = strchr(value, ',');

        if (delimiter) {
          int ok;

          {
            char oldDelimiter = *delimiter;
            *delimiter = 0;
            ok = isInteger(&offset, value);
            *delimiter = oldDelimiter;
          }

          if (!ok) return 0;
          value = delimiter + 1;
        } else if (isInteger(&offset, value)) {
          value += strlen(value);
        } else {
          return 0;
        }
      }

      if (offset < ((index == 0)? 0: previous)) return 0;
      if (offset >= getOutputCount(bcd)) return 0;

      bcd->input.offsets[index++] = (offset == previous)? CTB_NO_OFFSET: offset;
      previous = offset;
    }
  }

  return 1;
}

typedef struct {
  const char *name;
  int (*handler) (BrailleContractionData *bcd, const char *value);
  unsigned stop:1;
} ExternalResponseEntry;

static const ExternalResponseEntry externalResponseTable[] = {
  { .name = "brf",
    .stop = 1,
    .handler = handleExternalResponse_brf
  },

  { .name = "consumed-length",
    .handler = handleExternalResponse_consumedLength
  },

  { .name = "output-offsets",
    .handler = handleExternalResponse_outputOffsets
  },

  { .name = NULL }
};

static int
getExternalResponses (BrailleContractionData *bcd) {
  FILE *stream = bcd->table->data.external.standardOutput;

  while (readLine(stream, &bcd->table->data.external.input.buffer, &bcd->table->data.external.input.size, NULL)) {
    int ok = 0;
    int stop = 0;
    char *delimiter = strchr(bcd->table->data.external.input.buffer, '=');

    if (delimiter) {
      const char *value = delimiter + 1;
      const ExternalResponseEntry *rsp = externalResponseTable;

      char oldDelimiter = *delimiter;
      *delimiter = 0;

      while (rsp->name) {
        if (strcmp(bcd->table->data.external.input.buffer, rsp->name) == 0) {
          if (rsp->handler(bcd, value)) ok = 1;
          if (rsp->stop) stop = 1;
          break;
        }

        rsp += 1;
      }

      *delimiter = oldDelimiter;
    }

    if (!ok) logMessage(LOG_WARNING, "unexpected external contraction response: %s: %s", bcd->table->data.external.command, bcd->table->data.external.input.buffer);
    if (stop) return 1;
  }

  logMessage(LOG_WARNING, "incomplete external contraction response: %s", bcd->table->data.external.command);
  return 0;
}

static int
contractText_external (BrailleContractionData *bcd) {
  setOffset(bcd);
  while (++bcd->input.current < bcd->input.end) clearOffset(bcd);

  if (startContractionCommand(bcd->table)) {
    if (putExternalRequests(bcd)) {
      if (getExternalResponses(bcd)) {
        return 1;
      }
    }
  }

  stopContractionCommand(bcd->table);
  return 0;
}

static void
finishCharacterEntry_external (BrailleContractionData *bcd, CharacterEntry *entry) {
}

static const ContractionTableTranslationMethods externalTranslationMethods = {
  .contractText = contractText_external,
  .finishCharacterEntry = finishCharacterEntry_external
};

const ContractionTableTranslationMethods *
getContractionTableTranslationMethods_external (void) {
  return &externalTranslationMethods;
}
