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

/* Color library test program
 *
 * This program tests the color conversion and description library (color.c/color.h).
 * It includes tests for:
 * - VGA color palette round-trip conversion (RGB -> VGA -> RGB)
 * - HSV color space conversions (RGB <-> HSV)
 * - Human-readable color descriptions with HSV analysis
 * - RGB to VGA nearest-color mapping
 *
 * Test Color References:
 * The test suite uses standard CSS/HTML Named Colors from the W3C CSS Color Module
 * Level 3 specification (https://www.w3.org/TR/css-color-3/#svg-color). These are
 * well-established color standards used across web browsers, design tools, and the
 * X11 color system. Using standard colors ensures our detection algorithms work
 * with commonly recognized color names.
 *
 * Some tests include alternate acceptable descriptions for edge cases where multiple
 * valid color names exist (e.g., pure green can be described as both "vivid green"
 * and "lime" depending on the detection criteria used).
 */

#include "prologue.h"

#include <string.h>
#include <ctype.h>

#include "log.h"
#include "strfmt.h"
#include "cmdline.h"
#include "cmdput.h"
#include "cmdargs.h"
#include "color.h"
#include "color_internal.h"
#include "parse.h"
#include "file.h"

typedef enum {
   OPTQ_INFO,
   OPTQ_PASS,
   OPTQ_WARN,
   OPTQ_FAIL,
   OPTQ_SUMMARY,
   OPTQ_TEST,
} OptQuietness;

static int opt_quietness;
static int opt_performAllTests;

static int opt_testVGAtoRGBtoVGA;
static int opt_listVGAColors;
static int opt_testRGBtoHSVtoRGB;
static int opt_showRGBtoVGA;

BEGIN_COMMAND_LINE_OPTIONS(programOptions)
  { .word = "quieter",
    .letter = 'q',
    .setting.flag = &opt_quietness,
    .flags = OPT_Extend,
    .description = "reduce verbosity - this option is cumulative",
  },

  { .word = "all-tests",
    .letter = 'a',
    .setting.flag = &opt_performAllTests,
    .description = "perform all of the tests - conflicts with requesting specific tests",
  },

  { .word = "vga-roundtrip",
    .letter = 'v',
    .setting.flag = &opt_testVGAtoRGBtoVGA,
    .description = "test the VGA to RGB to VGA round-trip - conflicts with requesting all tests",
  },

  { .word = "vga-colors",
    .letter = 'l',
    .setting.flag = &opt_listVGAColors,
    .description = "list all of the VGA colors - conflicts with requesting all tests",
  },

  { .word = "rgb-roundtrip",
    .letter = 'r',
    .setting.flag = &opt_testRGBtoHSVtoRGB,
    .description = "test the RGB to HSV to RGB round-trip - conflicts with requesting all tests",
  },

  { .word = "vga-mappings",
    .letter = 'm',
    .setting.flag = &opt_showRGBtoVGA,
    .description = "show some RGB to nearest VGA mappings - conflicts with requesting all tests",
  },
END_COMMAND_LINE_OPTIONS(programOptions)

static const char *specifiedCommand;

BEGIN_COMMAND_LINE_PARAMETERS(programParameters)
  { .name = "command",
    .description = "the name of the command to execute or of the color model to evaluate",
    .setting = &specifiedCommand,
    .optional = 1,
  },
END_COMMAND_LINE_PARAMETERS(programParameters)

BEGIN_COMMAND_LINE_NOTES(programNotes)
  "The -a option may not be combined with any option that requests a specific test.",
  "Specifying a command conflicts with requesting that any tests be performed.",
  "If none of the tests are requested, and if a command isn't specified, then interactive mode is entered.",
  "",
  "The -q option is cumulative.",
  "Output verbosity is increasingly reduced, each time it's specified, as follows:",
  "  1: Informational messages and initial interactive mode help.",
  "  2: Test objectives and results that pass.",
  "  3: Test results that pass but with a qualification.",
  "  4: Test results that fail.",
  "  5: Test summaries.",
  "  6: Test headers, test status, and color model syntax (interactive mode).",
  "",
  "Command names aren't case-sensitive and may be abbreviated.",
  "The recognized commands are:",
  "  brightness [percent]",
  "  colors [name]",
  "  grayscale [percent]",
  "  hue [degrees]",
  "  options [[no]option ...]",
  "  problems",
  "  saturation [percent]",
  "",
  "Color model names aren't case-sensitive and may be abbreviated.",
  "The supported color models are:",
  "  ANSI  the ANSI terminal 256-color model",
  "  HLS   the Hue Lightness Saturation model",
  "  HSV   the Hue Saturation Value (brightness) model",
  "  RGB   the Red Green Blue model",
  "  VGA   the Video Graphics Array 16-color model",
  "",
  "The return codes of this command are:",
  "  0  Successful.",
  "  2  A syntax error was encountered.",
  "  3  A test failed, a problem was found, etc.",
  "  4  A system error occurred.",
END_COMMAND_LINE_NOTES

BEGIN_COMMAND_LINE_DESCRIPTOR(programDescriptor)
  .name = "colortest",
  .purpose = "Test the color conversion and name functions.",

  .options = &programOptions,
  .parameters = &programParameters,
  .notes = COMMAND_LINE_NOTES(programNotes),

  .extraParameters = {
    .name = "arg",
    .description = "arguments for the specified command or color model",
  },
END_COMMAND_LINE_DESCRIPTOR

#define VGA_NAME_FORMAT "(%13s)"
#define VGA_COLOR_FORMAT "VGA %2d"
#define RGB_COLOR_FORMAT "RGB(%3d, %3d, %3d)"
#define HSV_COLOR_FORMAT "HSV(%5.1f°, %3.0f%%, %3.0f%%)"

static void
showNotice (const char *notice) {
  size_t dividerWidth = 50;
  unsigned int noticeIndent;

  {
    size_t noticeLength = strlen(notice);

    if (noticeLength > dividerWidth) {
      dividerWidth = noticeLength;
      noticeIndent = 0;
    } else {
      noticeIndent = (dividerWidth - noticeLength) / 2;
    }
  }

  char divider[dividerWidth + 1];
  memset(divider, '=', dividerWidth);
  divider[dividerWidth] = 0;

  char indent[noticeIndent + 1];
  memset(indent, ' ', noticeIndent);
  indent[noticeIndent] = 0;

  putf("%s\n%s%s\n%s\n", divider, indent, notice, divider);
}

static void
showHeader (const char *name) {
  if (opt_quietness <= OPTQ_TEST) {
    putf("=== %s ===\n", name);
  }
}

static int
showResult (const char *name, int count, int passes) {
  int hasPassed = passes == count;

  if (!hasPassed) {
    if (opt_quietness <= OPTQ_SUMMARY) {
      putf("%d/%d tests failed.\n", (count - passes), count);
    }
  }

  if (opt_quietness <= OPTQ_TEST) {
    putf("[%s] %s\n", (hasPassed? "PASS": "FAIL"), name);
  }

  return hasPassed;
}

/* Test structure for predefined colors */
typedef struct {
  const char *name;
  unsigned char r, g, b;
} ColorTest;

/* Test VGA palette round-trip conversion */
static int
testVGAtoRGBtoVGA (const char *testName) {
  const int testCount = VGA_COLOR_COUNT;
  int passCount = 0;

  for (int vga=0; vga<testCount; vga+=1) {
    const char *name = vgaColorName(vga);

    RGBColor rgb = vgaToRgb(vga);
    int vgaBack = rgbColorToVga(rgb, 0);

    int passed = vga == vgaBack;
    if (passed) passCount += 1;

    if (opt_quietness <= (passed? OPTQ_PASS: OPTQ_FAIL)) {
      putf(VGA_COLOR_FORMAT " " VGA_NAME_FORMAT ": " RGB_COLOR_FORMAT " -> " VGA_COLOR_FORMAT " [%s]\n",
           vga, name, rgb.r, rgb.g, rgb.b, vgaBack,
           (passed? "OK": "FAIL"));
    }
  }

  return showResult(testName, testCount, passCount);
}

/* List VGA colors */
static int
listVGAColors (const char *testName) {
  if (opt_quietness <= OPTQ_PASS) {
    for (int vga=0; vga<VGA_COLOR_COUNT; vga+=1) {
      const char *vgaName = vgaColorName(vga);

      RGBColor rgb = vgaToRgb(vga);
      ColorNameBuffer rgbName;
      rgbColorToName(rgbName, sizeof(rgbName), rgb);

      putf(VGA_COLOR_FORMAT " " VGA_NAME_FORMAT ": " RGB_COLOR_FORMAT " -> \"%s\"\n",
           vga, vgaName, rgb.r, rgb.g, rgb.b, rgbName);
    }
  }

  return 1;
}

/* Test RGB conversion round-trip */
static int
testRGBtoHSVtoRGB (const char *testName) {
  static const ColorTest tests[] = {
    {"Pure Red",     255, 0,   0},
    {"Pure Green",   0,   255, 0},
    {"Pure Blue",    0,   0,   255},
    {"White",        255, 255, 255},
    {"Black",        0,   0,   0},
    {"Gray",         128, 128, 128},
    {"Yellow",       255, 255, 0},
    {"Cyan",         0,   255, 255},
    {"Magenta",      255, 0,   255},
    {"Orange",       255, 165, 0},
  };

  const int testCount = ARRAY_COUNT(tests);
  int passCount = 0;

  for (int i=0; i<testCount; i+=1) {
    const ColorTest *test = &tests[i];

    /* RGB -> HSV -> RGB */
    HSVColor hsv = rgbToHsv(test->r, test->g, test->b);
    RGBColor rgb = hsvColorToRgb(hsv);

    /* Allow small rounding errors (±1) */
    int rDiff = abs((int)rgb.r - (int)test->r);
    int gDiff = abs((int)rgb.g - (int)test->g);
    int bDiff = abs((int)rgb.b - (int)test->b);

    int passed = (rDiff <= 1) && (gDiff <= 1) && (bDiff <= 1);
    if (passed) passCount += 1;

    if (opt_quietness <= (passed? OPTQ_PASS: OPTQ_FAIL)) {
      putf("%-12s: " RGB_COLOR_FORMAT " -> " HSV_COLOR_FORMAT " -> " RGB_COLOR_FORMAT " [%s]\n",
           test->name, test->r, test->g, test->b,
           hsv.h, hsv.s*100.0f, hsv.v*100.0f,
           rgb.r, rgb.g, rgb.b,
           (passed? "OK": "FAIL"));
    }
  }

  return showResult(testName, testCount, passCount);
}

/* Test RGB to VGA mapping with various colors */
static int
showRGBtoVGA (const char *testName) {
  static const ColorTest tests[] = {
    {"Bright Red",       255, 0,   0},  /* Should be 12 (Light Red) */
    {"Dark Red",         128, 0,   0},  /* Should be 4 (Red) */
    {"Bright Green",     0,   255, 0},  /* Should be 10 (Light Green) */
    {"Dark Green",       0,   128, 0},  /* Should be 2 (Green) */
    {"Bright Blue",      0,   0,   255},  /* Should be 9 (Light Blue) */
    {"Dark Blue",        0,   0,   128},  /* Should be 1 (Blue) */
    {"Orange",           255, 165, 0},  /* Should be 6 (Brown) or 11 (Yellow) */
    {"Purple",           128, 0,   128},  /* Should be 5 (Magenta) */
  };

  if (opt_quietness <= OPTQ_PASS) {
    for (int i=0; i<ARRAY_COUNT(tests); i+=1) {
      const ColorTest *test = &tests[i];
      int vga = rgbToVga(test->r, test->g, test->b, 0);
      RGBColor rgb = vgaToRgb(vga);
      const char *color = vgaColorName(vga);

      putf("%-15s " RGB_COLOR_FORMAT " -> " VGA_COLOR_FORMAT " (%s) " RGB_COLOR_FORMAT "\n",
           test->name, test->r, test->g, test->b,
           vga, color, rgb.r, rgb.g, rgb.b);
    }
  }

  return 1;
}

static const char blockIndent[] = "  ";
static const char brightnessName[] = "brightness percent";
static const char grayName[] = "grayscale percent";
static const char hueName[] = "hue angle";
static const char lightnessName[] = "lightness percent";
static const char saturationName[] = "saturation percent";

static int
showColor (const RGBColor *rgbp, const HSVColor *hsvp, const HLSColor *hlsp) {
  RGBColor rgb;

  if (rgbp) {
    rgb = *rgbp;
  } else if (hsvp) {
    rgb = hsvColorToRgb(*hsvp);
  } else if (hlsp) {
    rgb = hlsColorToRgb(*hlsp);
  } else {
    return 0;
  }

  HSVColor hsv = hsvp? *hsvp: rgbColorToHsv(rgb);
  HLSColor hls = hlsp? *hlsp: rgbColorToHls(rgb);

  {
    unsigned char wasUsingSorting = useHSVColorSorting;
    useHSVColorSorting = 0;

    ColorNameBuffer colorName;
    hsvColorToName(colorName, sizeof(colorName), hsv);
    putf("%sName: %s\n", blockIndent, colorName);

    useHSVColorSorting = wasUsingSorting;
  }

  putf("%sRGB: (%d, %d, %d)\n", blockIndent, rgb.r, rgb.g, rgb.b);
  putf("%sHSV: (%.1f°, %.0f%%, %.0f%%)\n", blockIndent, hsv.h, hsv.s*100.0f, hsv.v*100.0f);
  putf("%sHLS: (%.1f°, %.0f%%, %.0f%%)\n", blockIndent, hls.h, hls.l*100.0f, hls.s*100.0f);

  {
    int vga = rgbToVga(rgb.r, rgb.g, rgb.b, 0);
    const char *name = vgaColorName(vga);
    putf("%sNearest VGA: %d \"%s\"\n", blockIndent, vga, name);
  }

  return 1;
}

static void
showRGBColor (RGBColor rgb) {
  showColor(&rgb, NULL, NULL);
}

static int
parseIntensity (int *intensity, const char *argument, const char *name) {
  return parseInteger(intensity, argument, 0, UINT8_MAX, name);
}

static int
rgbHandler (CommandArguments *arguments) {
  const char *redName = "red intensity";
  const char *redArgument;
  int redIntensity;

  const char *greenName = "green intensity";
  const char *greenArgument;
  int greenIntensity;

  const char *blueName = "blue intensity";
  const char *blueArgument;
  int blueIntensity;

  if ((redArgument = getNextArgument(arguments, redName))) {
    if ((greenArgument = getNextArgument(arguments, greenName))) {
      if ((blueArgument = getNextArgument(arguments, blueName))) {
        if (verifyNoMoreArguments(arguments)) {
          if (parseIntensity(&redIntensity, redArgument, redName)) {
            if (parseIntensity(&greenIntensity, greenArgument, greenName)) {
              if (parseIntensity(&blueIntensity, blueArgument, blueName)) {
                RGBColor rgb = {.r=redIntensity, .g=greenIntensity, .b=blueIntensity};
                showRGBColor(rgb);
                return 1;
              }
            }
          }
        }
      }
    }
  }

  return 0;
}

static int
hsvHandler (CommandArguments *arguments) {
  const char *hueArgument;
  float hueAngle;

  const char *saturationArgument;
  float saturationLevel;

  const char *brightnessArgument;
  float brightnessLevel;

  if ((hueArgument = getNextArgument(arguments, hueName))) {
    if ((saturationArgument = getNextArgument(arguments, saturationName))) {
      if ((brightnessArgument = getNextArgument(arguments, brightnessName))) {
        if (verifyNoMoreArguments(arguments)) {
          if (parseDegrees(&hueAngle, hueArgument, hueName)) {
            if (parsePercent(&saturationLevel, saturationArgument, saturationName)) {
              if (parsePercent(&brightnessLevel, brightnessArgument, brightnessName)) {
                HSVColor hsv = {.h=hueAngle, .s=saturationLevel, .v=brightnessLevel};
                showColor(NULL, &hsv, NULL);
                return 1;
              }
            }
          }
        }
      }
    }
  }

  return 0;
}

static int
hlsHandler (CommandArguments *arguments) {
  const char *hueArgument;
  float hueAngle;

  const char *lightnessArgument;
  float lightnessLevel;

  const char *saturationArgument;
  float saturationLevel;

  if ((hueArgument = getNextArgument(arguments, hueName))) {
    if ((lightnessArgument = getNextArgument(arguments, lightnessName))) {
      if ((saturationArgument = getNextArgument(arguments, saturationName))) {
        if (verifyNoMoreArguments(arguments)) {
          if (parseDegrees(&hueAngle, hueArgument, hueName)) {
            if (parsePercent(&lightnessLevel, lightnessArgument, lightnessName)) {
              if (parsePercent(&saturationLevel, saturationArgument, saturationName)) {
                HLSColor hls = {.h=hueAngle, .l=lightnessLevel, .s=saturationLevel};
                showColor(NULL, NULL, &hls);
                return 1;
              }
            }
          }
        }
      }
    }
  }

  return 0;
}

static int
vgaHandler (CommandArguments *arguments) {
  const char *vgaName = "VGA color number";
  const char *vgaArgument = getNextArgument(arguments, vgaName);

  if (vgaArgument) {
    if (verifyNoMoreArguments(arguments)) {
      int vgaColor;

      if (parseInteger(&vgaColor, vgaArgument, 0, (VGA_COLOR_COUNT - 1), vgaName)) {
        putf("%sVGA: %d\n", blockIndent, vgaColor);
        showRGBColor(vgaToRgb(vgaColor));
        return 1;
      }
    }
  }

  return 0;
}

static int
ansiHandler (CommandArguments *arguments) {
  const char *ansiName = "ANSI color number";
  const char *ansiArgument = getNextArgument(arguments, ansiName);

  if (ansiArgument) {
    if (verifyNoMoreArguments(arguments)) {
      int ansiColor;

      if (parseInteger(&ansiColor, ansiArgument, 0, UINT8_MAX, ansiName)) {
        putf("%sANSI: %d\n", blockIndent, ansiColor);
        showRGBColor(ansiToRgb(ansiColor));
        return 1;
      }
    }
  }

  return 0;
}

typedef struct {
  const char *name;
  const char *syntax;
  int (*handler) (CommandArguments *arguments);
} ColorModel;

static void
showColorModelSyntax (const ColorModel *model) {
  if (opt_quietness <= OPTQ_TEST) {
    putf("\n%s color model syntax: %s\n", model->name, model->syntax);
  }
}

static const ColorModel colorModels[] = {
  { .name = "RGB",
    .syntax = "red green blue (all integers within the range 0-255)",
    .handler = rgbHandler,
  },

  { .name = "HSV",
    .syntax = "hue (degrees) saturation (percent) brightness (percent)",
    .handler = hsvHandler,
  },

  { .name = "HLS",
    .syntax = "hue (degrees) lightness (percent) saturation (percent)",
    .handler = hlsHandler,
  },

  { .name = "VGA",
    .syntax = "an integer within the range 0-15",
    .handler = vgaHandler,
  },

  { .name = "ANSI",
    .syntax = "an integer within the range 0-255",
    .handler = ansiHandler,
  },
};

static const ColorModel *
getColorModel (const char *name) {
  for (int i=0; i<ARRAY_COUNT(colorModels); i+=1) {
    const ColorModel *model = &colorModels[i];
    if (isAbbreviation(model->name, name)) return model;
  }

  return NULL;
}

static void
showRanges (
  const char *(*getName) (unsigned int value),
  unsigned int from, unsigned int to, const char *unit
) {
  typedef struct {
    const char *name;
    unsigned int from;
    unsigned int to;
  } Range;

  Range ranges[20];
  unsigned int count = 0;

  for (unsigned int value=from; value<=to; value+=1) {
    const char *name = getName(value);

    if (count > 0) {
      Range *previous = &ranges[count - 1];
      previous->to = value;
      if (strcmp(name, previous->name) == 0) continue;
    }

    if (count == ARRAY_COUNT(ranges)) break;
    Range *range = &ranges[count++];
    range->name = name;
    range->from = value;
    range->to = value;
  }

  for (unsigned int i=0; i<count; i+=1) {
    const Range *range = &ranges[i];

    putf(
      "%s%s: %u%s-%u%s\n",
      blockIndent, range->name,
      range->from, unit,
      range->to, unit
    );
  }
}

static void
showAngleRanges (const char *(*getName) (unsigned int value)) {
  showRanges(getName, 0, 360, "°");
}

static void
showPercentRanges (const char *(*getName) (unsigned int value)) {
  showRanges(getName, 0, 100, "%");
}

static const char *
getGrayscaleColorName (unsigned int value) {
  return gsColorName((float)value / 100.0f);
}

static int
cmdGrayscale (CommandArguments *arguments) {
  if (checkNoMoreArguments(arguments)) {
    showPercentRanges(getGrayscaleColorName);
    return 1;
  }

  {
    const char *argument = getNextArgument(arguments, grayName);

    if (argument) {
      if (verifyNoMoreArguments(arguments)) {
        float level;

        if (parsePercent(&level, argument, grayName)) {
          putf("%s%s\n", blockIndent, gsColorName(level));
          return 1;
        }
      }
    }
  }

  return 0;
}

static const char *
getHueColorName (unsigned int value) {
  return hueColorName((float)value);
}

static int
cmdHue (CommandArguments *arguments) {
  if (checkNoMoreArguments(arguments)) {
    showAngleRanges(getHueColorName);
    return 1;
  }

  {
    const char *argument = getNextArgument(arguments, hueName);

    if (argument) {
      if (verifyNoMoreArguments(arguments)) {
        float angle;

        if (parseDegrees(&angle, argument, hueName)) {
          putf("%s%s\n", blockIndent, hueColorName(angle));
          return 1;
        }
      }
    }
  }

  return 0;
}

static const char *
getSaturationModifierName (unsigned int value) {
  return hsvSaturationModifier((float)value / 100.0f)->name;
}

static int
cmdSaturation (CommandArguments *arguments) {
  if (checkNoMoreArguments(arguments)) {
    showPercentRanges(getSaturationModifierName);
    return 1;
  }

  {
    const char *argument = getNextArgument(arguments, saturationName);

    if (argument) {
      if (verifyNoMoreArguments(arguments)) {
        float level;

        if (parsePercent(&level, argument, saturationName)) {
          putf("%s%s\n", blockIndent, hsvSaturationModifier(level)->name);
          return 1;
        }
      }
    }
  }

  return 0;
}

static const char *
getBrightnessModifierName (unsigned int value) {
  return hsvBrightnessModifier((float)value / 100.0f)->name;
}

static int
cmdBrightness (CommandArguments *arguments) {
  if (checkNoMoreArguments(arguments)) {
    showPercentRanges(getBrightnessModifierName);
    return 1;
  }

  {
    const char *argument = getNextArgument(arguments, brightnessName);

    if (argument) {
      if (verifyNoMoreArguments(arguments)) {
        float level;

        if (parsePercent(&level, argument, brightnessName)) {
          putf("%s%s\n", blockIndent, hsvBrightnessModifier(level)->name);
          return 1;
        }
      }
    }
  }

  return 0;
}

static int
sortColorsByName (const void *item1, const void *item2) {
  const HSVColorEntry *const *color1 = item1;
  const HSVColorEntry *const *color2 = item2;
  return strcasecmp((*color1)->name, (*color2)->name);
}

static void
showColorEntry (const HSVColorEntry *color) {
  putf(
    "%s%s: Hue:%.0f°-%.0f° Sat:%.0f%%-%.0f%% Val:%.0f%%-%.0f%%\n",
    blockIndent, color->name,
    color->hue.minimum, color->hue.maximum,
    color->sat.minimum*100.0f, color->sat.maximum*100.0f,
    color->val.minimum*100.0f, color->val.maximum*100.0f
  );
}

static int
cmdColors (CommandArguments *arguments) {
  const HSVColorEntry *colors[hsvColorCount];

  {
    for (int i=0; i<hsvColorCount; i+=1) {
      colors[i] = &hsvColorTable[i];
    }

    qsort(colors, hsvColorCount, sizeof(colors[0]), sortColorsByName);
  }

  if (checkNoMoreArguments(arguments)) {
    for (int i=0; i<hsvColorCount; i+=1) {
      showColorEntry(colors[i]);
    }

    return 1;
  }

  {
    const char *name = getNextArgument(arguments, "color name");

    if (name) {
      if (verifyNoMoreArguments(arguments)) {
        int found = 0;

        for (int i=0; i<hsvColorCount; i+=1) {
          const HSVColorEntry *color = colors[i];

          if (isAbbreviation(color->name, name)) {
            showColorEntry(color);
            found = 1;
          }
        }

        if (found) return 1;
        logMessage(LOG_ERR, "color not recognized: %s", name);
        return 2;
      }
    }
  }

  return 0;
}

static int
hsvValidRange (const HSVComponentRange *range, float minimum, float maximum, int isCyclic) {
  if (maximum < minimum) return 0;
  if (range->minimum < minimum) return 0;
  if (range->maximum > maximum) return 0;

  if (isCyclic) return 1;
  return range->minimum < range->maximum;
}

static int
hsvValidAngleRange (const HSVComponentRange *range) {
  if ((float)(int)range->minimum != range->minimum) return 0;
  if ((float)(int)range->maximum != range->maximum) return 0;
  return hsvValidRange(range, 0.0f, 360.0f, 1);
}

static int
hsvValidLevelRange (const HSVComponentRange *range) {
  return hsvValidRange(range, 0.0f, 1.0f, 0);
}

static int
hsvCyclicOverlap (const HSVComponentRange *range1, const HSVComponentRange *range2) {
  if (range2->minimum >= range1->minimum) return 1;
  if (range2->maximum > range1->minimum) return 1;
  if (range2->minimum < range1->maximum) return 1;

  if (hsvCyclicRange(range2)) {
    if ((range2->minimum <= range1->minimum) && (range2->maximum >= range1->maximum)) {
      return 1;
    }
  }

  return 0;
}

static int
hsvRangesOverlap (const HSVComponentRange *range1, const HSVComponentRange *range2, int isCyclic) {
  if (isCyclic) {
    if (hsvCyclicRange(range1)) return hsvCyclicOverlap(range1, range2);
    if (hsvCyclicRange(range2)) return hsvCyclicOverlap(range2, range1);
  }

  if (range2->minimum >= range1->maximum) return 0;
  return range2->maximum > range1->minimum;
}

static int
hsvColorsOverlap (const HSVColorEntry *color1, const HSVColorEntry *color2) {
  return hsvRangesOverlap(&color1->hue, &color2->hue, 1) &&
         hsvRangesOverlap(&color1->sat, &color2->sat, 0) &&
         hsvRangesOverlap(&color1->val, &color2->val, 0);
}

static int
hsvRangeContains (const HSVComponentRange *outer, const HSVComponentRange *inner, int isCyclic) {
  if (isCyclic) {
    if (hsvCyclicRange(outer)) {
      if (!hsvCyclicRange(inner)) {
        return (inner->minimum >= outer->minimum) || (inner->maximum <= outer->maximum);
      }
    } else if (hsvCyclicRange(inner)) {
      return 0;
    }
  }

  return (inner->minimum >= outer->minimum) && (inner->maximum <= outer->maximum);
}

static int
hsvColorContains (const HSVColorEntry *outer, const HSVColorEntry *inner) {
  return hsvRangeContains(&outer->hue, &inner->hue, 1) &&
         hsvRangeContains(&outer->sat, &inner->sat, 0) &&
         hsvRangeContains(&outer->val, &inner->val, 0);
}

static void showColorProblem (const HSVColorEntry *color, const char *format, ...) PRINTF(2, 3);

static void
showColorProblem (const HSVColorEntry *color, const char *format, ...) {
  putf("%s%s: ", blockIndent, color->name);

  {
    va_list args;
    va_start(args, format);
    vputf(format, args);
    va_end(args);
  }

  putNewline();
}

static void
showColorConflict (const HSVColorEntry *color1, const HSVColorEntry *color2, const char *relation) {
  putf("%s%s %s %s\n", blockIndent, color1->name, relation, color2->name);
}

static int
cmdProblems (CommandArguments *arguments) {
  if (verifyNoMoreArguments(arguments)) {
    unsigned int problemCount = 0;
    unsigned int colorCount = 0;
    unsigned int pairCount = 0;

    for (int i=0; i<hsvColorCount; i+=1) {
      const HSVColorEntry *color1 = &hsvColorTable[i];
      colorCount += 1;

      if (!hsvValidAngleRange(&color1->hue)) {
        problemCount += 1;
        showColorProblem(
          color1, "invalid hue range: %.0f-%.0f",
          color1->hue.minimum, color1->hue.maximum
        );
      }

      if (!hsvValidLevelRange(&color1->sat)) {
        problemCount += 1;
        showColorProblem(
          color1, "invalid saturation range: %.2f-%.2f",
          color1->sat.minimum, color1->sat.maximum
        );
      }

      if (!hsvValidLevelRange(&color1->val)) {
        problemCount += 1;
        showColorProblem(
          color1, "invalid value range: %.2f-%.2f",
          color1->val.minimum, color1->val.maximum
        );
      }

      if (useHSVColorSorting) {
        static const float increment = 0.001;

        HSVColor hsv = {
          .h = color1->hue.minimum + increment,
          .s = color1->sat.minimum + increment,
          .v = color1->val.minimum + increment,
        };

        const HSVColorEntry *color = hsvColorEntry(hsv);

        if (color1 != color) {
          problemCount += 1;
          showColorProblem(
            color1, "sorted search failed (minimum) -> %s",
            color? color->name: "none"
          );
        }
      }

      if (useHSVColorSorting) {
        static const float decrement = 0.001;

        HSVColor hsv = {
          .h = color1->hue.maximum - decrement,
          .s = color1->sat.maximum - decrement,
          .v = color1->val.maximum - decrement,
        };

        const HSVColorEntry *color = hsvColorEntry(hsv);

        if (color1 != color) {
          problemCount += 1;
          showColorProblem(
            color1, "sorted search failed (maximum) -> %s",
            color? color->name: "none"
          );
        }
      }

      for (int j=i+1; j<hsvColorCount; j+=1) {
        const HSVColorEntry *color2 = &hsvColorTable[j];
        pairCount += 1;

        if (strcasecmp(color1->name, color2->name) == 0) {
          problemCount += 1;
          showColorProblem(color1, "duplicate definition");
        }

        if (hsvColorContains(color1, color2)) {
          problemCount += 1;
          showColorConflict(color1, color2, "contains");
        } else if (hsvColorContains(color2, color1)) {
          problemCount += 1;
          showColorConflict(color1, color2, "is within");
        } else if (hsvColorsOverlap(color1, color2)) {
          problemCount += 1;
          showColorConflict(color1, color2,"overlaps");
        }
      }
    }

    if (problemCount > 0) {
      if (opt_quietness <= OPTQ_FAIL) {
        putf(
          "%u %s found (%u colors, %u color pairs).\n",
          problemCount, ((problemCount == 1)? "problem": "problems"),
          colorCount, pairCount
        );
      }

      return 2;
    }

    if (opt_quietness <= OPTQ_PASS) putf("No problems found.\n");
    return 1;
  }

  return 0;
}

static int
cmdOptions (CommandArguments *arguments) {
  typedef struct {
    const char *name;
    unsigned char *setting;
  } OptionEntry;

  static const OptionEntry optionTable[] = {
    { .name = "sorting",
      .setting = &useHSVColorSorting,
    },
  };

  if (checkNoMoreArguments(arguments)) {
    for (int i=0; i<ARRAY_COUNT(optionTable); i+=1) {
      const OptionEntry *option = &optionTable[i];

      putf("%s%s %s\n", blockIndent, option->name, (*option->setting? "on": "off"));
    }
  } else {
    unsigned char alreadySpecified[ARRAY_COUNT(optionTable)];

    for (int i=0; i<ARRAY_COUNT(alreadySpecified); i+=1) {
      alreadySpecified[i] = 0;
    }

    typedef struct {
      unsigned char *setting;
      unsigned char value;
    } ChangeEntry;

    ChangeEntry changes[ARRAY_COUNT(optionTable)];
    unsigned int changeCount = 0;

    const char *noPrefix = "no";
    size_t noLength = strlen(noPrefix);

    while (1) {
      const char *name = getNextArgument(arguments, "option name");
      unsigned char newValue = 1;

      if (strlen(name) > noLength) {
        if (strncasecmp(name, noPrefix, noLength) == 0) {
          newValue = 0;
          name += noLength;
        }
      }

      for (int i=0; i<ARRAY_COUNT(optionTable); i+=1) {
        const OptionEntry *option = &optionTable[i];

        if (isAbbreviation(option->name, name)) {
          if (alreadySpecified[i]) {
            logMessage(LOG_ERR, "option already specified: %s", option->name);
            return 0;
          }

          alreadySpecified[i] = 1;
          ChangeEntry *change = &changes[changeCount];

          if (*option->setting != newValue) {
            change->setting = option->setting;
            change->value = newValue;
            changeCount += 1;
          }

          goto NEXT_ARGUMENT;
        }
      }

      logMessage(LOG_ERR, "unrecognized option name: %s", name);
      return 0;

    NEXT_ARGUMENT:
      if (checkNoMoreArguments(arguments)) break;
    }

    if (changeCount > 0) {
      for (int i=0; i<changeCount; i+=1) {
        const ChangeEntry *change = &changes[i];
        *change->setting = change->value;
      }
    } else if (opt_quietness <= OPTQ_INFO) {
      putf("No changes.\n");
    }
  }

  return 1;
}

#define CMD_NAME_QUIT "quit"
#define CMD_NAME_HELP "help"

static int cmdHelp (CommandArguments *arguments);
static const ColorModel *const defaultColorModel = colorModels;

typedef struct {
  const char *name;
  const char *help;
  const char *syntax;
  int (*handler) (CommandArguments *arguments);
} CommandEntry;

static const CommandEntry commandTable[] = {
  { .name = "brightness",
    .help = "List the HSV brightness (value) modifier names and ranges.",
    .syntax = "[percent]",
    .handler = cmdBrightness,
  },

  { .name = "colors",
    .help = "List the color definitions table.",
    .syntax = "[name]",
    .handler = cmdColors,
  },

  { .name = "grayscale",
    .help = "List the grayscale color names and brightness ranges.",
    .syntax = "[percent]",
    .handler = cmdGrayscale,
  },

  { .name = CMD_NAME_HELP,
    .help = "Show a command usage summary.",
    .syntax = "",
    .handler = cmdHelp,
  },

  { .name = "hue",
    .help = "List the main hue color names and ranges.",
    .syntax = "[degrees]",
    .handler = cmdHue,
  },

  { .name = "options",
    .help = "Inspect or change options.",
    .syntax = "[[no]option ...]",
    .handler = cmdOptions,
  },

  { .name = "problems",
    .help = "Check the color definition table for problems.",
    .syntax = "",
    .handler = cmdProblems,
  },

  { .name = "saturation",
    .help = "List the HSV saturation modifier names and ranges.",
    .syntax = "[percent]",
    .handler = cmdSaturation,
  },
};

static const CommandEntry *
getCommandEntry (const char *name) {
  for (int i=0; i<ARRAY_COUNT(commandTable); i+=1) {
    const CommandEntry *cmd = &commandTable[i];
    if (isAbbreviation(cmd->name, name)) return cmd;
  }

  return NULL;
}

static int
doCommand (const char *name, CommandArguments *arguments, const ColorModel **currentModel) {
  {
    const CommandEntry *cmd = getCommandEntry(name);
    if (cmd) return cmd->handler(arguments);
  }

  {
    const ColorModel *model = getColorModel(name);

    if (model) {
      if (currentModel && checkNoMoreArguments(arguments)) {
        *currentModel = model;
        showColorModelSyntax(model);
        return 1;
      }

      return model->handler(arguments);
    }
  }

  logMessage(LOG_ERR, "unrecognized command: %s", name);
  return 0;
}

static void
showHelp (int listCommands) {
  putf("Command and color model names aren't case-sensitive and may be abbreviated.\n");
  putf("Use the \"" CMD_NAME_QUIT "\" command to exit interactive mode.\n");

  if (listCommands) {
    putf("The rest of the commands are:\n");

    int offsets[ARRAY_COUNT(commandTable)];
    int lengths[ARRAY_COUNT(commandTable)];
    int longest = 0;

    char buffer[0X400];
    STR_BEGIN(buffer, sizeof(buffer));

    for (int i=0; i<ARRAY_COUNT(commandTable); i+=1) {
      const CommandEntry *cmd = &commandTable[i];

      int *offset = &offsets[i];
      *offset = STR_LENGTH;

      STR_PRINTF("%s", cmd->name);
      if (cmd->syntax && *cmd->syntax) STR_PRINTF(" %s", cmd->syntax);

      int *length = &lengths[i];
      *length = STR_LENGTH - *offset;

      if (*length > longest) longest = *length;
    }

    STR_END;

    for (int i=0; i<ARRAY_COUNT(commandTable); i+=1) {
      const CommandEntry *cmd = &commandTable[i];
      int *length = &lengths[i];

      putf("%s%.*s", blockIndent, *length, &buffer[offsets[i]]);

      if (cmd->help && *cmd->help) {
        putf("%*s  %s", (longest - *length), "", cmd->help);
      }

      putf("\n");
    }
  } else {
    putf("Use the \"" CMD_NAME_HELP "\" command to discover the rest of them.\n");
  }

  {
    putf("\nThe supported color models are:");
    int last = ARRAY_COUNT(colorModels) - 1;

    for (int i=0; i<=last; i+=1) {
      const ColorModel *model = &colorModels[i];

      if (i > 0) putf(",");
      if (i == last) putf(" and");
      putf(" %s", model->name);
      if (model == defaultColorModel) putf(" (the default)");
    }

    putf("\n");
  }

  putf("To switch to another color model, enter its name with no additional arguments.\n");
  putf("If additional arguments follow the name then that color model is used.\n");
  putf("If only numeric arguments are specified then the current color model is used.\n");
}

static int
cmdHelp (CommandArguments *arguments) {
  if (verifyNoMoreArguments(arguments)) {
    showHelp(1);
    return 1;
  }

  return 0;
}

static void
doInteractiveMode (void) {
  beginInteractiveMode();
  const ColorModel *currentColorModel = defaultColorModel;

  showHeader("Interactive Mode");
  if (opt_quietness <= OPTQ_INFO) showHelp(0);
  showColorModelSyntax(currentColorModel);

  CommandArguments *arguments = newCommandArguments();
  char *line = NULL;
  size_t lineSize = 0;

  while (1) {
    putf("%s> ", currentColorModel->name);
    putFlush();

    if (!readLine(stdin, &line, &lineSize, NULL)) {
      putf("\n");
      break;
    }

    removeArguments(arguments);
    addArgumentsFromString(arguments, line);

    if (checkNoMoreArguments(arguments)) continue;
    char *command = getNextArgument(arguments, "command");

    if (isAbbreviation(CMD_NAME_QUIT, command)) {
      if (verifyNoMoreArguments(arguments)) break;
    } else if (isdigit(command[0])) {
      restoreArgument(arguments, command);
      currentColorModel->handler(arguments);
    } else {
      doCommand(command, arguments, &currentColorModel);
    }
  }

  if (line) free(line);
  destroyCommandArguments(arguments);
  endInteractiveMode();
}

typedef struct {
  const char *name;
  const char *objective;
  int *requested;
  int (*perform) (const char *testName);
} RequestableTest;

static const RequestableTest requetableTestTable[] = {
  { .name = "VGA to RGB to VGA Round-Trip Test",
    .objective = "Verify the successful conversion of each VGA color to RGB and then back to VGA",
    .requested = &opt_testVGAtoRGBtoVGA,
    .perform = testVGAtoRGBtoVGA,
  },

  { .name = "VGA Color Listing",
    .objective = "List the VGA number and name, as well as the HSV name, of each VGA color",
    .requested = &opt_listVGAColors,
    .perform = listVGAColors,
  },

  { .name = "RGB to HSV to RGB Round-Trip Test",
    .objective = "Verify the successful conversion of some RGB colors to HSV and then back to RGB",
    .requested = &opt_testRGBtoHSVtoRGB,
    .perform = testRGBtoHSVtoRGB,
  },

  { .name = "RGB to Nearest VGA Mappings",
    .objective = "Show how some common colors are mapped to their nearest VGA colors",
    .requested = &opt_showRGBtoVGA,
    .perform = showRGBtoVGA,
  },
};

static const size_t requetableTestCount = ARRAY_COUNT(requetableTestTable);

/* perform the requested tests */
static int
performRequestedTests (void) {
  int allPassed = 1;

  if (opt_quietness <= OPTQ_INFO) {
    showNotice("BRLTTY Color Conversion Test Suite");
    putf("\n");
  }

  for (int i=0; i<requetableTestCount; i+=1) {
    const RequestableTest *test = &requetableTestTable[i];

    if (*test->requested) {
      showHeader(test->name);

      if (test->objective) {
        if (opt_quietness <= OPTQ_PASS) {
          putf("%s...\n\n", test->objective);
        }
      }

      if (!test->perform(test->name)) {
        allPassed = 0;
      }

      if (opt_quietness <= OPTQ_TEST) {
        putf("\n");
      }
    }
  }

  if (opt_quietness <= OPTQ_INFO) {
    showNotice("Tests Complete");
  }

  return allPassed;
}

/* Main test program */
int
main (int argc, char *argv[]) {
  PROCESS_COMMAND_LINE(programDescriptor, argc, argv);

  int testRequested = opt_performAllTests;

  for (int i=0; i<requetableTestCount; i+=1) {
    const RequestableTest *test = &requetableTestTable[i];

    if (*test->requested) {
      if (opt_performAllTests) {
        logMessage(LOG_ERR, "conflicting test options");
        return PROG_EXIT_SYNTAX;
      }

      testRequested = 1;
    } else if (opt_performAllTests) {
      *test->requested = 1;
    }
  }

  if (specifiedCommand) {
    if (testRequested) {
      logMessage(LOG_ERR, "can't request a test and specify a command");
      return PROG_EXIT_SYNTAX;
    }

    int result;
    {
      CommandArguments *arguments = newCommandArguments();
      addArgumentsFromArray(arguments, argv, argc);
      result = doCommand(specifiedCommand, arguments, NULL);
      destroyCommandArguments(arguments);
    }

    if (result == 2) return PROG_EXIT_SEMANTIC;
    return result? PROG_EXIT_SUCCESS: PROG_EXIT_SYNTAX;
  }

  if (!testRequested) {
    doInteractiveMode();
  } else if (!performRequestedTests()) {
    return PROG_EXIT_SEMANTIC;
  }

  return PROG_EXIT_SUCCESS;
}
