/*
 * 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 "brl_cmds.h"
#include "brl_custom.h"
#include "cmd.h"
#include "program.h"

const CommandEntry commandTable[] = {
  #include "cmds.auto.h"
  {.name=NULL, .code=EOF, .description=NULL}
};

const CommandModifierEntry commandModifierTable_toggle[] = {
  {.name="on" , .bit=BRL_FLG_TOGGLE_ON },
  {.name="off", .bit=BRL_FLG_TOGGLE_OFF},
  {.name=NULL , .bit=0                 }
};

const CommandModifierEntry commandModifierTable_motion[] = {
  {.name="route", .bit=BRL_FLG_MOTION_ROUTE},
  {.name=NULL   , .bit=0                   }
};

const CommandModifierEntry commandModifierTable_row[] = {
  {.name="scaled", .bit=BRL_FLG_MOTION_SCALED},
  {.name=NULL    , .bit=0                  }
};

const CommandModifierEntry commandModifierTable_vertical[] = {
  {.name="toleft", .bit=BRL_FLG_MOTION_TOLEFT},
  {.name=NULL    , .bit=0                  }
};

const CommandModifierEntry commandModifierTable_input[] = {
  {.name="shift"  , .bit=BRL_FLG_INPUT_SHIFT  },
  {.name="control", .bit=BRL_FLG_INPUT_CONTROL},
  {.name="meta"   , .bit=BRL_FLG_INPUT_META   },
  {.name="altgr"  , .bit=BRL_FLG_INPUT_ALTGR  },
  {.name="gui"    , .bit=BRL_FLG_INPUT_GUI    },
  {.name=NULL     , .bit=0                   }
};

const CommandModifierEntry commandModifierTable_character[] = {
  {.name="upper", .bit=BRL_FLG_INPUT_UPPER},
  {.name=NULL   , .bit=0                 }
};

const CommandModifierEntry commandModifierTable_braille[] = {
  {.name="dot1" , .bit=BRL_DOT1},
  {.name="dot2" , .bit=BRL_DOT2},
  {.name="dot3" , .bit=BRL_DOT3},
  {.name="dot4" , .bit=BRL_DOT4},
  {.name="dot5" , .bit=BRL_DOT5},
  {.name="dot6" , .bit=BRL_DOT6},
  {.name="dot7" , .bit=BRL_DOT7},
  {.name="dot8" , .bit=BRL_DOT8},
  {.name="space", .bit=BRL_DOTC},
  {.name=NULL   , .bit=0       }
};

const CommandModifierEntry commandModifierTable_keyboard[] = {
  {.name="release", .bit=BRL_FLG_KBD_RELEASE},
  {.name="emul0"  , .bit=BRL_FLG_KBD_EMUL0  },
  {.name="emul1"  , .bit=BRL_FLG_KBD_EMUL1  },
  {.name=NULL     , .bit=0                  }
};

static int
compareCommandCodes (const void *element1, const void *element2) {
  const CommandEntry *const *cmd1 = element1;
  const CommandEntry *const *cmd2 = element2;

  if ((*cmd1)->code < (*cmd2)->code) return -1;
  if ((*cmd1)->code > (*cmd2)->code) return 1;
  return 0;
}

int
getCommandCount (void) {
  static int commandCount = -1;

  if (commandCount < 0) {
    const CommandEntry *cmd = commandTable;
    while (cmd->name) cmd += 1;
    commandCount = cmd - commandTable;
  }

  return commandCount;
}

const CommandEntry *
findCommandEntry (int code) {
  static const CommandEntry **commandEntries = NULL;
  int count = getCommandCount();
  code &= BRL_MSK_CMD;

  if (!commandEntries) {
    const CommandEntry **entries = malloc(ARRAY_SIZE(entries, count));

    if (!entries) {
      logMallocError();
      return NULL;
    }

    {
      const CommandEntry *cmd = commandTable;
      const CommandEntry **entry = entries;
      while (cmd->name) *entry++ = cmd++;
    }

    qsort(entries, count, sizeof(*entries), compareCommandCodes);
    commandEntries = entries;
    registerProgramMemory("sorted-command-table", &commandEntries);
  }

  {
    int first = 0;
    int last = count - 1;

    while (first <= last) {
      int current = (first + last) / 2;
      const CommandEntry *cmd = commandEntries[current];

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

    if (last >= 0) {
      const CommandEntry *cmd = commandEntries[last];
      int blk = cmd->code & BRL_MSK_BLK;
      int arg = cmd->code & BRL_MSK_ARG;

      if (blk == (code & BRL_MSK_BLK)) {
        if (arg == (code & BRL_MSK_ARG)) return cmd;

        if (blk) {
          int next = last + 1;

          if (next < count) {
            if (blk != (commandEntries[next]->code & BRL_MSK_BLK)) {
              return cmd;
            }
          }
        }
      }
    }
  }

  return NULL;
}

static
STR_BEGIN_FORMATTER(formatCommandModifiers, int command, const CommandModifierEntry *modifiers)
  const CommandModifierEntry *modifier = modifiers;

  while (modifier->name) {
    if (command & modifier->bit) {
      STR_PRINTF(" + %s", modifier->name);
    }

    modifier += 1;
  }
STR_END_FORMATTER

STR_BEGIN_FORMATTER(describeCommand, int command, CommandDescriptionOption options)
  unsigned int arg = BRL_ARG_GET(command);
  unsigned int arg1 = BRL_CODE_GET(ARG, command);
  unsigned int arg2 = BRL_CODE_GET(EXT, command);
  const CommandEntry *cmd = findCommandEntry(command);

  if (!cmd) {
    STR_PRINTF("%s: %06X", gettext("unknown command"), command);
  } else {
    if (options & CDO_IncludeName) {
      STR_PRINTF("%s: ", cmd->name);
    }

    if (cmd->isToggle && (command & BRL_FLG_TOGGLE_MASK)) {
      const char *text = gettext(cmd->description);
      size_t length = strlen(text);
      char buffer[length + 1];
      char *delimiter;

      strcpy(buffer, text);
      delimiter = strchr(buffer, '/');

      if (delimiter) {
        char *source;
        char *target;

        if (command & BRL_FLG_TOGGLE_ON) {
          target = delimiter;
          if (!(source = strchr(target, ' '))) source = target + strlen(target);
        } else if (command & BRL_FLG_TOGGLE_OFF) {
          {
            char oldDelimiter = *delimiter;
            *delimiter = 0;
            target = strrchr(buffer, ' ');
            *delimiter = oldDelimiter;
          }

          target = target? (target + 1): buffer;
          source = delimiter + 1;
        } else {
          goto toggleReady;
        }

        memmove(target, source, (strlen(source) + 1));
      }

    toggleReady:
      STR_PRINTF("%s", buffer);
    } else {
      STR_PRINTF("%s", gettext(cmd->description));
    }

    if (options & CDO_IncludeOperand) {
      if (cmd->isCharacter) {
        STR_PRINTF(" [U+%04X]", arg);
      }

      if (cmd->isBraille) {
        int none = 1;
        STR_PRINTF(" [");

        {
          static const int dots[] = {
            BRL_DOTC,
            BRL_DOT1, BRL_DOT2, BRL_DOT3, BRL_DOT4,
            BRL_DOT5, BRL_DOT6, BRL_DOT7, BRL_DOT8,
            0
          };
          const int *dot = dots;

          while (*dot) {
            if (command & *dot) {
              none = 0;

              if (dot == dots) {
                STR_PRINTF("C");
              } else {
                unsigned int number = dot - dots;
                STR_PRINTF("%u", number);
              }
            }

            dot += 1;
          }
        }

        if (none) STR_PRINTF("%s", gettext("space"));
        STR_PRINTF("]");
      }

      if (cmd->isKeyboard) {
        STR_PRINTF(" [\\X%02X]", arg1);
      }

      if (cmd->isColumn && !cmd->isRouting && (
           (arg == BRL_MSK_ARG) /* key event processing */
         ||
           ((options & CDO_DefaultOperand) && !arg) /* key table listing */
         )) {
        STR_PRINTF(" %s", gettext("at cursor"));
      } else if (cmd->isColumn || cmd->isRow || cmd->isOffset) {
        STR_PRINTF(" #%u", arg - (cmd->code & BRL_MSK_ARG) + 1);
      } else if (cmd->isRange) {
        STR_PRINTF(" #%u-%u", arg1, arg2);
      }

      if (cmd->isInput) {
        STR_FORMAT(formatCommandModifiers, command, commandModifierTable_input);
      }

      if (cmd->isCharacter) {
        STR_FORMAT(formatCommandModifiers, command, commandModifierTable_character);
      }

      if (cmd->isKeyboard) {
        STR_FORMAT(formatCommandModifiers, command, commandModifierTable_keyboard);
      }
    }

    if (cmd->isMotion) {
      STR_FORMAT(formatCommandModifiers, command, commandModifierTable_motion);
    }

    if (cmd->isRow) {
      STR_FORMAT(formatCommandModifiers, command, commandModifierTable_row);
    }

    if (cmd->isVertical) {
      STR_FORMAT(formatCommandModifiers, command, commandModifierTable_vertical);
    }
  }
STR_END_FORMATTER

static
STR_BEGIN_FORMATTER(formatCommand, int command)
  STR_PRINTF("%06X (", command);
  STR_FORMAT(describeCommand, command, CDO_IncludeName | CDO_IncludeOperand);
  STR_PRINTF(")");
STR_END_FORMATTER

typedef struct {
  int command;
} LogCommandData;

static
STR_BEGIN_FORMATTER(formatLogCommandData, const void *data)
  const LogCommandData *cmd = data;

  STR_PRINTF("command: ");
  STR_FORMAT(formatCommand, cmd->command);
STR_END_FORMATTER

void
logCommand (int command) {
  const LogCommandData cmd = {
    .command = command
  };

  logData(LOG_DEBUG, formatLogCommandData, &cmd);
}

typedef struct {
  int oldCommand;
  int newCommand;
} LogTransformedCommandData;

static
STR_BEGIN_FORMATTER(formatLogTransformedCommandData, const void *data)
  const LogTransformedCommandData *cmd = data;

  STR_PRINTF("command: ");
  STR_FORMAT(formatCommand, cmd->oldCommand);

  STR_PRINTF(" -> ");
  STR_FORMAT(formatCommand, cmd->newCommand);
STR_END_FORMATTER

void
logTransformedCommand (int oldCommand, int newCommand) {
  const LogTransformedCommandData cmd = {
    .oldCommand = oldCommand,
    .newCommand = newCommand
  };

  logData(LOG_DEBUG, formatLogTransformedCommandData, &cmd);
}
