
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "kernel.h"
#include "swis.h"
#include "Global/RISCOS.h"

#include "debugrep.h"
#include "usbjoystick.h"
#include "errors.h"
#include "joyhelp.h"
#include "cmdmap.h"
#include "device.h"
#include "map.h"
#include "adc.h"

#include "config.h"


// Largest config file we will read into memory. Far above any realistic
// per-device store; a bound just so a corrupt/huge file can't make us try
// to allocate the world.
#define CONFIG_MAX_FILE_SIZE  (64 * 1024)


// One character of a device serial, sanitised for use inside the config
// file's [section] header. Anything that would break that line - control
// chars, spaces, or the []; = punctuation the parser keys on - becomes
// '_'. One-way: this makes a stable key, not a reversible encoding.
static char sanitise_serial_char(char c)
{
  if (c <= ' ') return '_';
  switch (c) {
    case '[':
    case ']':
    case ';':
    case '=':
      return '_';
    default:
      return c;
  }
}


_kernel_oserror *config_fingerprint(uint32_t slot, char *buf, size_t buflen)
{
  _kernel_oserror *err = validate_joystick_slot("config_fingerprint", slot);
  if (err) return err;

  err = validate_joystick_slot_active("config_fingerprint", slot);
  if (err) return err;

  struct joydata_struct *j = &joy_data[slot];

  // Tier 2 prefix: "vvvv:pppp". snprintf keeps us inside buflen even if a
  // caller passes an undersized buffer (shouldn't happen - the contract is
  // CONFIG_FINGERPRINT_MAX - but never overflow on the strength of that).
  int n = snprintf(buf, buflen, "%04x:%04x",
                   (unsigned)(j->vendor_id & 0xffff),
                   (unsigned)(j->product_id & 0xffff));
  if (n < 0 || (size_t)n >= buflen) {
    debug_printf("config_fingerprint: buffer too small for id prefix (slot %u)\n", slot);
    return geterror(Error_BadNoParms);
  }
  size_t pos = (size_t)n;

  // Some devices report a serial that isn't a stable per-unit id (cheap
  // Xbox360 clones change it every enumeration - see doc.Config). Their
  // driver carries JOY_QUIRK_NO_SERIAL; for those we stay at the Tier 2
  // VID:PID key rather than baking a value that won't match next replug.
  int trust_serial = !(j->device && (j->device->quirks & JOY_QUIRK_NO_SERIAL));

  // Tier 1 only: the device has a genuine serial (set_up_joystick leaves it
  // empty when iSerialNumber was 0) and its driver doesn't distrust it, so
  // append ":serial=<sanitised>".
  if (trust_serial && j->serial[0] != '\0') {
    static const char tag[] = ":serial=";
    if (pos + sizeof(tag) - 1 >= buflen) {
      debug_printf("config_fingerprint: buffer too small for serial tag (slot %u)\n", slot);
      return geterror(Error_BadNoParms);
    }
    memcpy(buf + pos, tag, sizeof(tag) - 1);
    pos += sizeof(tag) - 1;

    for (size_t i = 0; j->serial[i] != '\0' && i < USB_MAX_STRING_LEN; i++) {
      if (pos + 1 >= buflen) {   // leave room for the NUL
        debug_printf("config_fingerprint: serial truncated to fit buffer (slot %u)\n", slot);
        break;
      }
      buf[pos++] = sanitise_serial_char(j->serial[i]);
    }
    buf[pos] = '\0';
  }

  debug_printf("config_fingerprint: slot %u -> '%s'\n", slot, buf);
  return NULL;
}


_kernel_oserror *command_fingerprint(const char *args, int32_t argc)
{
  if (argc != 1) {
    debug_printf("command_fingerprint: Bad arguments, argc isn't 1 (it is %d)\n", argc);
    return geterror(Error_BadNoParms);
  }

  uint32_t slot;
  if (sscanf(args, "%u", &slot) != 1) {
    debug_printf("command_fingerprint: couldn't parse slot argument\n");
    return geterror(Error_BadNoParms);
  }

  char fp[CONFIG_FINGERPRINT_MAX];
  _kernel_oserror *err = config_fingerprint(slot, fp, sizeof(fp));
  if (err) return err;

  printf("Joystick slot %u fingerprint: %s\n", slot, fp);
  return NULL;
}




/* ------------------------------------------------------------------ *
 * Config-store reading (see doc.Config)                              *
 * ------------------------------------------------------------------ */

// One device section's parsed contents. The mapping is stored as a flat list
// of bindings (one per bind= line, in the *USBJoystick_Map grammar), applied
// wholesale via binding_write. "stick" (the device->slot number) is separate,
// not a binding.
struct parsed_section {
  int      seen;            // the fingerprint's [section] was found

  int      has_stick;   int32_t  stick;

  joy_binding binds[JB_MAX_BINDINGS];   // collected bind= lines
  uint32_t    nbinds;
};


// Load CONFIG_READ_PATH into a freshly malloc'd, NUL-terminated buffer.
// Returns NULL (and never an error) if there is no readable file - a
// missing file, or an unset Choices: path (module running before !Boot),
// are both just "nothing to load". Caller frees.
static char *config_load_file(void)
{
  int objtype = 0;
  int length  = 0;

  // OS_File 17: read catalogue info by name. Any error (e.g. the Choices:
  // path variable isn't set yet) means "treat as no file".
  _kernel_oserror *e = _swix(OS_File, _INR(0,1) | _OUT(0) | _OUT(4),
                             17, CONFIG_READ_PATH, &objtype, &length);
  if (e) {
    debug_printf("config_load_file: OS_File 17 on '%s' failed (%s) - no config\n",
                 CONFIG_READ_PATH, e->errmess);
    return NULL;
  }

  if (objtype != 1) {   // 0 = absent, 2 = directory, 3 = image file
    debug_printf("config_load_file: '%s' is not a file (objtype %d) - no config\n",
                 CONFIG_READ_PATH, objtype);
    return NULL;
  }

  if (length < 0 || length > CONFIG_MAX_FILE_SIZE) {
    debug_printf("config_load_file: '%s' length %d out of range - ignoring\n",
                 CONFIG_READ_PATH, length);
    return NULL;
  }

  char *buf = malloc((size_t)length + 1);
  if (!buf) {
    debug_printf("config_load_file: out of memory for %d bytes\n", length + 1);
    return NULL;
  }

  // OS_File 16: load named file at buf (R3=0 => ignore the file's own load
  // address and use R2).
  e = _swix(OS_File, _INR(0,3), 16, CONFIG_READ_PATH, (int)buf, 0);
  if (e) {
    debug_printf("config_load_file: OS_File 16 on '%s' failed (%s)\n",
                 CONFIG_READ_PATH, e->errmess);
    free(buf);
    return NULL;
  }

  buf[length] = '\0';
  return buf;
}


// The file's format version, from the "Format:N" marker in its header comment
// (0 if absent - i.e. a pre-versioned or foreign file). Scans the whole buffer;
// the marker only ever appears in the one header line we write.
static int config_file_version(const char *buf)
{
  const char *p = strstr(buf, "Format:");
  if (!p) return 0;
  return atoi(p + 7);   // strlen("Format:")
}


// Trim ASCII spaces/tabs from both ends of s in place, returning s.
static char *trim(char *s)
{
  while (*s == ' ' || *s == '\t') s++;
  char *end = s + strlen(s);
  while (end > s && (end[-1] == ' ' || end[-1] == '\t')) end--;
  *end = '\0';
  return s;
}


// Split value on commas in place, recording up to maxtoks token starts
// (each NUL-terminated and trimmed). Returns the number of tokens found.
static uint32_t split_csv(char *value, char **toks, uint32_t maxtoks)
{
  uint32_t n = 0;
  if (*value == '\0') return 0;   // empty value = zero tokens

  char *p = value;
  toks[n++] = p;
  while (*p && n < maxtoks) {
    if (*p == ',') {
      *p = '\0';
      toks[n++] = p + 1;
    }
    p++;
  }
  // trim each token
  for (uint32_t i = 0; i < n; i++)
    toks[i] = trim(toks[i]);
  return n;
}


// Case-insensitive ASCII equality. Inline lower-casing, NOT the library
// tolower() (which can data-abort from module code - see c.cmdbind).
static int ci_eq(const char *a, const char *b)
{
  while (*a && *b) {
    int ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
    int cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
    if (ca != cb) return 0;
    a++; b++;
  }
  return *a == *b;
}


// Handle one "key=value" line inside our fingerprint's section. slot is needed
// so bind= lines can resolve axis names against the device's axes[].
static void parse_key(uint32_t slot, struct parsed_section *s, char *key, char *value)
{
  if (strcmp(key, "stick") == 0) {
    s->has_stick = TRUE;
    s->stick = (int32_t) atoi(value);
  }
  else if (strcmp(key, "bind") == 0) {
    // bind=<source>,<target>[,invert] - the *USBJoystick_Map grammar.
    char *toks[3];
    uint32_t nt = split_csv(value, toks, 3);
    if (nt < 2) {
      debug_printf("config: bad bind line (need source,target): '%s'\n", value);
      return;
    }
    int invert = (nt >= 3) && ci_eq(toks[2], "invert");

    if (s->nbinds >= JB_MAX_BINDINGS) {
      debug_printf("config: too many bind lines (max %u)\n", (unsigned)JB_MAX_BINDINGS);
      return;
    }

    joy_binding b;
    _kernel_oserror *e = binding_parse_spec(slot, toks[0], toks[1], invert, &b);
    if (e) {
      debug_printf("config: bad bind '%s,%s' - skipping\n", toks[0], toks[1]);
      return;
    }
    s->binds[s->nbinds++] = b;
  }
  // Unknown keys (e.g. profile=) are ignored, not errors - see doc.Config.
}


// Parse the whole file, filling *out for the section whose header matches
// fingerprint. buf is modified in place (comment stripping, NUL splits). slot
// is threaded through so bind= lines can resolve axis names.
static void parse_config(uint32_t slot, char *buf, const char *fingerprint, struct parsed_section *out)
{
  enum { SEC_NONE, SEC_OURS, SEC_OTHER } cur = SEC_NONE;

  char *line = buf;
  while (line && *line) {
    char *next = strchr(line, '\n');
    if (next) { *next = '\0'; next++; }

    // Strip a trailing CR (in case the file was edited on a CRLF host).
    size_t len = strlen(line);
    if (len && line[len - 1] == '\r') line[len - 1] = '\0';

    char *p = trim(line);

    // Trailing ';' comment. '#'/'|' at the start are whole-line comments.
    char *semi = strchr(p, ';');
    if (semi) *semi = '\0';
    p = trim(p);

    if (*p == '\0' || *p == '#' || *p == '|') {
      line = next;
      continue;
    }

    if (*p == '[') {
      char *close = strchr(p, ']');
      if (close) {
        *close = '\0';
        char *name = trim(p + 1);
        if (strcmp(name, fingerprint) == 0) {
          cur = SEC_OURS;
          out->seen = TRUE;
        }
        else {
          cur = SEC_OTHER;   // [global] and other devices - not us
        }
      }
      line = next;
      continue;
    }

    if (cur == SEC_OURS) {
      char *eq = strchr(p, '=');
      if (eq) {
        *eq = '\0';
        char *key = trim(p);
        char *value = trim(eq + 1);
        parse_key(slot, out, key, value);
      }
    }

    line = next;
  }
}


// Apply the parsed section to slot: the stick number via joy_set_stick, and the
// bindings via binding_write (which replaces the slot's table wholesale, then
// compiles). Errors are logged and skipped - one bad saved value must not abort
// a device attach.
static void apply_section(uint32_t slot, const struct parsed_section *s)
{
  _kernel_oserror *e;

  if (s->has_stick) {
    e = joy_set_stick(slot, s->stick);
    if (e) debug_printf("config: stick apply failed: %s\n", e->errmess);
  }

  // Replace the slot's table with the section's bindings (empty section => empty
  // mapping; a saved file always carries the full table). Old-format files never
  // reach here - config_apply's version check rejects them first.
  e = binding_write(slot, s->binds, s->nbinds);
  if (e) debug_printf("config: binding_write failed: %s\n", e->errmess);
}


void config_apply(uint32_t slot)
{
  char fp[CONFIG_FINGERPRINT_MAX];
  if (config_fingerprint(slot, fp, sizeof(fp)) != NULL)
    return;   // bad/inactive slot - config_fingerprint already logged

  char *buf = config_load_file();
  if (!buf)
    return;   // no config file - keep the default auto-mapping

  // Ignore a file of a different format version (e.g. a v1 file from a released
  // build): keep the auto-mapping. A later SaveMappings replaces it with a v2
  // file. See CONFIG_FORMAT_VERSION.
  if (config_file_version(buf) != CONFIG_FORMAT_VERSION) {
    debug_printf("config_apply: file is not format %d - ignoring, keeping auto-map\n",
                 CONFIG_FORMAT_VERSION);
    free(buf);
    return;
  }

  struct parsed_section s;
  memset(&s, 0, sizeof(s));
  parse_config(slot, buf, fp, &s);

  if (!s.seen) {
    debug_printf("config_apply: no section for '%s' - keeping defaults\n", fp);
    free(buf);
    return;
  }

  // An empty section (no bindings, no stick) means "nothing saved" - keep the
  // auto-mapping. To go back to the default map for a device that DOES have a
  // saved section, use *USBJoystick_RevertToDefaultMap (which deletes it).
  if (s.nbinds == 0 && !s.has_stick) {
    debug_printf("config_apply: '%s' has no saved mapping - keeping auto-map\n", fp);
    free(buf);
    return;
  }

  debug_printf("config_apply: applying saved mapping for '%s'\n", fp);
  apply_section(slot, &s);
  free(buf);
}




/* ------------------------------------------------------------------ *
 * Config-store writing (see doc.Config)                              *
 * ------------------------------------------------------------------ */

// Append s to out (cap bytes) at pos, defensively truncating rather than
// overrunning (the caller sizes cap generously - see config_save_mapping).
// Returns the new length. out is NOT NUL-terminated here; the length is
// authoritative, which is all OS_File 10 needs.
static size_t append_str(char *out, size_t cap, size_t pos, const char *s)
{
  size_t n = strlen(s);
  if (pos >= cap) return pos;
  if (pos + n > cap) n = cap - pos;
  memcpy(out + pos, s, n);
  return pos + n;
}


// Emit one "key=x,y\n" axis line, writing "off" for an unmapped (negative)
// index - the same keyword the reader and the *command accept.
// Generate slot's fresh config section into out: the [fingerprint] header, the
// stick number, then one bind= line per binding - the exact grammar
// parse_key/binding_parse_spec read back.
static size_t format_section(uint32_t slot, const char *fp, char *out, size_t cap)
{
  struct joydata_struct *j = &joy_data[slot];
  char line[CONFIG_FINGERPRINT_MAX + 8];
  size_t pos = 0;

  snprintf(line, sizeof line, "\n[%s]\n", fp);
  pos = append_str(out, cap, pos, line);
  pos = append_str(out, cap, pos, "profile=Default\n");

  // stick: omit the line entirely when unmapped, so the reader leaves the
  // default stick-number assignment alone.
  if (j->mapped_number != NOT_MAPPED) {
    snprintf(line, sizeof line, "stick=%d\n", (int)j->mapped_number);
    pos = append_str(out, cap, pos, line);
  }

  // The mapping itself: one bind= line per binding, in the *USBJoystick_Map
  // grammar (source,target[,invert]). This IS the whole binding table, so it
  // covers every target kind uniformly (stick axes/buttons, mouse, key, ADC).
  for (uint32_t i = 0; i < j->num_bindings; i++) {
    const joy_binding *b = &j->bindings[i];
    char sbuf[32], tbuf[32];
    binding_format_source(b, slot, sbuf, sizeof sbuf);
    binding_format_target(b, tbuf, sizeof tbuf);
    snprintf(line, sizeof line, "bind=%s,%s%s\n", sbuf, tbuf,
             (b->flags & JB_FLAG_INVERT) ? ",invert" : "");
    pos = append_str(out, cap, pos, line);
  }

  return pos;
}


// Classify one raw line (len bytes, no newline) against fingerprint:
//   0 = not a section header (blank/comment/key line)
//   1 = a section we regenerate and therefore drop here: the [fingerprint]
//       being rewritten, OR [global] (config_rewrite always re-emits it from
//       live state)
//   2 = some other [section] header (another device) - kept verbatim
// Works on a bounded copy so the source stays byte-for-byte intact for
// verbatim copy-through.
static int classify_line(const char *line, size_t len, const char *fp)
{
  char tmp[CONFIG_FINGERPRINT_MAX + 4];
  if (len >= sizeof tmp) len = sizeof tmp - 1;
  memcpy(tmp, line, len);
  tmp[len] = '\0';

  char *p = trim(tmp);
  char *semi = strchr(p, ';');
  if (semi) *semi = '\0';
  p = trim(p);

  if (*p != '[') return 0;

  char *close = strchr(p, ']');
  if (!close) return 0;
  *close = '\0';
  char *name = trim(p + 1);

  return (strcmp(name, fp) == 0 || strcmp(name, "global") == 0) ? 1 : 2;
}


static void trim_trailing_blank_lines(const char *out, size_t *pos)
{
  while (*pos >= 2 && out[*pos - 1] == '\n' && out[*pos - 2] == '\n')
    (*pos)--;
}


// Copy old (the existing file text) into out, dropping the fingerprint
// section AND [global] (both regenerated by config_rewrite from live state).
// Everything else - the leading header comment, other devices' sections -
// is preserved verbatim. Returns bytes written.
static size_t copy_excluding_section(const char *old, const char *fp,
                                     char *out, size_t cap)
{
  size_t pos = 0;
  int skipping = FALSE;
  const char *line = old;

  while (*line) {
    const char *eol = strchr(line, '\n');
    const char *next = eol ? eol + 1 : line + strlen(line);
    size_t linelen = (eol ? (size_t)(eol - line) : strlen(line));
    // strip a trailing CR for classification purposes only
    size_t classlen = linelen;
    if (classlen && line[classlen - 1] == '\r') classlen--;

    int c = classify_line(line, classlen, fp);

    if (c == 1) {                 // a regenerated section - start skipping
      skipping = TRUE;
    }
    else if (c == 2) {            // another section - stop skipping, keep it
      skipping = FALSE;
    }

    if (!skipping) {              // copy this line (with its newline) verbatim
      size_t span = (size_t)(next - line);
      if (pos + span > cap) span = (pos < cap) ? cap - pos : 0;
      memcpy(out + pos, line, span);
      pos += span;
    }

    line = next;
  }

  return pos;
}


// Emit a fresh [global] section from the live module-wide settings. Written
// on every rewrite so the file stays in step with the current toggles.
static size_t format_global(char *out, size_t cap, size_t pos)
{
  char line[64];
  pos = append_str(out, cap, pos, "\n[global]\n");
  snprintf(line, sizeof line, "emulate_serialport=%s\n", emulate_serial_port_on ? "yes" : "no");
  pos = append_str(out, cap, pos, line);
  snprintf(line, sizeof line, "emulate_joy=%s\n", emulate_joy_on ? "yes" : "no");
  pos = append_str(out, cap, pos, line);
  snprintf(line, sizeof line, "emulate_adc=%s\n", emulate_adc_on ? "yes" : "no");
  pos = append_str(out, cap, pos, line);
  snprintf(line, sizeof line, "mouse_control=%s\n", mouse_control_on ? "yes" : "no");
  pos = append_str(out, cap, pos, line);
  snprintf(line, sizeof line, "keyboard_control=%s\n", keyboard_control_on ? "yes" : "no");
  pos = append_str(out, cap, pos, line);
  return pos;
}


// Parse a yes/no-style value. Returns TRUE/FALSE, or def if unrecognised.
static int parse_bool(const char *v, int def)
{
  if (!strcmp(v, "yes") || !strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "on"))
    return TRUE;
  if (!strcmp(v, "no") || !strcmp(v, "false") || !strcmp(v, "0") || !strcmp(v, "off"))
    return FALSE;
  return def;
}


// Read the [global] section and apply the module-wide Emulate + Control
// flags. A missing file or missing [global] leaves the current (default)
// flags alone. Sets the flags directly rather than via joy_flag_toggle - no
// need for that SWI/command layer's argument validation when reading our
// own previously-written file.
static void config_apply_global(void)
{
  char *buf = config_load_file();
  if (!buf) return;

  int in_global = FALSE;
  char *line = buf;
  while (*line) {
    char *next = strchr(line, '\n');
    if (next) { *next = '\0'; next++; }
    size_t len = strlen(line);
    if (len && line[len - 1] == '\r') line[len - 1] = '\0';

    char *p = trim(line);
    char *semi = strchr(p, ';');
    if (semi) *semi = '\0';
    p = trim(p);

    if (*p == '\0' || *p == '#' || *p == '|') { line = next; continue; }

    if (*p == '[') {
      char *close = strchr(p, ']');
      if (close) { *close = '\0'; in_global = (strcmp(trim(p + 1), "global") == 0); }
      line = next;
      continue;
    }

    if (in_global) {
      char *eq = strchr(p, '=');
      if (eq) {
        *eq = '\0';
        char *key = trim(p), *value = trim(eq + 1);
        if      (!strcmp(key, "emulate_serialport")) emulate_serial_port_on = parse_bool(value, emulate_serial_port_on);
        else if (!strcmp(key, "emulate_joy"))        emulate_joy_on         = parse_bool(value, emulate_joy_on);
        else if (!strcmp(key, "emulate_adc"))        emulate_adc_on         = parse_bool(value, emulate_adc_on);
        else if (!strcmp(key, "mouse_control"))      mouse_control_on       = parse_bool(value, mouse_control_on);
        else if (!strcmp(key, "keyboard_control"))   keyboard_control_on    = parse_bool(value, keyboard_control_on);
      }
    }

    line = next;
  }

  free(buf);
}


// Write buf[0..len) to <Choices$Write>.<dir>.<leaf> as a Text file,
// creating the <dir> directory if need be.
static _kernel_oserror *config_write_file(const char *buf, size_t len)
{
  char cwrite[256];
  int used = 0;

  // Read Choices$Write directly (rather than GSTrans'ing the name) so an
  // unset variable is an explicit, reportable condition rather than a path
  // silently starting with '.'.
  _kernel_oserror *e = _swix(OS_ReadVarVal, _INR(0,4) | _OUT(2),
                             "Choices$Write", cwrite, (int)sizeof(cwrite) - 1,
                             0, 0, &used);
  if (e || used <= 0) {
    debug_printf("config_write_file: Choices$Write not set - cannot save\n");
    return geterror(Error_NoChoicesWrite);
  }
  cwrite[used] = '\0';

  char dir[300];
  char path[320];
  snprintf(dir,  sizeof dir,  "%s.%s", cwrite, CONFIG_DIR);
  snprintf(path, sizeof path, "%s.%s", dir, CONFIG_LEAFNAME);

  // OS_File 8: create directory (R2=0 => default number of entries). Not an
  // error if it already exists.
  e = _swix(OS_File, _INR(0,2), 8, dir, 0);
  if (e) {
    debug_printf("config_write_file: couldn't create '%s' (%s)\n", dir, e->errmess);
    return e;
  }

  // OS_File 10: save memory block as a typed file (R2 = &FFF = Text).
  e = _swix(OS_File, _INR(0,2) | _INR(4,5),
            10, path, 0xFFF, (int)buf, (int)(buf + len));
  if (e) {
    debug_printf("config_write_file: couldn't save '%s' (%s)\n", path, e->errmess);
    return e;
  }

  debug_printf("config_write_file: wrote %u bytes to '%s'\n", (unsigned)len, path);
  return NULL;
}


// Shared by save and revert: rebuild the file with our section excluded,
// optionally appending a freshly-formatted section, and write it out.
// append_ours == FALSE leaves the device with no section (=> auto-mapping).
static _kernel_oserror *config_rewrite(uint32_t slot, int append_ours)
{
  char fp[CONFIG_FINGERPRINT_MAX];
  _kernel_oserror *e = config_fingerprint(slot, fp, sizeof(fp));
  if (e) return e;

  char *old = config_load_file();         // NULL if there's no file yet

  // Don't merge into an older/foreign-format file - replace it. This keeps us
  // from carrying pre-v2 sections (other devices) into a v2 file; those devices
  // fall back to auto-map until re-saved. See CONFIG_FORMAT_VERSION.
  if (old && config_file_version(old) != CONFIG_FORMAT_VERSION) {
    debug_printf("config_rewrite: existing file is not format %d - replacing it\n",
                 CONFIG_FORMAT_VERSION);
    free(old);
    old = NULL;
  }

  size_t oldlen = old ? strlen(old) : 0;

  // Generous headroom: the old text, plus a freshly-regenerated [global] and
  // our (largest-case) device section, plus a header line.
  size_t cap = oldlen + 2048;
  char *out = malloc(cap);
  if (!out) {
    free(old);
    debug_printf("config_rewrite: out of memory (%u bytes)\n", (unsigned)cap);
    return geterror(Error_BadNoParms);
  }

  size_t pos = 0;
  if (!old) {
    // Brand-new (or format-replaced) file: write the versioned header.
    char header[64];
    snprintf(header, sizeof header,
             "# USBJoystick config - hand-editable   Format:%d\n", CONFIG_FORMAT_VERSION);
    pos = append_str(out, cap, pos, header);
  }
  else {
    pos = copy_excluding_section(old, fp, out, cap);
  }

  // Always re-emit [global] from live state (copy_excluding_section dropped
  // the old one), then our device section if we're saving rather than
  // reverting.
  trim_trailing_blank_lines(out, &pos);
  pos = format_global(out, cap, pos);

  if (append_ours)
    pos += format_section(slot, fp, out + pos, cap - pos);

  e = config_write_file(out, pos);

  free(out);
  free(old);
  return e;
}


_kernel_oserror *config_save_mapping(uint32_t slot)
{
  _kernel_oserror *e = validate_joystick_slot("Joystick_SaveMapping", slot);
  if (e) return e;
  e = validate_joystick_slot_active("Joystick_SaveMapping", slot);
  if (e) return e;

  return config_rewrite(slot, TRUE);
}


_kernel_oserror *config_revert_to_default_map(uint32_t slot)
{
  _kernel_oserror *e = validate_joystick_slot("Joystick_RevertToDefaultMap", slot);
  if (e) return e;
  e = validate_joystick_slot_active("Joystick_RevertToDefaultMap", slot);
  if (e) return e;

  // Drop the saved section first, so the auto_map() below (which re-reads
  // the store via config_apply) sees no override and produces clean
  // defaults. A missing file just means "nothing to remove".
  e = config_rewrite(slot, FALSE);
  if (e) return e;

  auto_map(slot);
  return NULL;
}


// Persist the module-wide [global] settings (currently: the Emulate +
// Control gates) without touching any device's own section - the Save path
// for Joystick_Control/Joystick_Emulate, independent of config_save_mapping
// so the two can never clash over the same part of the file. Reuses
// copy_excluding_section with "" as the fingerprint to exclude: a real
// fingerprint always starts "vvvv:pppp" (config_fingerprint), so "" can
// never match one and only the old [global] block - which
// copy_excluding_section already drops unconditionally - gets removed,
// leaving every device section untouched.
_kernel_oserror *config_save_global(void)
{
  char *old = config_load_file();

  if (old && config_file_version(old) != CONFIG_FORMAT_VERSION) {
    debug_printf("config_save_global: existing file is not format %d - replacing it\n",
                 CONFIG_FORMAT_VERSION);
    free(old);
    old = NULL;
  }

  size_t oldlen = old ? strlen(old) : 0;
  size_t cap = oldlen + 256;   // headroom for a freshly-regenerated [global] only
  char *out = malloc(cap);
  if (!out) {
    free(old);
    debug_printf("config_save_global: out of memory (%u bytes)\n", (unsigned)cap);
    return geterror(Error_BadNoParms);
  }

  size_t pos = 0;
  if (!old) {
    char header[64];
    snprintf(header, sizeof header,
             "# USBJoystick config - hand-editable   Format:%d\n", CONFIG_FORMAT_VERSION);
    pos = append_str(out, cap, pos, header);
  }
  else {
    pos = copy_excluding_section(old, "", out, cap);
  }

  trim_trailing_blank_lines(out, &pos);
  pos = format_global(out, cap, pos);

  _kernel_oserror *e = config_write_file(out, pos);

  free(out);
  free(old);
  return e;
}


void config_reload_all(void)
{
  // Module-wide [global] settings first (once), then the per-device mappings.
  config_apply_global();

  for (uint32_t slot = 0; slot < JOY_MAX; slot++) {
    if (joy_data[slot].in_use && joy_data[slot].open)
      config_apply(slot);
  }
}




/* ------------------------------------------------------------------ *
 * SWI and *command wrappers                                          *
 * ------------------------------------------------------------------ */

_kernel_oserror *swi_joystick_save_mapping(_kernel_swi_regs *r)
{
  // R1 (profile) is reserved for the future multi-profile feature - only
  // the default profile exists today, so it's accepted and ignored.
  return config_save_mapping((uint32_t) r->r[0]);
}


_kernel_oserror *swi_joystick_revert_to_default_map(_kernel_swi_regs *r)
{
  return config_revert_to_default_map((uint32_t) r->r[0]);
}


_kernel_oserror *swi_joystick_reload_config(_kernel_swi_regs *r)
{
  IGNORE(r);
  config_reload_all();
  return NULL;
}


_kernel_oserror *swi_joystick_save_global(_kernel_swi_regs *r)
{
  IGNORE(r);
  return config_save_global();
}


_kernel_oserror *command_save_mapping(const char *args, int32_t argc)
{
  if (argc < 1 || argc > 2)
    return geterror(Error_BadNoParms);

  uint32_t slot;
  if (sscanf(args, "%u", &slot) != 1)
    return geterror(Error_BadNoParms);

  return config_save_mapping(slot);
}


_kernel_oserror *command_revert_to_default_map(const char *args, int32_t argc)
{
  if (argc != 1)
    return geterror(Error_BadNoParms);

  uint32_t slot;
  if (sscanf(args, "%u", &slot) != 1)
    return geterror(Error_BadNoParms);

  return config_revert_to_default_map(slot);
}


_kernel_oserror *command_reload_config(const char *args, int32_t argc)
{
  IGNORE(args);
  IGNORE(argc);
  config_reload_all();
  return NULL;
}


_kernel_oserror *command_save_settings(const char *args, int32_t argc)
{
  IGNORE(args);
  IGNORE(argc);
  return config_save_global();
}
