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

#include "program.h"
#include "cmdline.h"
#include "log.h"
#include "file.h"
#include "unicode.h"
#include "utf8.h"
#include "brl_dots.h"
#include "ttb.h"

static char *opt_tablesDirectory;
static char *opt_inputTable;
static char *opt_outputTable;

static int opt_sixDots;
static int opt_noBaseCharacters;

static const char tableName_autoselect[] = "auto";
static const char tableName_unicode[] = "unicode";

BEGIN_OPTION_TABLE(programOptions)
  { .word = "input-table",
    .letter = 'i',
    .argument = strtext("file"),
    .setting.string = &opt_inputTable,
    .internal.setting = tableName_autoselect,
    .description = strtext("Path to input text table.")
  },

  { .word = "output-table",
    .letter = 'o',
    .argument = strtext("file"),
    .setting.string = &opt_outputTable,
    .internal.setting = tableName_unicode,
    .description = strtext("Path to output text table.")
  },

  { .word = "six-dots",
    .letter = '6',
    .setting.flag = &opt_sixDots,
    .description = strtext("Remove dots seven and eight.")
  },

  { .word = "no-base-characters",
    .letter = 'b',
    .setting.flag = &opt_noBaseCharacters,
    .description = strtext("Don't fall back to the Unicode base character.")
  },

  { .word = "tables-directory",
    .letter = 'T',
    .argument = strtext("directory"),
    .setting.string = &opt_tablesDirectory,
    .internal.setting = TABLES_DIRECTORY,
    .internal.adjust = fixInstallPath,
    .description = strtext("Path to directory for text tables.")
  },
END_OPTION_TABLE(programOptions)

static TextTable *inputTable;
static TextTable *outputTable;

static FILE *outputStream;
static const char *outputName;

static unsigned char (*toDots) (wchar_t character);
static wchar_t (*toCharacter) (unsigned char dots);

static unsigned char
toDots_mapped (wchar_t character) {
  return convertCharacterToDots(inputTable, character);
}

static unsigned char
toDots_unicode (wchar_t character) {
  return ((character & UNICODE_ROW_MASK) == UNICODE_BRAILLE_ROW)?
         character & UNICODE_CELL_MASK:
         (BRL_DOT_1 | BRL_DOT_2 | BRL_DOT_3 | BRL_DOT_4 | BRL_DOT_5 | BRL_DOT_6 | BRL_DOT_7 | BRL_DOT_8);
}

static wchar_t
toCharacter_mapped (unsigned char dots) {
  return convertDotsToCharacter(outputTable, dots);
}

static wchar_t
toCharacter_unicode (unsigned char dots) {
  return UNICODE_BRAILLE_ROW | dots;
}

static int
writeCharacter (const wchar_t *character, mbstate_t *state) {
  char bytes[0X1000];
  size_t result = wcrtomb(bytes, (character? *character: WC_C('\0')), state);

  if (result == (size_t)-1) return 0;
  if (!character) result -= 1;

  fwrite(bytes, 1, result, outputStream);
  return !ferror(outputStream);
}

static int
processStream (FILE *inputStream, const char *inputName) {
  mbstate_t inputState;
  memset(&inputState, 0, sizeof(inputState));

  mbstate_t outputState;
  memset(&outputState, 0, sizeof(outputState));

  while (!feof(inputStream)) {
    char inputBuffer[0X1000];
    size_t inputCount = fread(inputBuffer, 1, sizeof(inputBuffer)-1, inputStream);

    if (ferror(inputStream)) goto inputError;
    if (!inputCount) break;
    inputBuffer[inputCount] = 0;

    {
      char *byte = inputBuffer;

      while (inputCount) {
        wchar_t character;

        {
          size_t result = mbrtowc(&character, byte, inputCount, &inputState);

          if (result == (size_t)-2) break;
          if (result == (size_t)-1) goto inputError;
          if (!result) result = 1;

          byte += result;
          inputCount -= result;
        }

        if (!iswcntrl(character)) {
          unsigned char dots = toDots(character);

          if (dots || !iswspace(character)) {
            if (opt_sixDots) dots &= ~(BRL_DOT_7 | BRL_DOT_8);
            character = toCharacter(dots);
          }
        }

        if (!writeCharacter(&character, &outputState)) goto outputError;
      }
    }
  }

  if (!writeCharacter(NULL, &outputState)) goto outputError;
  fflush(outputStream);
  if (ferror(outputStream)) goto outputError;

  if (!mbsinit(&inputState)) {
#ifdef EILSEQ
    errno = EILSEQ;
#else /* EILSEQ */
    errno = EINVAL;
#endif /* EILSEQ */
    goto inputError;
  }

  return 1;

inputError:
  logMessage(LOG_ERR, "input error: %s: %s", inputName, strerror(errno));
  return 0;

outputError:
  logMessage(LOG_ERR, "output error: %s: %s", outputName, strerror(errno));
  return 0;
}

static int
getTable (TextTable **table, const char *name) {
  const char *directory = opt_tablesDirectory;

  *table = NULL;

  if (strcmp(name, tableName_unicode) != 0) {
    char *allocated = NULL;

    if (strcmp(name, tableName_autoselect) == 0) {
      if (!(name = allocated = getTextTableForLocale(directory))) {
        logMessage(LOG_ERR, "cannot find text table for current locale");
      }
    }

    if (name) {
      char *path = makeTextTablePath(directory, name);

      if (path) {
        *table = compileTextTable(path);
        free(path);
      }
    }

    if (allocated) free(allocated);
     if (opt_noBaseCharacters) setTryBaseCharacter(*table, 0);
    if (!*table) return 0;
  }

  return 1;
}

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

  {
    const CommandLineDescriptor descriptor = {
      .options = &programOptions,
      .applicationName = "brltty-trtxt",

      .usage = {
        .purpose = strtext("Translate one binary braille representation to another."),
        .parameters = "[{input-file | -} ...]",
      }
    };

    PROCESS_OPTIONS(descriptor, argc, argv);
  }

  if (getTable(&inputTable, opt_inputTable)) {
    if (getTable(&outputTable, opt_outputTable)) {
      outputStream = stdout;
      outputName = standardOutputName;

      toDots = inputTable? toDots_mapped: toDots_unicode;
      toCharacter = outputTable? toCharacter_mapped: toCharacter_unicode;

      if (argc) {
        do {
          const char *file = argv[0];
          FILE *stream;

          if (strcmp(file, standardStreamArgument) == 0) {
            if (!processStream(stdin, standardInputName)) break;
          } else if ((stream = fopen(file, "r"))) {
            int ok = processStream(stream, file);
            fclose(stream);
            if (!ok) break;
          } else {
            logMessage(LOG_ERR, "cannot open file: %s: %s", file, strerror(errno));
            exitStatus = PROG_EXIT_SEMANTIC;
            break;
          }

          argv += 1, argc -= 1;
        } while (argc);

        if (!argc) exitStatus = PROG_EXIT_SUCCESS;
      } else if (processStream(stdin, standardInputName)) {
        exitStatus = PROG_EXIT_SUCCESS;
      }

      if (outputTable) destroyTextTable(outputTable);
    }

    if (inputTable) destroyTextTable(inputTable);
  }

  return exitStatus;
}
