/*
 * 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-2021 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 "strfmt.h"
#include "utf8.h"
#include "cmd.h"
#include "brl_cmds.h"
#include "ktb.h"
#include "ktb_list.h"
#include "ktb_cmds.h"

struct CommandGroupHookDataStruct {
  ListGenerationData *const lgd;
  const KeyContext *const ctx;

  int ok;
};

static inline void
listCommandSubgroup (
  int (*list) (ListGenerationData *lgd, const KeyContext *ctx),
  CommandGroupHookData *cgh
) {
  cgh->ok = list(cgh->lgd, cgh->ctx);
}

static int
handleCommandGroupHook (CommandGroupHook *handler, CommandGroupHookData *cgh) {
  if (!handler) return 1;
  cgh->ok = 0;
  handler(cgh);
  return cgh->ok;
}

static void *
getMethodData (ListGenerationData *lgd) {
  return lgd->list.internal? lgd: lgd->list.data;
}

static int
writeHeader (ListGenerationData *lgd, const wchar_t *text, int level) {
  return lgd->list.methods->writeHeader(text, level, getMethodData(lgd));
}

static int
writeLine (ListGenerationData *lgd, const wchar_t *line) {
  return lgd->list.writeLine(line, lgd->list.data);
}

static int
writeBlankLine (ListGenerationData *lgd) {
  return writeLine(lgd, WS_C(""));
}

static int
addCharacters (ListGenerationData *lgd, const wchar_t *characters, size_t count) {
  size_t newLength = lgd->line.length + count;

  if (newLength > lgd->line.size) {
    size_t newSize = (newLength | 0X3F) + 1;
    wchar_t *newCharacters = realloc(lgd->line.characters, ARRAY_SIZE(newCharacters, newSize));

    if (!newCharacters) {
      logSystemError("realloc");
      return 0;
    }

    lgd->line.characters = newCharacters;
    lgd->line.size = newSize;
  }

  wmemcpy(&lgd->line.characters[lgd->line.length], characters, count);
  lgd->line.length = newLength;
  return 1;
}

static int
putCharacters (ListGenerationData *lgd, const wchar_t *characters, size_t count) {
  if (lgd->line.length == 0) {
    if (lgd->list.elementLevel > 0) {
      const unsigned int indent = 2;
      const unsigned int count = indent * lgd->list.elementLevel;
      wchar_t characters[count];

      wmemset(characters, WC_C(' '), count);
      characters[count - indent] = lgd->list.elementBullet;
      lgd->list.elementBullet = WC_C(' ');

      if (!addCharacters(lgd, characters, count)) return 0;
    }
  }

  return addCharacters(lgd, characters, count);
}

static int
putCharacter (ListGenerationData *lgd, wchar_t character) {
  return putCharacters(lgd, &character, 1);
}

static int
putCharacterString (ListGenerationData *lgd, const wchar_t *string) {
  return putCharacters(lgd, string, wcslen(string));
}

static int
putUtf8String (ListGenerationData *lgd, const char *string) {
  size_t size = strlen(string) + 1;
  wchar_t characters[size];
  wchar_t *character = characters;

  convertUtf8ToWchars(&string, &character, size);
  return putCharacters(lgd, characters, character-characters);
}

static void
clearLine (ListGenerationData *lgd) {
  lgd->line.length = 0;
}

static void
trimLine (ListGenerationData *lgd) {
  while (lgd->line.length > 0) {
    size_t last = lgd->line.length - 1;

    if (lgd->line.characters[last] != WC_C(' ')) break;
    lgd->line.length = last;
  }
}

static int
finishLine (ListGenerationData *lgd) {
  trimLine(lgd);
  if (!putCharacter(lgd, 0)) return 0;
  return 1;
}

static int
endLine (ListGenerationData *lgd) {
  if (lgd->topicHeader) {
    if (!writeHeader(lgd, lgd->topicHeader, 1)) return 0;
    lgd->topicHeader = NULL;
  }

  if (lgd->listHeader) {
    const char *string = lgd->listHeader;
    lgd->listHeader = NULL;

    size_t size = strlen(string) + 1;
    wchar_t characters[size];
    wchar_t *character = characters;

    convertUtf8ToWchars(&string, &character, size);
    if (!writeHeader(lgd, characters, 2)) return 0;
  }

  if (!finishLine(lgd)) return 0;
  if (!writeLine(lgd, lgd->line.characters)) return 0;
  clearLine(lgd);
  return 1;
}

static int
beginList (ListGenerationData *lgd, const char *header) {
  lgd->listHeader = header;
  return 1;
}

static int
endList (ListGenerationData *lgd) {
  return lgd->list.methods->endList(getMethodData(lgd));
}

static int
beginElement (ListGenerationData *lgd, unsigned int level) {
  return lgd->list.methods->beginElement(level, getMethodData(lgd));
}

static int
searchKeyNameEntry (const void *target, const void *element) {
  const KeyValue *value = target;
  const KeyNameEntry *const *kne = element;
  return compareKeyValues(value, &(*kne)->value);
}

const KeyNameEntry *
findKeyNameEntry (KeyTable *table, const KeyValue *value) {
  const KeyNameEntry *const *array = table->keyNames.table;
  unsigned int count = table->keyNames.count;

  const KeyNameEntry *const *kne = bsearch(value, array, count, sizeof(*array), searchKeyNameEntry);
  if (!kne) return NULL;

  while (kne > array) {
    if (compareKeyValues(value, &(*--kne)->value) != 0) {
      kne += 1;
      break;
    }
  }

  return *kne;
}

STR_BEGIN_FORMATTER(formatKeyName, KeyTable *table, const KeyValue *value)
  const KeyNameEntry *kne = findKeyNameEntry(table, value);

  if (kne) {
    STR_PRINTF("%s", kne->name);
  } else if (value->number != KTB_KEY_ANY) {
    const KeyValue anyKey = {
      .group = value->group,
      .number = KTB_KEY_ANY
    };

    if ((kne = findKeyNameEntry(table, &anyKey))) {
      STR_PRINTF("%s.%u", kne->name, value->number+1);
    }
  }

  if (STR_LENGTH == 0) STR_PRINTF("?");
STR_END_FORMATTER

static int
putKeyName (ListGenerationData *lgd, const KeyValue *value) {
  char string[0X100];
  formatKeyName(string, sizeof(string), lgd->keyTable, value);
  return putUtf8String(lgd, string);
}

STR_BEGIN_FORMATTER(formatKeyCombination, KeyTable *table, const KeyCombination *combination)
  char keyDelimiter = 0;
  unsigned char dotCount = 0;

  const char *dotPrefix = "dot";
  const size_t dotPrefixLength = strlen(dotPrefix);
  const size_t dotNameLength = dotPrefixLength + 1;

  for (unsigned char index=0; index<combination->modifierCount; index+=1) {
    char keyName[0X100];
    formatKeyName(keyName, sizeof(keyName), table,
                  &combination->modifierKeys[combination->modifierPositions[index]]);

    if (strlen(keyName) == dotNameLength) {
      if (strncasecmp(keyName, dotPrefix, dotPrefixLength) == 0) {
        char dotNumber = keyName[dotPrefixLength];

        if ((dotNumber >= '1') && (dotNumber <= '8')) {
          if (++dotCount == 1) goto FIRST_DOT;

          if (dotCount == 2) {
            char firstDot = *STR_POP();
            STR_PRINTF("s%c", firstDot);
          }

          STR_PRINTF("%c", dotNumber);
          continue;
        }
      }
    }

    dotCount = 0;
  FIRST_DOT:

    if (keyDelimiter) {
      STR_PRINTF("%c", keyDelimiter);
    } else {
      keyDelimiter = '+';
    }

    STR_PRINTF("%s", keyName);
  }

  if (combination->flags & KCF_IMMEDIATE_KEY) {
    if (keyDelimiter) STR_PRINTF("%c", keyDelimiter);
    STR_FORMAT(formatKeyName, table, &combination->immediateKey);
  }
STR_END_FORMATTER

static int
putKeyCombination (ListGenerationData *lgd, const KeyCombination *combination) {
  char string[0X100];
  formatKeyCombination(string, sizeof(string), lgd->keyTable, combination);
  return putUtf8String(lgd, string);
}

static int
putCommandDescription (ListGenerationData *lgd, const BoundCommand *cmd, int details) {
  char description[0X60];

  describeCommand(description, sizeof(description), cmd->value,
                  (details? (CDO_IncludeOperand | CDO_DefaultOperand): 0));
  return putUtf8String(lgd, description);
}

static int
putKeyboardFunction (ListGenerationData *lgd, const KeyboardFunction *kbf) {
  if (!beginElement(lgd, 1)) return 0;
  if (!putCharacterString(lgd, WS_C("braille keyboard "))) return 0;
  if (!putUtf8String(lgd, kbf->name)) return 0;
  if (!putCharacterString(lgd, WS_C(": "))) return 0;
  return 1;
}

static int
listKeyboardFunctions (ListGenerationData *lgd, const KeyContext *ctx) {
  if (ctx->mappedKeys.count > 0) {
    for (unsigned int index=0; index<ctx->mappedKeys.count; index+=1) {
      const MappedKeyEntry *map = &ctx->mappedKeys.table[index];

      if (!(map->flags & MKF_HIDDEN)) {
        const KeyboardFunction *kbf = map->keyboardFunction;

        if (!putKeyboardFunction(lgd, kbf)) return 0;
        if (!putKeyName(lgd, &map->keyValue)) return 0;
        if (!endLine(lgd)) return 0;
      }
    }

    {
      const KeyboardFunction *kbf = keyboardFunctionTable;
      const KeyboardFunction *end = kbf + keyboardFunctionCount;

      while (kbf < end) {
        if (ctx->mappedKeys.superimpose & kbf->bit) {
          if (!putKeyboardFunction(lgd, kbf)) return 0;
          if (!putCharacterString(lgd, WS_C("superimposed"))) return 0;
          if (!endLine(lgd)) return 0;
        }

        kbf += 1;
      }
    }
  }

  return 1;
}

void
commandGroupHook_keyboardFunctions (CommandGroupHookData *cgh) {
  listCommandSubgroup(listKeyboardFunctions, cgh);
}

static int
listHotkeyEvent (ListGenerationData *lgd, const KeyValue *keyValue, const char *event, const BoundCommand *cmd) {
  if (cmd->value != BRL_CMD_NOOP) {
    if (!beginElement(lgd, 1)) return 0;

    if ((cmd->value & BRL_MSK_BLK) == BRL_CMD_BLK(CONTEXT)) {
      const KeyContext *ctx = getKeyContext(lgd->keyTable, (KTB_CTX_DEFAULT + (cmd->value & BRL_MSK_ARG)));
      if (!ctx) return 0;
      if (!putUtf8String(lgd, "switch to ")) return 0;
      if (!putCharacterString(lgd, ctx->title)) return 0;
    } else {
      if (!putCommandDescription(lgd, cmd, (keyValue->number != KTB_KEY_ANY))) return 0;
    }

    if (!putCharacterString(lgd, WS_C(": "))) return 0;
    if (!putUtf8String(lgd, event)) return 0;
    if (!putCharacter(lgd, WC_C(' '))) return 0;
    if (!putKeyName(lgd, keyValue)) return 0;
    if (!endLine(lgd)) return 0;
  }

  return 1;
}

static int
listHotkeys (ListGenerationData *lgd, const KeyContext *ctx) {
  const HotkeyEntry *hotkey = ctx->hotkeys.table;
  unsigned int count = ctx->hotkeys.count;

  while (count) {
    if (!(hotkey->flags & HKF_HIDDEN)) {
      if (!listHotkeyEvent(lgd, &hotkey->keyValue, "press", &hotkey->pressCommand)) return 0;
      if (!listHotkeyEvent(lgd, &hotkey->keyValue, "release", &hotkey->releaseCommand)) return 0;
    }

    hotkey += 1, count -= 1;
  }

  return 1;
}

void
commandGroupHook_hotkeys (CommandGroupHookData *cgh) {
  listCommandSubgroup(listHotkeys, cgh);
}

static int
saveBindingLine (
  ListGenerationData *lgd, size_t keysOffset,
  const BoundCommand *command, const KeyBinding *binding
) {
  if (lgd->binding.count == lgd->binding.size) {
    size_t newSize = lgd->binding.size? (lgd->binding.size << 1): 0X10;
    BindingLine **newLines = realloc(lgd->binding.lines, ARRAY_SIZE(newLines, newSize));

    if (!newLines) {
      logMallocError();
      return 0;
    }

    lgd->binding.lines = newLines;

    while (lgd->binding.size < newSize) {
      lgd->binding.lines[lgd->binding.size++] = NULL;
    }
  }

  {
    BindingLine *line;
    size_t size = sizeof(*line) + (sizeof(line->text[0]) * lgd->line.length);

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

    line->command = command;
    line->keyCombination = &binding->keyCombination;
    line->keysOffset = keysOffset;
    wmemcpy(line->text, lgd->line.characters, (line->length = lgd->line.length));
    lgd->binding.lines[lgd->binding.count++] = line;
  }

  clearLine(lgd);
  return 1;
}

static void
removeBindingLine (ListGenerationData *lgd, int index) {
  {
    BindingLine *bl = lgd->binding.lines[index];

    free(bl);
  }

  if (--lgd->binding.count > index) {
    memmove(&lgd->binding.lines[index], &lgd->binding.lines[index+1],
            ((lgd->binding.count - index) * sizeof(*lgd->binding.lines)));
  }
}

static void
removeBindingLines (ListGenerationData *lgd) {
  size_t *count = &lgd->binding.count;

  while (*count > 0) removeBindingLine(lgd, (*count - 1));
}

static int
sortBindingLines (const void *element1, const void *element2) {
  const BindingLine *const *line1 = element1;
  const BindingLine *const *line2 = element2;

  int command1 = (*line1)->command->value;
  int command2 = (*line2)->command->value;

  int cmd1 = command1 & BRL_MSK_CMD;
  int cmd2 = command2 & BRL_MSK_CMD;
  if (cmd1 < cmd2) return -1;
  if (cmd1 > cmd2) return 1;

  if (command1 < command2) return -1;
  if (command1 > command2) return 1;

  const KeyCombination *combination1 = (*line1)->keyCombination;
  const KeyCombination *combination2 = (*line2)->keyCombination;
  if (combination1->anyKeyCount < combination2->anyKeyCount) return -1;
  if (combination1->anyKeyCount > combination2->anyKeyCount) return 1;

  if (combination1 < combination2) return -1;
  if (combination1 > combination2) return 1;
  return 0;
}

static int
listBindingLine (ListGenerationData *lgd, int index, int *isSame) {
  const BindingLine *bl = lgd->binding.lines[index];
  int asList = *isSame;

  if (*isSame) {
    *isSame = 0;
  } else {
    if (!beginElement(lgd, 1)) return 0;
    if (!putCharacters(lgd, bl->text, bl->keysOffset)) return 0;
  }

  {
    int next = index + 1;

    if (next < lgd->binding.count) {
      const BindingLine *nl = lgd->binding.lines[next];

      if (bl->command->value == nl->command->value) {
        if (bl->keyCombination->anyKeyCount == nl->keyCombination->anyKeyCount) {
          if (!asList) {
            if (!endLine(lgd)) return 0;
          }

          asList = 1;
          *isSame = 1;
        }
      }
    }
  }

  if (asList) {
    if (!beginElement(lgd, 2)) return 0;
  }

  if (!putCharacters(lgd, &bl->text[bl->keysOffset], (bl->length - bl->keysOffset))) return 0;
  if (!endLine(lgd)) return 0;

  removeBindingLine(lgd, index);
  return 1;
}

static int
listBindingLines (ListGenerationData *lgd, const KeyContext *ctx) {
  if (lgd->binding.count > 0) {
    qsort(lgd->binding.lines, lgd->binding.count,
          sizeof(*lgd->binding.lines), sortBindingLines);

    {
      const CommandGroupEntry *grp = commandGroupTable;
      const CommandGroupEntry *grpEnd = grp + commandGroupCount;

      while (grp < grpEnd) {
        const CommandListEntry *cmd = grp->commands.table;
        const CommandListEntry *cmdEnd = cmd + grp->commands.count;

        CommandGroupHookData cgh = {
          .lgd = lgd,
          .ctx = ctx
        };

        if (!beginList(lgd, grp->name)) return 0;
        if (!handleCommandGroupHook(grp->before, &cgh)) return 0;

        while (cmd < cmdEnd) {
          int first = 0;
          int last = lgd->binding.count - 1;

          while (first <= last) {
            int current = (first + last) / 2;
            const BindingLine *bl = lgd->binding.lines[current];
            int command = bl->command->value & BRL_MSK_CMD;

            if (command < cmd->code) {
              first = current + 1;
            } else {
              last = current - 1;
            }
          }

          int isSame = 0;
          int current = cmd->code;

          // Binding lines are removed as they're processed, so, even though
          // first isn't being incremented, the count will be decremented.
          while (first < lgd->binding.count) {
            const BindingLine *bl = lgd->binding.lines[first];
            int next = bl->command->value;

            if ((next & BRL_MSK_CMD) != current) {
              int blk = next & BRL_MSK_BLK;
              if (!blk) break;
              if (blk != (current & BRL_MSK_BLK)) break;
            }

            if (!listBindingLine(lgd, first, &isSame)) return 0;
          }

          cmd += 1;
        }

        if (!handleCommandGroupHook(grp->after, &cgh)) return 0;
        if (!endList(lgd)) return 0;
        grp += 1;
      }

      {
        int isSame = 0;

        if (!beginList(lgd, "Uncategorized Bindings")) return 0;

        while (lgd->binding.count > 0) {
          if (!listBindingLine(lgd, 0, &isSame)) return 0;
        }

        if (!endList(lgd)) return 0;
      }
    }
  }

  return 1;
}

static int listKeyBindings (ListGenerationData *lgd, const KeyContext *ctx, const wchar_t *keysPrefix);

static int
listKeyBinding (ListGenerationData *lgd, const KeyBinding *binding, int longPress, const wchar_t *keysPrefix) {
  const BoundCommand *cmd = longPress? &binding->secondaryCommand: &binding->primaryCommand;

  if (cmd->value == BRL_CMD_NOOP) return 1;

  if (!putCommandDescription(lgd, cmd, !binding->keyCombination.anyKeyCount)) return 0;
  if (!putCharacterString(lgd, WS_C(": "))) return 0;
  size_t keysOffset = lgd->line.length;

  if (keysPrefix) {
    if (!putCharacterString(lgd, keysPrefix)) return 0;
    if (!putCharacterString(lgd, WS_C(", "))) return 0;
  }

  if (longPress) {
    if (!putCharacterString(lgd, WS_C("long "))) return 0;
  }

  if (!putKeyCombination(lgd, &binding->keyCombination)) return 0;

  if ((cmd->value & BRL_MSK_BLK) == BRL_CMD_BLK(CONTEXT)) {
    const KeyContext *ctx = getKeyContext(lgd->keyTable, (KTB_CTX_DEFAULT + (cmd->value & BRL_MSK_ARG)));
    if (!ctx) return 0;

    {
      size_t length = lgd->line.length - keysOffset;
      wchar_t keys[length + 1];

      wmemcpy(keys, &lgd->line.characters[keysOffset], length);
      keys[length] = 0;
      clearLine(lgd);

      if (isTemporaryKeyContext(lgd->keyTable, ctx)) {
        if (!listKeyBindings(lgd, ctx, keys)) return 0;
      } else {
        if (!putCharacterString(lgd, WS_C("switch to "))) return 0;
        if (!putCharacterString(lgd, ctx->title)) return 0;
        if (!putCharacterString(lgd, WS_C(": "))) return 0;
        keysOffset = lgd->line.length;
        if (!putCharacterString(lgd, keys)) return 0;
        if (!saveBindingLine(lgd, keysOffset, cmd, binding)) return 0;
      }
    }
  } else {
    if (!saveBindingLine(lgd, keysOffset, cmd, binding)) return 0;
  }

  return 1;
}

static int
listKeyBindings (ListGenerationData *lgd, const KeyContext *ctx, const wchar_t *keysPrefix) {
  const KeyBinding *binding = ctx->keyBindings.table;
  unsigned int count = ctx->keyBindings.count;

  while (count) {
    if (!(binding->flags & KBF_HIDDEN)) {
      if (!listKeyBinding(lgd, binding, 0, keysPrefix)) return 0;
      if (!listKeyBinding(lgd, binding, 1, keysPrefix)) return 0;
    }

    binding += 1, count -= 1;
  }

  return 1;
}

static int
listKeyContext (ListGenerationData *lgd, const KeyContext *ctx) {
  lgd->topicHeader = ctx->title;
  if (!listKeyBindings(lgd, ctx, NULL)) return 0;
  if (!listBindingLines(lgd, ctx)) return 0;
  return 1;
}

static int
listSpecialKeyContexts (ListGenerationData *lgd) {
  static const unsigned char contexts[] = {
    KTB_CTX_DEFAULT,
    KTB_CTX_MENU
  };

  const unsigned char *context = contexts;
  unsigned int count = ARRAY_COUNT(contexts);

  while (count) {
    const KeyContext *ctx = getKeyContext(lgd->keyTable, *context);

    if (ctx) {
      if (!listKeyContext(lgd, ctx)) return 0;
    }

    context += 1, count -= 1;
  }

  return 1;
}

static int
listPersistentKeyContexts (ListGenerationData *lgd) {
  for (unsigned int context=KTB_CTX_DEFAULT+1; context<lgd->keyTable->keyContexts.count; context+=1) {
    const KeyContext *ctx = getKeyContext(lgd->keyTable, context);

    if (ctx && !isTemporaryKeyContext(lgd->keyTable, ctx)) {
      if (!listKeyContext(lgd, ctx)) return 0;
    }
  }

  return 1;
}

static int
listKeyTableTitle (ListGenerationData *lgd) {
  if (!putUtf8String(lgd, gettext("Key Table"))) return 0;

  if (lgd->keyTable->title) {
    if (!putCharacterString(lgd, WS_C(": "))) return 0;
    if (!putCharacterString(lgd, lgd->keyTable->title)) return 0;

    if (!finishLine(lgd)) return 0;
    if (!writeHeader(lgd, lgd->line.characters, 0)) return 0;
    clearLine(lgd);
  }

  return 1;
}

static int
listKeyTableNotes (ListGenerationData *lgd) {
  unsigned int noteIndex;

  if (!beginList(lgd, "Notes")) return 0;

  for (noteIndex=0; noteIndex<lgd->keyTable->notes.count; noteIndex+=1) {
    const wchar_t *line = lgd->keyTable->notes.table[noteIndex];
    unsigned int level;
    int prefixed;

    if (*line == WC_C('*')) {
      level = 0;
      prefixed = 1;
    } else if (*line == WC_C('+')) {
      level = 2;
      prefixed = 1;
    } else {
      level = 1;
      prefixed = 0;
    }

    if (level > 0) {
      if (!beginElement(lgd, level)) return 0;
    }

    if (prefixed) {
      line += 1;
      while (iswspace(*line)) line += 1;
    }

    if (!putCharacterString(lgd, line)) return 0;
    if (!endLine(lgd)) return 0;
  }

  if (!endList(lgd)) return 0;
  return 1;
}

static int
listKeyTableSections (ListGenerationData *lgd) {
  typedef int Lister (ListGenerationData *lgd);

  static Lister *const listerTable[] = {
    listKeyTableTitle,
    listKeyTableNotes,
    listSpecialKeyContexts,
    listPersistentKeyContexts
  };

  Lister *const *lister = listerTable;
  Lister *const *end = lister + ARRAY_COUNT(listerTable);

  while (lister < end) {
    if (!(*lister)(lgd)) return 0;
    lister += 1;
  }

  return 1;
}

static int
internalWriteHeader (const wchar_t *text, unsigned int level, void *data) {
  static const wchar_t characters[] = {WC_C('='), WC_C('-')};
  ListGenerationData *lgd = data;

  if (!writeLine(lgd, text)) return 0;

  if (level < ARRAY_COUNT(characters)) {
    size_t length = wcslen(text);
    wchar_t underline[length + 1];

    wmemset(underline, characters[level], length);
    underline[length] = 0;

    if (!writeLine(lgd, underline)) return 0;
    if (!writeBlankLine(lgd)) return 0;
  }

  return 1;
}

static int
internalBeginElement (unsigned int level, void *data) {
  ListGenerationData *lgd = data;

  static const wchar_t bullets[] = {
    WC_C('*'),
    WC_C('+'),
    WC_C('-')
  };

  lgd->list.elementLevel = level;
  lgd->list.elementBullet = bullets[level - 1];
  return 1;
}

static int
internalEndList (void *data) {
  ListGenerationData *lgd = data;

  if (lgd->list.elementLevel > 0) {
    lgd->list.elementLevel = 0;
    lgd->listHeader = NULL;
    if (!writeBlankLine(lgd)) return 0;
  }

  return 1;
}

int
listKeyTable (KeyTable *table, const KeyTableListMethods *methods, KeyTableWriteLineMethod *writeLine, void *data) {
  static const KeyTableListMethods internalMethods = {
    .writeHeader = internalWriteHeader,
    .beginElement = internalBeginElement,
    .endList = internalEndList
  };

  ListGenerationData lgd = {
    .keyTable = table,

    .topicHeader = NULL,
    .listHeader = NULL,

    .line = {
      .characters = NULL,
      .size = 0,
      .length = 0,
    },

    .list = {
      .methods = methods? methods: &internalMethods,
      .writeLine = writeLine,
      .data = data,
      .internal = !methods,

      .elementLevel = 0
    },

    .binding = {
      .lines = NULL,
      .size = 0,
      .count = 0
    }
  };

  int result = listKeyTableSections(&lgd);

  if (lgd.binding.lines) {
    removeBindingLines(&lgd);
    free(lgd.binding.lines);
  }

  if (lgd.line.characters) free(lgd.line.characters);
  return result;
}

int
forEachKeyName (KEY_NAME_TABLES_REFERENCE keys, KeyNameEntryHandler *handleKeyNameEntry, void *data) {
  const KeyNameEntry *const *knt = keys;

  while (*knt) {
    const KeyNameEntry *kne = *knt;

    if (knt != keys) {
      if (!handleKeyNameEntry(NULL, data)) {
        return 0;
      }
    }

    while (kne->name) {
      if (!handleKeyNameEntry(kne, data)) return 0;
      kne += 1;
    }

    knt += 1;
  }

  return 1;
}

typedef struct {
  KeyTableWriteLineMethod *const writeLine;
  void *const data;
} ListKeyNameData;

static int
listKeyName (const KeyNameEntry *kne, void *data) {
  const ListKeyNameData *lkn = data;
  const char *name = kne? kne->name: "";
  size_t size = strlen(name) + 1;
  wchar_t characters[size];
  wchar_t *character = characters;

  convertUtf8ToWchars(&name, &character, size);
  return lkn->writeLine(characters, lkn->data);
}

int
listKeyNames (KEY_NAME_TABLES_REFERENCE keys, KeyTableWriteLineMethod *writeLine, void *data) {
  ListKeyNameData lkn = {
    .writeLine = writeLine,
    .data = data
  };

  return forEachKeyName(keys, listKeyName, &lkn);
}
