// Append a one-shot stop marker to a Protracker .MOD file so the Amiga
// audio HAL (PTPlayer-based) can honor `jlAudioPlayMod(loop=false)`.
//
// PTPlayer has no built-in song-end signal -- songs loop forever
// unless an `E8FF` effect command is encountered, which latches into
// the public `mt_E8Trigger` byte. Our halAudioFrameTick polls that
// byte and clears mt_Enable when the marker fires, but the marker has
// to actually be IN the .MOD for the polling to ever match.
//
// This tool injects a new pattern at the end of the song that
// contains exactly one E8FF effect on row 0 and is otherwise empty.
// The order table is extended to play that pattern once after the
// existing song. Result: the song plays normally, then the marker
// pattern fires, mt_E8Trigger goes 0xFF, and PlayMod(loop=false)
// stops cleanly. Other replayers (libxmp on DOS/ST, NTP on IIgs)
// either ignore E8FF or treat it as a no-op filter command -- on
// those ports the engine's own song-end detection is what stops the
// song, so the injected marker is harmless there.
//
// Usage: modloopend input.mod output.mod
//
// The existing pattern data is not modified -- only the order table,
// song length, and a new pattern at the end are written.

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

#define MOD_HEADER_SIZE        1084
#define MOD_SONG_LENGTH_OFFSET 950
#define MOD_RESTART_OFFSET     951
#define MOD_ORDER_TABLE_OFFSET 952
#define MOD_ORDER_TABLE_LEN    128
#define MOD_MAGIC_OFFSET       1080
#define MOD_MAGIC_LEN          4
#define MOD_PATTERN_BYTES      1024
#define MOD_MAX_PATTERNS       128
#define MOD_MIN_BYTES          (MOD_HEADER_SIZE + MOD_PATTERN_BYTES)

// E8FF on row 0, channel 0. Per Protracker pattern row layout:
//   byte 0 (SSSSPPPP): sample hi nibble + period hi nibble = 0x00
//   byte 1 (PPPPPPPP): period lo byte = 0x00
//   byte 2 (SSSSEEEE): sample lo nibble + effect cmd = 0x0E
//   byte 3 (PPPPPPPP): effect param = 0xFF
#define E8FF_BYTE_0 0x00
#define E8FF_BYTE_1 0x00
#define E8FF_BYTE_2 0x0E
#define E8FF_BYTE_3 0xFF


// ----- Prototypes -----

static bool magicMatches(const uint8_t *header);
static int  process(const char *inPath, const char *outPath);


// ----- Internal helpers (alphabetical) -----

static bool magicMatches(const uint8_t *header) {
    static const char *known[] = {
        "M.K.", "M!K!", "FLT4", "FLT8", "6CHN", "8CHN", "CD81", "OKTA", "OCTA"
    };
    size_t i;

    for (i = 0; i < sizeof(known) / sizeof(known[0]); i++) {
        if (memcmp(header + MOD_MAGIC_OFFSET, known[i], MOD_MAGIC_LEN) == 0) {
            return true;
        }
    }
    return false;
}


static int process(const char *inPath, const char *outPath) {
    FILE    *fp;
    uint8_t *buf;
    long     fileSize;
    size_t   readBytes;
    size_t   newSize;
    int      i;
    int      songLength;
    int      maxPattern;
    int      patternCount;
    long     samplesOffset;
    long     samplesLen;
    uint8_t *newBuf;
    uint8_t *newPatternStart;

    fp = fopen(inPath, "rb");
    if (fp == NULL) {
        fprintf(stderr, "modloopend: cannot open %s\n", inPath);
        return 2;
    }

    if (fseek(fp, 0, SEEK_END) != 0) {
        fprintf(stderr, "modloopend: seek failed on %s\n", inPath);
        fclose(fp);
        return 2;
    }
    fileSize = ftell(fp);
    if (fileSize < MOD_MIN_BYTES) {
        fprintf(stderr, "modloopend: %s is %ld bytes, too small for a MOD\n", inPath, fileSize);
        fclose(fp);
        return 2;
    }
    rewind(fp);

    buf = (uint8_t *)malloc((size_t)fileSize);
    if (buf == NULL) {
        fprintf(stderr, "modloopend: out of memory\n");
        fclose(fp);
        return 2;
    }
    readBytes = fread(buf, 1, (size_t)fileSize, fp);
    fclose(fp);
    if (readBytes != (size_t)fileSize) {
        fprintf(stderr, "modloopend: short read on %s\n", inPath);
        free(buf);
        return 2;
    }

    if (!magicMatches(buf)) {
        fprintf(stderr, "modloopend: %s does not have a Protracker magic at offset 1080\n", inPath);
        free(buf);
        return 2;
    }

    songLength = buf[MOD_SONG_LENGTH_OFFSET];
    if (songLength <= 0 || songLength >= MOD_ORDER_TABLE_LEN) {
        fprintf(stderr, "modloopend: %s has unusable song length %d\n", inPath, songLength);
        free(buf);
        return 2;
    }

    maxPattern = 0;
    for (i = 0; i < MOD_ORDER_TABLE_LEN; i++) {
        int p = buf[MOD_ORDER_TABLE_OFFSET + i];
        if (p > maxPattern) {
            maxPattern = p;
        }
    }
    patternCount = maxPattern + 1;
    if (patternCount >= MOD_MAX_PATTERNS) {
        fprintf(stderr, "modloopend: %s already uses pattern %d, no room for stop marker\n",
                inPath, maxPattern);
        free(buf);
        return 2;
    }

    samplesOffset = MOD_HEADER_SIZE + (long)patternCount * MOD_PATTERN_BYTES;
    if (samplesOffset > fileSize) {
        fprintf(stderr, "modloopend: %s claims %d patterns but file is only %ld bytes\n",
                inPath, patternCount, fileSize);
        free(buf);
        return 2;
    }
    samplesLen = fileSize - samplesOffset;

    newSize = (size_t)fileSize + MOD_PATTERN_BYTES;
    newBuf = (uint8_t *)malloc(newSize);
    if (newBuf == NULL) {
        fprintf(stderr, "modloopend: out of memory\n");
        free(buf);
        return 2;
    }

    // Copy header + existing patterns.
    memcpy(newBuf, buf, (size_t)samplesOffset);

    // New pattern: 1024 bytes of zeros except row 0 channel 0 = E8FF.
    newPatternStart = newBuf + samplesOffset;
    memset(newPatternStart, 0, MOD_PATTERN_BYTES);
    newPatternStart[0] = E8FF_BYTE_0;
    newPatternStart[1] = E8FF_BYTE_1;
    newPatternStart[2] = E8FF_BYTE_2;
    newPatternStart[3] = E8FF_BYTE_3;

    // Append samples after the new pattern.
    if (samplesLen > 0) {
        memcpy(newBuf + samplesOffset + MOD_PATTERN_BYTES, buf + samplesOffset, (size_t)samplesLen);
    }

    // Patch song length: now plays one extra position pointing at the
    // new pattern index.
    newBuf[MOD_SONG_LENGTH_OFFSET]                       = (uint8_t)(songLength + 1);
    newBuf[MOD_ORDER_TABLE_OFFSET + songLength]          = (uint8_t)patternCount;

    free(buf);

    fp = fopen(outPath, "wb");
    if (fp == NULL) {
        fprintf(stderr, "modloopend: cannot open %s for writing\n", outPath);
        free(newBuf);
        return 2;
    }
    if (fwrite(newBuf, 1, newSize, fp) != newSize) {
        fprintf(stderr, "modloopend: short write on %s\n", outPath);
        fclose(fp);
        free(newBuf);
        return 2;
    }
    fclose(fp);
    free(newBuf);

    printf("modloopend: %s -> %s (added pattern %d as stop marker, %zu bytes)\n",
           inPath, outPath, patternCount, newSize);
    return 0;
}


// ----- Public API (alphabetical) -----

int main(int argc, char **argv) {
    if (argc != 3) {
        fprintf(stderr, "usage: modloopend input.mod output.mod\n");
        return 2;
    }
    return process(argv[1], argv[2]);
}
