// mkstlevel: convert a human-readable text level definition into the
// STL3 binary format read by examples/spacetaxi/stLevel.c.
//
// Usage:
//   mkstlevel input.txt output.dat
//
// Input format (.txt, line-oriented, comments start with '#'):
//
//   @name <up-to-23-char level name>
//   @tilebank <0..255>
//   @music <0..255>
//   @bgColor <palette slot 0..15>
//   @borderColor <palette slot 0..15>
//   @taxiSpawn <tileX> <tileY>
//   @accel <xAccel> <yAccel>                           [optional, default 14 14]
//   @gravity <xGrav> <yGrav>                           [optional, default 0 1; yGrav signed]
//   @vicColor <bg1> <bg2> <bg3> <mc0> <mc1> <sc0> <sc1> [optional, default all 0]
//   @pad <letter> <tileX> <tileY> <tileW> [standX]     [up to 10 of these;
//                     letter is a readability label only (not stored);
//                     standX = passenger stand column in pixels, optional
//                     (defaults to the pad's left edge, tileX*8)]
//   @tilemap        -- followed by exactly 25 lines of 40 chars each
//   @colormap       -- followed by exactly 25 lines of 40 chars each
//
// Tilemap / colormap character encoding (chars are tile/palette indices):
//   '.' or ' ' -> 0     (background)
//   '0'..'9'   -> 0..9
//   'A'..'Z'   -> 10..35
//   'a'..'z'   -> 36..61
//
// Per the spacetaxi engine convention:
//   tile index 0       = empty (non-solid)
//   tile index 1..63   = solid (walls)
//   tile index 64..127 = landing-pad surface
//   tile index 128..255 = decorative non-solid
//
// Color cells use the same character->index decoding; cell values are
// palette slots 0..15. Use '0'..'9','a'..'f' for the standard 16
// surface palettes.

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

#define ST_TILEMAP_W       40
#define ST_PLAYFIELD_ROWS  25
#define ST_MAX_PADS        10
#define ST_NAME_MAX        23
#define LINE_MAX           256


typedef struct {
    uint8_t tileX;
    uint8_t tileY;
    uint8_t tileW;
    uint8_t standX;    // passenger stand column, screen pixels
} PadT;


typedef struct {
    char     name[ST_NAME_MAX + 1];
    uint8_t  tileBank;
    uint8_t  music;
    uint8_t  bgColor;
    uint8_t  borderColor;
    uint8_t  taxiSpawnX;
    uint8_t  taxiSpawnY;
    uint8_t  xAccel;
    uint8_t  yAccel;
    int8_t   xGrav;
    int8_t   yGrav;
    uint8_t  bgColor1;
    uint8_t  bgColor2;
    uint8_t  bgColor3;
    uint8_t  spriteMc0;
    uint8_t  spriteMc1;
    uint8_t  sprite0Color;
    uint8_t  sprite1Color;
    uint8_t  padCount;
    PadT     pads[ST_MAX_PADS];
    uint8_t  tilemap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS];
    uint8_t  colormap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS];
} LevelT;


static int   charToIndex(int c, int *outIdx);
static int   parseGrid(FILE *fp, const char *gridName, uint8_t *outCells, int *outLine);
static int   parseLevel(FILE *fp, LevelT *out);
static int   readLine(FILE *fp, char *buf, int n);
static int   stripLine(char *s);
static int   writeOut(const LevelT *L, const char *path);


static int charToIndex(int c, int *outIdx) {
    if (c == '.' || c == ' ') {
        *outIdx = 0;
        return 1;
    }
    if (c >= '0' && c <= '9') { *outIdx = c - '0';        return 1; }
    if (c >= 'A' && c <= 'Z') { *outIdx = 10 + (c - 'A'); return 1; }
    if (c >= 'a' && c <= 'z') { *outIdx = 36 + (c - 'a'); return 1; }
    return 0;
}


// Reads one line; returns >0 on success, 0 on EOF, -1 on error.
// Strips trailing newline. Lines starting with '#' are returned with
// the leading '#' so the caller can decide whether to skip them.
static int readLine(FILE *fp, char *buf, int n) {
    int  c;
    int  k = 0;

    while (k < n - 1) {
        c = fgetc(fp);
        if (c == EOF) {
            buf[k] = '\0';
            return (k == 0) ? 0 : 1;
        }
        if (c == '\n') {
            break;
        }
        if (c == '\r') {
            continue;       // tolerate CRLF
        }
        buf[k++] = (char)c;
    }
    buf[k] = '\0';
    return 1;
}


// Strip leading whitespace; return 1 if blank-or-comment, 0 if data.
static int stripLine(char *s) {
    int  i = 0;
    int  j = 0;
    while (s[i] == ' ' || s[i] == '\t') {
        i++;
    }
    if (s[i] == '\0' || s[i] == '#') {
        return 1;
    }
    while (s[i] != '\0') {
        s[j++] = s[i++];
    }
    s[j] = '\0';
    return 0;
}


static int parseGrid(FILE *fp, const char *gridName, uint8_t *outCells, int *outLine) {
    char  line[LINE_MAX];
    int   row;
    int   col;
    int   idx;

    for (row = 0; row < ST_PLAYFIELD_ROWS; row++) {
        if (readLine(fp, line, sizeof(line)) <= 0) {
            fprintf(stderr, "mkstlevel: %s: unexpected EOF on row %d\n",
                    gridName, row);
            return 0;
        }
        (*outLine)++;
        // Strip CRLF/trailing space but DON'T treat indent as significant;
        // do NOT strip leading whitespace within the grid (chars are literal).
        {
            int  len = (int)strlen(line);
            while (len > 0 && (line[len-1] == ' ' || line[len-1] == '\t')) {
                line[--len] = '\0';
            }
            // Allow a leading '#' comment line inside the grid only if it
            // appears in the first column; otherwise treat as data error.
            if (line[0] == '#') {
                row--;            // re-run this slot
                continue;
            }
            if (len == 0) {
                row--;            // skip blank lines silently
                continue;
            }
            if (len != ST_TILEMAP_W) {
                fprintf(stderr,
                    "mkstlevel: %s line %d: expected %d chars, got %d (\"%s\")\n",
                    gridName, *outLine, ST_TILEMAP_W, len, line);
                return 0;
            }
        }
        for (col = 0; col < ST_TILEMAP_W; col++) {
            if (!charToIndex((unsigned char)line[col], &idx)) {
                fprintf(stderr,
                    "mkstlevel: %s line %d col %d: bad char '%c' (0x%02X)\n",
                    gridName, *outLine, col + 1, line[col],
                    (unsigned)(unsigned char)line[col]);
                return 0;
            }
            outCells[row * ST_TILEMAP_W + col] = (uint8_t)idx;
        }
    }
    return 1;
}


static int parseLevel(FILE *fp, LevelT *out) {
    char  line[LINE_MAX];
    int   lineNo = 0;
    int   gotTilemap = 0;
    int   gotColormap = 0;

    memset(out, 0, sizeof(*out));
    out->bgColor     = 0;
    out->borderColor = 0;
    // Physics defaults match level 1's templates so hand-authored
    // levels without an @accel/@gravity directive still play sanely.
    out->xAccel      = 14;
    out->yAccel      = 14;
    out->xGrav       = 0;
    out->yGrav       = 1;

    while (readLine(fp, line, sizeof(line)) > 0) {
        lineNo++;
        if (stripLine(line)) {
            continue;
        }
        if (line[0] != '@') {
            fprintf(stderr,
                "mkstlevel: line %d: expected @directive or comment, got \"%s\"\n",
                lineNo, line);
            return 0;
        }

        if (strncmp(line, "@name ", 6) == 0) {
            int  len = (int)strlen(line + 6);
            if (len > ST_NAME_MAX) {
                len = ST_NAME_MAX;
            }
            memcpy(out->name, line + 6, (size_t)len);
            out->name[len] = '\0';
        } else if (strncmp(line, "@tilebank ", 10) == 0) {
            out->tileBank = (uint8_t)atoi(line + 10);
        } else if (strncmp(line, "@music ", 7) == 0) {
            out->music = (uint8_t)atoi(line + 7);
        } else if (strncmp(line, "@bgColor ", 9) == 0) {
            out->bgColor = (uint8_t)atoi(line + 9);
        } else if (strncmp(line, "@borderColor ", 13) == 0) {
            out->borderColor = (uint8_t)atoi(line + 13);
        } else if (strncmp(line, "@taxiSpawn ", 11) == 0) {
            int  x = 0;
            int  y = 0;
            if (sscanf(line + 11, "%d %d", &x, &y) != 2) {
                fprintf(stderr, "mkstlevel: line %d: bad taxiSpawn\n", lineNo);
                return 0;
            }
            out->taxiSpawnX = (uint8_t)x;
            out->taxiSpawnY = (uint8_t)y;
        } else if (strncmp(line, "@accel ", 7) == 0) {
            int  ax = 0;
            int  ay = 0;
            if (sscanf(line + 7, "%d %d", &ax, &ay) != 2) {
                fprintf(stderr, "mkstlevel: line %d: bad accel\n", lineNo);
                return 0;
            }
            out->xAccel = (uint8_t)ax;
            out->yAccel = (uint8_t)ay;
        } else if (strncmp(line, "@gravity ", 9) == 0) {
            int  gx = 0;
            int  gy = 0;
            if (sscanf(line + 9, "%d %d", &gx, &gy) != 2) {
                fprintf(stderr, "mkstlevel: line %d: bad gravity\n", lineNo);
                return 0;
            }
            out->xGrav = (int8_t)gx;
            out->yGrav = (int8_t)gy;
        } else if (strncmp(line, "@vicColor ", 10) == 0) {
            int  b1 = 0;
            int  b2 = 0;
            int  b3 = 0;
            int  mc0 = 0;
            int  mc1 = 0;
            int  sc0 = 0;
            int  sc1 = 0;
            if (sscanf(line + 10, "%d %d %d %d %d %d %d",
                       &b1, &b2, &b3, &mc0, &mc1, &sc0, &sc1) != 7) {
                fprintf(stderr, "mkstlevel: line %d: bad vicColor (need 7 values)\n", lineNo);
                return 0;
            }
            out->bgColor1     = (uint8_t)b1;
            out->bgColor2     = (uint8_t)b2;
            out->bgColor3     = (uint8_t)b3;
            out->spriteMc0    = (uint8_t)mc0;
            out->spriteMc1    = (uint8_t)mc1;
            out->sprite0Color = (uint8_t)sc0;
            out->sprite1Color = (uint8_t)sc1;
        } else if (strncmp(line, "@pad ", 5) == 0) {
            char  letter = '\0';   // kept in the syntax for readability only
            int   px = 0;
            int   py = 0;
            int   pw = 0;
            int   sx = -1;         // optional passenger stand X (pixels)
            int   got = sscanf(line + 5, " %c %d %d %d %d",
                               &letter, &px, &py, &pw, &sx);
            if (got != 4 && got != 5) {
                fprintf(stderr, "mkstlevel: line %d: bad pad\n", lineNo);
                return 0;
            }
            if (out->padCount >= ST_MAX_PADS) {
                fprintf(stderr, "mkstlevel: line %d: too many pads (max %d)\n",
                        lineNo, ST_MAX_PADS);
                return 0;
            }
            out->pads[out->padCount].tileX  = (uint8_t)px;
            out->pads[out->padCount].tileY  = (uint8_t)py;
            out->pads[out->padCount].tileW  = (uint8_t)pw;
            // Default the passenger stand column to the pad's left edge.
            out->pads[out->padCount].standX =
                (uint8_t)((sx >= 0) ? sx : (px * 8));
            out->padCount++;
        } else if (strcmp(line, "@tilemap") == 0) {
            if (!parseGrid(fp, "tilemap", out->tilemap, &lineNo)) {
                return 0;
            }
            gotTilemap = 1;
        } else if (strcmp(line, "@colormap") == 0) {
            if (!parseGrid(fp, "colormap", out->colormap, &lineNo)) {
                return 0;
            }
            gotColormap = 1;
        } else {
            fprintf(stderr, "mkstlevel: line %d: unknown directive \"%s\"\n",
                    lineNo, line);
            return 0;
        }
    }

    if (out->name[0] == '\0') {
        fprintf(stderr, "mkstlevel: missing @name\n");
        return 0;
    }
    if (!gotTilemap) {
        fprintf(stderr, "mkstlevel: missing @tilemap\n");
        return 0;
    }
    if (!gotColormap) {
        fprintf(stderr, "mkstlevel: missing @colormap\n");
        return 0;
    }
    if (out->padCount == 0) {
        fprintf(stderr, "mkstlevel: at least one @pad required\n");
        return 0;
    }
    return 1;
}


static int writeOut(const LevelT *L, const char *path) {
    FILE     *fp;
    uint8_t   nameLen;
    uint8_t   i;
    size_t    cells;

    fp = fopen(path, "wb");
    if (fp == NULL) {
        fprintf(stderr, "mkstlevel: cannot open %s for write\n", path);
        return 0;
    }
    if (fputc('S', fp) == EOF || fputc('T', fp) == EOF ||
        fputc('L', fp) == EOF || fputc('3', fp) == EOF) {
        goto fail;
    }
    nameLen = (uint8_t)strlen(L->name);
    if (fputc(nameLen, fp) == EOF) {
        goto fail;
    }
    if (fwrite(L->name, 1, nameLen, fp) != nameLen) {
        goto fail;
    }
    if (fputc(L->tileBank,    fp) == EOF ||
        fputc(L->music,       fp) == EOF ||
        fputc(L->bgColor,     fp) == EOF ||
        fputc(L->borderColor, fp) == EOF ||
        fputc(L->taxiSpawnX,  fp) == EOF ||
        fputc(L->taxiSpawnY,  fp) == EOF) {
        goto fail;
    }
    if (fputc(L->xAccel,             fp) == EOF ||
        fputc(L->yAccel,             fp) == EOF ||
        fputc((uint8_t)L->xGrav,     fp) == EOF ||
        fputc((uint8_t)L->yGrav,     fp) == EOF) {
        goto fail;
    }
    if (fputc(L->bgColor1,     fp) == EOF ||
        fputc(L->bgColor2,     fp) == EOF ||
        fputc(L->bgColor3,     fp) == EOF ||
        fputc(L->spriteMc0,    fp) == EOF ||
        fputc(L->spriteMc1,    fp) == EOF ||
        fputc(L->sprite0Color, fp) == EOF ||
        fputc(L->sprite1Color, fp) == EOF) {
        goto fail;
    }
    if (fputc(L->padCount, fp) == EOF) {
        goto fail;
    }
    for (i = 0; i < L->padCount; i++) {
        if (fputc(L->pads[i].tileX,  fp) == EOF ||
            fputc(L->pads[i].tileY,  fp) == EOF ||
            fputc(L->pads[i].tileW,  fp) == EOF ||
            fputc(L->pads[i].standX, fp) == EOF) {
            goto fail;
        }
    }
    // STL3 stores no fares -- fares are RNG-chosen at runtime.
    cells = (size_t)ST_TILEMAP_W * ST_PLAYFIELD_ROWS;
    if (fwrite(L->tilemap, 1, cells, fp) != cells) {
        goto fail;
    }
    if (fwrite(L->colormap, 1, cells, fp) != cells) {
        goto fail;
    }
    fclose(fp);
    return 1;
fail:
    fclose(fp);
    fprintf(stderr, "mkstlevel: write error\n");
    return 0;
}


int main(int argc, char **argv) {
    FILE   *fp;
    LevelT  L;

    if (argc != 3) {
        fprintf(stderr, "usage: %s input.txt output.dat\n", argv[0]);
        return 2;
    }
    fp = fopen(argv[1], "r");
    if (fp == NULL) {
        fprintf(stderr, "mkstlevel: cannot open %s\n", argv[1]);
        return 1;
    }
    if (!parseLevel(fp, &L)) {
        fclose(fp);
        return 1;
    }
    fclose(fp);
    if (!writeOut(&L, argv[2])) {
        return 1;
    }
    printf("mkstlevel: wrote %s (name=\"%s\", pads=%u)\n",
           argv[2], L.name, (unsigned)L.padCount);
    return 0;
}
