#include "hb-fuzzer.hh"

#include <glib.h>

#include <cassert>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <ctime>

/* Globals defined in hb-depend-fuzzer.cc */
extern const char *_hb_depend_fuzzer_current_font_path;
extern unsigned _hb_depend_fuzzer_seed;
extern uint64_t _hb_depend_fuzzer_single_test;  /* Specific test to run, or 0 for all */
extern unsigned _hb_depend_fuzzer_num_tests;    /* Number of tests to run (default 1024) */
extern bool _hb_depend_fuzzer_verbose;
extern bool _hb_depend_fuzzer_report_over_approximation;
extern bool _hb_depend_fuzzer_report_under_approximation;
extern float _hb_depend_fuzzer_inject_over_approx;
extern float _hb_depend_fuzzer_inject_under_approx;

/* Explicit test mode */
extern bool _hb_depend_fuzzer_explicit_test;
extern const char *_hb_depend_fuzzer_explicit_unicodes;
extern const char *_hb_depend_fuzzer_explicit_gids;
extern bool _hb_depend_fuzzer_explicit_no_layout_closure;
extern const char *_hb_depend_fuzzer_explicit_layout_features;

/* Container for injection rate variables passed to callback */
struct inject_rates_t {
  float *over_approx;
  float *under_approx;
};

/* Glib option parsing callback for optional float arguments with defaults */
static gboolean
parse_inject_rate (const gchar *option_name,
                   const gchar *value,
                   gpointer data,
                   GError **error)
{
  inject_rates_t *rates = (inject_rates_t *) data;
  float *target;

  /* Determine which rate to set based on option name */
  if (g_str_equal (option_name, "-o") || g_str_equal (option_name, "--inject-over-approx"))
    target = rates->over_approx;
  else if (g_str_equal (option_name, "-u") || g_str_equal (option_name, "--inject-under-approx"))
    target = rates->under_approx;
  else
    return FALSE;  /* Unknown option */

  if (value)
    *target = strtof (value, NULL);
  else
    *target = 0.02f;  /* Default 2% */
  return TRUE;
}

static void
print_usage (const char *prog)
{
  fprintf (stderr, "Usage: %s [OPTIONS] FONT...\n", prog);
  fprintf (stderr, "\n");
  fprintf (stderr, "Validate depend API by comparing closures against subset.\n");
  fprintf (stderr, "\n");
  fprintf (stderr, "Test Modes:\n");
  fprintf (stderr, "  (default)             Run random tests using seed\n");
  fprintf (stderr, "  -t, --test HEXSEED    Run only specific test (64-bit hex with 0x prefix)\n");
  fprintf (stderr, "  --unicodes LIST       Run explicit test with given unicodes (U+XXXX or decimal)\n");
  fprintf (stderr, "  --gids LIST           Run explicit test with given glyph IDs (decimal)\n");
  fprintf (stderr, "\n");
  fprintf (stderr, "Explicit Test Options (used with --unicodes or --gids):\n");
  fprintf (stderr, "  --no-layout-closure   Skip GSUB closure (use with --gids)\n");
  fprintf (stderr, "  --layout-features=LIST\n");
  fprintf (stderr, "                        Specify GSUB features (comma-separated tags or * for all)\n");
  fprintf (stderr, "                        Default: subset's default features\n");
  fprintf (stderr, "\n");
  fprintf (stderr, "General Options:\n");
  fprintf (stderr, "  -s, --seed SEED       Use specific random seed (default: random)\n");
  fprintf (stderr, "  -n, --num-tests N     Run N tests per font (default: 1024)\n");
  fprintf (stderr, "  -v, --verbose         Enable verbose output (all test details)\n");
  fprintf (stderr, "  -r, --report-over-approximation\n");
  fprintf (stderr, "                        Report when depend finds more glyphs than subset\n");
  fprintf (stderr, "  --report-under-approximation\n");
  fprintf (stderr, "                        Report under-approximation instead of aborting\n");
  fprintf (stderr, "  -o, --inject-over-approx[=RATE]\n");
  fprintf (stderr, "                        Inject over-approximation errors (default rate: 0.02 = 2%%)\n");
  fprintf (stderr, "  -u, --inject-under-approx[=RATE]\n");
  fprintf (stderr, "                        Inject under-approximation errors (default rate: 0.02 = 2%%)\n");
  fprintf (stderr, "  -h, --help            Show this help message\n");
  fprintf (stderr, "\n");
  fprintf (stderr, "Examples:\n");
  fprintf (stderr, "  %s fonts/*.ttf                           # Run 1024 tests on each font\n", prog);
  fprintf (stderr, "  %s -n 100 fonts/*.ttf                    # Run 100 tests per font\n", prog);
  fprintf (stderr, "  %s -v fonts/MyFont.ttf                   # Run with verbose output\n", prog);
  fprintf (stderr, "  %s -t 0x0000303900000015 fonts/MyFont.ttf  # Run specific test\n", prog);
  fprintf (stderr, "  %s --unicodes=U+0041,U+0042 fonts/MyFont.ttf  # Test with 'A' and 'B'\n", prog);
  fprintf (stderr, "  %s --gids=10,20,30 --no-layout-closure fonts/MyFont.ttf  # Non-GSUB test\n", prog);
  fprintf (stderr, "  %s --unicodes=65-90 --layout-features=liga,calt fonts/MyFont.ttf  # Custom features\n", prog);
}

int main (int argc, char **argv)
{
  unsigned seed = 0;
  const char *single_test_str = NULL;  /* Parse as string to support full uint64 range */
  uint64_t single_test = 0;
  unsigned num_tests = 1024;
  gboolean verbose = FALSE;
  gboolean report_over_approximation = FALSE;
  gboolean report_under_approximation = FALSE;
  gboolean seed_specified = FALSE;
  float inject_over_approx = 0.0f;
  float inject_under_approx = 0.0f;

  /* Explicit test mode */
  gboolean explicit_test = FALSE;
  const char *explicit_unicodes = NULL;
  const char *explicit_gids = NULL;
  gboolean explicit_no_layout_closure = FALSE;
  const char *explicit_layout_features = NULL;

  /* Injection rate callback data */
  inject_rates_t inject_rates = {&inject_over_approx, &inject_under_approx};

  /* Define glib option entries */
  GOptionEntry test_mode_entries[] =
  {
    {"test", 't', 0, G_OPTION_ARG_STRING, &single_test_str,
     "Run only specific test (64-bit hex with 0x prefix)", "0xHEXSEED"},
    {"unicodes", 0, 0, G_OPTION_ARG_STRING, &explicit_unicodes,
     "Run explicit test with given unicodes (U+XXXX or decimal)", "LIST"},
    {"gids", 0, 0, G_OPTION_ARG_STRING, &explicit_gids,
     "Run explicit test with given glyph IDs (decimal)", "LIST"},
    {NULL}
  };

  GOptionEntry explicit_test_entries[] =
  {
    {"no-layout-closure", 0, 0, G_OPTION_ARG_NONE, &explicit_no_layout_closure,
     "Skip GSUB closure (use with --gids)", NULL},
    {"layout-features", 0, 0, G_OPTION_ARG_STRING, &explicit_layout_features,
     "Specify GSUB features (comma-separated tags or * for all, default: subset's default features)", "LIST"},
    {NULL}
  };

  GOptionEntry general_entries[] =
  {
    {"seed", 's', 0, G_OPTION_ARG_INT, &seed,
     "Use specific random seed (default: random)", "SEED"},
    {"num-tests", 'n', 0, G_OPTION_ARG_INT, &num_tests,
     "Run N tests per font (default: 1024)", "N"},
    {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose,
     "Enable verbose output (all test details)", NULL},
    {"report-over-approximation", 'r', 0, G_OPTION_ARG_NONE, &report_over_approximation,
     "Report when depend finds more glyphs than subset", NULL},
    {"report-under-approximation", 0, 0, G_OPTION_ARG_NONE, &report_under_approximation,
     "Report under-approximation instead of aborting", NULL},
    {"inject-over-approx", 'o', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, (gpointer) &parse_inject_rate,
     "Inject over-approximation errors (default rate: 0.02 = 2%)", "[RATE]"},
    {"inject-under-approx", 'u', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, (gpointer) &parse_inject_rate,
     "Inject under-approximation errors (default rate: 0.02 = 2%)", "[RATE]"},
    {NULL}
  };

  /* Parse command line options */
  GError *error = NULL;
  GOptionContext *context = g_option_context_new ("FONT... - Validate depend API by comparing closures against subset");

  /* Add main entries (no callbacks, no user_data needed) */
  GOptionGroup *main_group = g_option_group_new (nullptr, nullptr, nullptr, nullptr, nullptr);
  g_option_group_add_entries (main_group, test_mode_entries);
  g_option_context_set_main_group (context, main_group);

  GOptionGroup *explicit_group = g_option_group_new ("explicit", "Explicit Test Options:", "Show explicit test options", NULL, NULL);
  g_option_group_add_entries (explicit_group, explicit_test_entries);
  g_option_context_add_group (context, explicit_group);

  GOptionGroup *general_group = g_option_group_new ("general", "General Options:", "Show general options",
                                                     static_cast<gpointer>(&inject_rates), nullptr);
  g_option_group_add_entries (general_group, general_entries);
  g_option_context_add_group (context, general_group);

  if (!g_option_context_parse (context, &argc, &argv, &error))
  {
    fprintf (stderr, "Error parsing options: %s\n", error->message);
    g_error_free (error);
    g_option_context_free (context);
    return 1;
  }
  g_option_context_free (context);

  /* Check if we have font files */
  if (argc < 2)
  {
    fprintf (stderr, "Error: No font files specified\n\n");
    print_usage (argv[0]);
    return 1;
  }

  /* Derive seed_specified from whether seed was explicitly set */
  /* Note: glib doesn't directly tell us if an option was set, so we check if seed != 0 */
  seed_specified = (seed != 0);

  /* Parse test seed if specified */
  if (single_test_str)
  {
    char *endptr;
    single_test = strtoull (single_test_str, &endptr, 0);  /* Base 0 auto-detects 0x */
    if (*endptr != '\0')
    {
      fprintf (stderr, "Error: Invalid test seed value: %s\n\n", single_test_str);
      print_usage (argv[0]);
      return 1;
    }
  }

  /* Derive explicit_test from whether --unicodes or --gids was specified */
  explicit_test = (explicit_unicodes != NULL || explicit_gids != NULL);

  /* Validate test mode combinations */
  if (explicit_test && single_test != 0)
  {
    fprintf (stderr, "Error: Cannot use both explicit test mode (--unicodes/--gids) and -t/--test\n\n");
    print_usage (argv[0]);
    return 1;
  }

  if (explicit_unicodes && explicit_gids)
  {
    fprintf (stderr, "Error: Cannot specify both --unicodes and --gids\n\n");
    print_usage (argv[0]);
    return 1;
  }

  if (!explicit_test && (explicit_no_layout_closure || explicit_layout_features))
  {
    fprintf (stderr, "Warning: --no-layout-closure and --layout-features are ignored without --unicodes or --gids\n");
  }

  /* Use random seed if not specified */
  if (!seed_specified)
    seed = (unsigned) time (NULL);

  /* Set global configuration */
  _hb_depend_fuzzer_seed = seed;
  _hb_depend_fuzzer_single_test = (uint64_t) single_test;
  _hb_depend_fuzzer_num_tests = num_tests;
  _hb_depend_fuzzer_verbose = verbose;
  _hb_depend_fuzzer_report_over_approximation = report_over_approximation;
  _hb_depend_fuzzer_report_under_approximation = report_under_approximation;
  _hb_depend_fuzzer_inject_over_approx = inject_over_approx;
  _hb_depend_fuzzer_inject_under_approx = inject_under_approx;

  /* Set explicit test mode configuration */
  _hb_depend_fuzzer_explicit_test = explicit_test;
  _hb_depend_fuzzer_explicit_unicodes = explicit_unicodes;
  _hb_depend_fuzzer_explicit_gids = explicit_gids;
  _hb_depend_fuzzer_explicit_no_layout_closure = explicit_no_layout_closure;
  _hb_depend_fuzzer_explicit_layout_features = explicit_layout_features;

  /* TAP version must be the first line */
  printf ("TAP version 14\n");

  /* Print test mode info */
  if (explicit_test)
  {
    if (explicit_unicodes)
      printf ("# Explicit test mode: --unicodes=%s\n", explicit_unicodes);
    else if (explicit_gids)
      printf ("# Explicit test mode: --gids=%s\n", explicit_gids);

    if (explicit_no_layout_closure)
      printf ("# Using --no-layout-closure\n");
    if (explicit_layout_features)
      printf ("# Using --layout-features=%s\n", explicit_layout_features);
  }
  else if (single_test == 0)
    printf ("# Using seed: %u, running %u tests per font\n", seed, num_tests);
  else
    printf ("# Running single test: 0x%016llx\n", (unsigned long long) single_test);

  /* Process font files (glib leaves non-option arguments in argv[1..argc-1]) */
  int file_count = 0;
  for (int i = 1; i < argc; i++)
  {
    file_count++;
    hb_blob_t *blob = hb_blob_create_from_file_or_fail (argv[i]);
    if (!blob)
    {
      fprintf (stderr, "Error: Could not load font file: %s\n", argv[i]);
      return 1;
    }

    unsigned len = 0;
    const char *font_data = hb_blob_get_data (blob, &len);
    printf ("# %s (%u bytes)\n", argv[i], len);

    /* Set global so fuzzer can log font path on mismatch */
    _hb_depend_fuzzer_current_font_path = argv[i];

    LLVMFuzzerTestOneInput ((const uint8_t *) font_data, len);

    /* Clear global after processing */
    _hb_depend_fuzzer_current_font_path = NULL;

    printf ("ok %d - %s\n", file_count, argv[i]);

    hb_blob_destroy (blob);
  }

  printf ("1..%d\n", file_count);

  return 0;
}
