/*
  Copyright ® Olivier Montanuy,
              André Majorel,
              contributors to the DeuTex project.

  DeuTex incorporates code derived from DEU 5.21 that was put in the
  public domain in 1994 by Raphaël Quinet and Brendon Wyber.

  SPDX-License-Identifier: GPL-2.0-or-later
*/

/*
** This code should contain all the tricky O/S related
** functions. If you're porting DeuTex, look here!
*/

#include "deutex.h"
#include <errno.h>
#include "tools.h"
#include "text.h"
#include <ctype.h>

/* A special instance of struct TXTFILE that is treated by the
   TXT*() output functions as the equivalent of /dev/null.
   Writing into it is a no-op. This convention is _not_
   supported by the input functions ! */
struct TXTFILE TXTdummy;

/*****************************************************/

const int16_t SPACE = 0x2;
const int16_t NEWLINE = 0x4;
const int16_t COMMENT = 0x8;
const int16_t SECTION = 0x10;   /*ok for SECTION header */
const int16_t NAME = 0x20;      /*valid as a name identifier */
const int16_t NUMBER = 0x40;    /*valid for a number */
const int16_t STPATCH = 0x80;   /*start of patch? */
const int16_t EXESTRNG = 0x100; /*valid in #string# */
const int16_t BOUNDARY = 0x200; /*# */
const int16_t STEQUAL = 0x400;  /*=*/
static int16_t TXTval[256];
static bool TXTok = false;

void TXTinit(void)
{
    int16_t n, val;
    for (n = 0; n < 256; n++) {
        val = 0;
        switch (n) {
        case '#':              /*comment,string boundary */
            val |= BOUNDARY + COMMENT;
            break;
        case ';':              /*comment */
            val |= COMMENT + EXESTRNG;
            break;
        case '\0':
        case '\n':             /*newline */
            val |= NEWLINE;
            break;
        case '_':              /*in name */
        case '\\':             /* deal with VILE strange name */
            val |= NAME + EXESTRNG;
            break;
        case '[':              /* deal with VILE strange name */
        case ']':              /* deal with VILE strange name */
            val |= SECTION + NAME + EXESTRNG;
            break;
        case '-':
        case '+':              /*in integer number */
            val |= NUMBER + EXESTRNG;
            break;
        case '*':              /*start of patch wall */
            val |= EXESTRNG + STPATCH;
            break;
        case '=':
            val |= STEQUAL + EXESTRNG;
            break;
        case '?':
        case '!':
        case '.':
        case ',':
        case '\'':
        case '&':
        case '(':
        case ')':
        case '$':
        case '%':
        case '@':
        case '/':
        case '<':
        case '>':
        case ' ':
        case '^':
        case '\"':
        case ':':
            val |= EXESTRNG;
            break;
        default:
            break;
        }
        if (isdigit(n))
            val |= NUMBER + EXESTRNG;
        if (isalpha(n))
            val |= SECTION + NAME + EXESTRNG;
        if (isspace(n))
            val |= SPACE;
        if (n == '%')    // Deal with Strife's "INVFONG%" and "INVFONY%"
            val |= NAME;
        TXTval[n] = val;
    }
    TXTok = true;
}

void TXTcloseR(struct TXTFILE *TXT)
{
    if (!TXTok)
        Bug("TR91", "TxtClo");
    fclose(TXT->fp);
    free(TXT);
}

struct TXTFILE *TXTopenR(const char *file, int silent)
{
    struct TXTFILE *TXT;
    size_t pathname_len;
    /*characters */
    if (!TXTok)
        TXTinit();

    pathname_len = strlen(file);
    TXT = (struct TXTFILE *) Malloc(sizeof(struct TXTFILE) + pathname_len);
    /*some inits */
    strcpy(TXT->pathname, file);
    TXT->Lines = 1;             /*start in line 1 */
    TXT->SectionStart = 0;
    TXT->SectionEnd = 0;
    TXT->fp = fopen(file, FOPEN_RT);
    if (TXT->fp == NULL) {
        if (silent) {
            free(TXT);
            return NULL;
        }
        ProgError("TR03", "%s: %s", fname(file), strerror(errno));
    }
    return TXT;
}

static bool TXTgetc(struct TXTFILE *TXT, int16_t * c, int16_t * val)
{
    int16_t cc = (int16_t) getc(TXT->fp);
    TXT->LastChar = cc;
    if (cc == EOF)
        return false;
    *c = cc = (cc & 0xFF);
    *val = TXTval[cc];
    if (TXTval[cc] & NEWLINE)
        TXT->Lines++;
    return true;
}

static void TXTungetc(struct TXTFILE *TXT)
{
    int16_t cc = TXT->LastChar;
    cc = (cc & 0xFF);
    ungetc(cc, TXT->fp);
    if (TXTval[cc] & NEWLINE)
        TXT->Lines--;
}

/*skip lines beginning with # or ; */
bool TXTskipComment(struct TXTFILE *TXT)
{
    int16_t c = 0, val = 0;
    bool comment;
    for (comment = false;;) {
        if (!TXTgetc(TXT, &c, &val))
            return false;
        if (val & NEWLINE) {    /*eat newlines */
            comment = false;
            continue;
        }
        if (val & COMMENT) {    /*eat commentaries */
            comment = true;
            continue;
        }
        if (val & SPACE) {      /*eat space */
            continue;
        }
        if (!comment) {
            TXTungetc(TXT);
            return true;
        }
    }
}

/* find '*' */
static bool TXTcheckStartPatch(struct TXTFILE *TXT)
{
    int16_t c = 0, val = 0;
    if (!TXTgetc(TXT, &c, &val))
        return false;
    if (val & STPATCH)
        return true;
    TXTungetc(TXT);
    return false;
}

/*read string, skip space before, stop space/\n*/
static bool TXTread(struct TXTFILE *TXT, char name[8], int16_t valid)
{
    int16_t c = 0, val = 0, n = 0;
    while (1) {
        if (!TXTgetc(TXT, &c, &val))
            return false;
        if (val & NEWLINE)
            continue;
        if (val & SPACE)
            continue;
        if (val & valid)
            break;
        ProgError("TR11", "%s(%ld): illegal char %s", TXT->pathname,
                  (long) TXT->Lines, quotechar(c));
    }
    name[0] = (char) c;
    for (n = 1; n < 256; n++) {
        if (!TXTgetc(TXT, &c, &val))
            break;
        if (val & SPACE) {
            TXTungetc(TXT);
            break;
        }
        if (!(val & valid))
            ProgError("TR13", "%s(%ld): illegal char %s in word",
                      TXT->pathname, (long) TXT->Lines, quotechar(c));
        if (n < 8)
            name[n] = (char) c;
    }
    if (n < 8)
        name[n] = '\0';
    return true;
}

int16_t TXTreadShort(struct TXTFILE * TXT)
{
    static char buffer[9];
    TXTread(TXT, buffer, NUMBER);
    buffer[8] = '\0';
    return (int16_t) atoi(buffer);
}

static bool TXTboundSection(struct TXTFILE *TXT);
static bool TXTreadIdent(struct TXTFILE *TXT, char name[8])
{
    if (!TXTok)
        Bug("TR21", "%s: TxtRid", fname(TXT->pathname));
    if (!TXTskipComment(TXT))
        return false;
    /*check end of section */
    if ((TXT->Lines) > (TXT->SectionEnd)) {
        if (!TXTboundSection(TXT))
            return false;       /*no other section */
    }
    if (!TXTread(TXT, name, NAME | NUMBER))
        ProgError("TR23", "%s(%ld): expected identifier or \"END:\"",
                  TXT->pathname, (long) TXT->Lines);
    Normalise(name, name);
    return true;
}

/*
** STPATCH is also used to indicate repetition
*/
static bool TXTreadOptionalRepeat(struct TXTFILE *TXT)
{
    int16_t c = 0, val = 0;
    while (1) {
        if (!TXTgetc(TXT, &c, &val))
            return false;
        if (!(val & NEWLINE)) {
            if (val & STPATCH)
                break;          /*look for STPATCH */
            if (val & SPACE)
                continue;       /*skip space */
        }
        TXTungetc(TXT);
        return false;
    }
    return true;                /*found */
}

/*
** STEQUAL is used to indicate alternate name
*/
static void TXTreadOptionalName(struct TXTFILE *TXT, char name[8])
{
    int16_t c = 0, val = 0;
    while (1) {
        if (!TXTgetc(TXT, &c, &val))
            return;
        if (!(val & NEWLINE)) {
            if (val & STEQUAL)
                continue;       /*skip '=' */
            if (val & SPACE)
                continue;       /*skip space */
            if (val & (NAME & (~NUMBER)))
                break;
        }
        TXTungetc(TXT);
        return;                 /*name is NOT modified */
    }
    TXTungetc(TXT);
    if (!TXTread(TXT, name, NAME | NUMBER)) {
        ProgError("TR32", "%s(%ld): invalid optional name", TXT->pathname,
                  (long) TXT->Lines);
    }
}

/*
** Read integer if exist before NEWLINE,
** but don't eat NEWLINE
*/
static int16_t TXTreadOptionalShort(struct TXTFILE *TXT)
{
    static char name[9];
    int16_t n, c = 0, val = 0;
    while (1) {
        if (!TXTgetc(TXT, &c, &val))
            return INVALIDINT;
        if (!(val & NEWLINE)) {
            if (val & SPACE)
                continue;       /*skip space */
            if (val & STEQUAL)
                continue;       /*skip '=' */
            if (val & NUMBER)
                break;          /*look for number */
        }
        TXTungetc(TXT);
        return INVALIDINT;      /*not a number. abort */
    }
    name[0] = (char) c;
    for (n = 1; n < 256; n++) {
        if (!TXTgetc(TXT, &c, &val))
            break;
        if (val & NEWLINE) {
            TXTungetc(TXT);
            break;
        }
        if (val & SPACE)
            break;
        if (!(val & NUMBER))
            ProgError("TR42", "%s(%ld): illegal char %s in number",
                      fname(TXT->pathname), (long) TXT->Lines,
                      quotechar(c));
        if (n < 8)
            name[n] = (char) c;
    }
    if (n < 8)
        name[n] = '\0';
    name[8] = '\0';
    return (int16_t) atoi(name);
}

/* read Blocks of the form
** [BLOCKID]
** identifier   ... anything ...
** identifier   ... anything ...
*/
static bool TXTfindSection(struct TXTFILE *TXT, bool Match)
{
    int16_t c = 0, val = 0, n;
    char buffer[8];
    while (1) {
        if (!TXTskipComment(TXT))
            return false;
        if (!TXTgetc(TXT, &c, &val))
            return false;
        if (c == '[') {
            for (n = 0; n < 256; n++) {
                if (!TXTgetc(TXT, &c, &val))
                    return false;
                if (c == ']') {
                    if (n < 8)
                        buffer[n] = '\0';
                    if (!Match)
                        return true;    /*any section is ok */
                    Normalise(buffer, buffer);  /*the right section? */
                    if (strncmp(buffer, TXT->Section, 8) == 0)
                        return true;
                    break;      /*not the right section */
                }
                if (!(val & (NAME | NUMBER)))
                    break;      /*not a section */
                if (n < 8)
                    buffer[n] = c;
            }
        }
        while (1) {             /*look for end of line */
            if (!TXTgetc(TXT, &c, &val))
                return false;
            if (val & NEWLINE)
                break;
        }
    }
}

/*
** find the section boundaries, from current position in file
*/
static bool TXTboundSection(struct TXTFILE *TXT)
{
    int16_t c = 0, val = 0;
    if (!TXTfindSection(TXT, true))
        return false;
    TXT->SectionStart = TXT->Lines + 1;
    /*check that we don't read twice the same section */
    if (TXT->SectionEnd > TXT->SectionStart)
        Bug("TR51", "TxtBdS");
    if (TXTfindSection(TXT, false))
        TXT->SectionEnd = TXT->Lines - 1;
    else
        TXT->SectionEnd = TXT->Lines;
    /* set pointer to first section line */
    fseek(TXT->fp, 0, SEEK_SET);
    TXT->Lines = 1;             /*start in line 1 */
    while (TXT->Lines < TXT->SectionStart) {
        if (!TXTgetc(TXT, &c, &val))
            return false;
    }
    return true;
}

bool TXTseekSection(struct TXTFILE * TXT, const char *section)
{
    if (!TXTok)
        Bug("TR61", "TxtSks");
    /*seek begin of file */
    TXT->SectionStart = 0;
    TXT->SectionEnd = 0;
    Normalise(TXT->Section, section);
    fseek(TXT->fp, 0L, SEEK_SET);
    TXT->Lines = 1;             /*start in line 1 */
    /*skipping comments, look for a line with
      <space>[section] */
    return TXTboundSection(TXT);
}

/*read a texture definition*/
/*return false if read End*/
bool TXTreadTexDef(struct TXTFILE * TXT, char name[8], int16_t * szx,
                   int16_t * szy)
{
    if (!TXTok)
        Bug("TR71", "TxtTxd");
    if (!TXTskipComment(TXT))
        return false;           /*End */
    if (!TXTread(TXT, name, NAME | NUMBER))
        ProgError("TR73", "%s(%ld): expecting identifier",
                  fname(TXT->pathname), (long) TXT->Lines);
    Normalise(name, name);
    *szx = TXTreadShort(TXT);
    *szy = TXTreadShort(TXT);
    return true;
}

/*read a patch def.  Return false if could not find '*' */
bool TXTreadPatchDef(struct TXTFILE * TXT, char name[8], int16_t * ofsx,
                     int16_t * ofsy)
{
    if (!TXTok)
        Bug("TR81", "TxtRpd");
    if (!TXTskipComment(TXT))
        return false;
    if (!TXTcheckStartPatch(TXT))
        return false;           /*not a patch line */
    if (!TXTread(TXT, name, NAME | NUMBER))
        ProgError("TR83", "%s(%ld): expecting identifier",
                  fname(TXT->pathname), (long) TXT->Lines);
    Normalise(name, name);
    *ofsx = TXTreadShort(TXT);
    *ofsy = TXTreadShort(TXT);
    return true;
}

bool TXTentryParse(char *name, char *filenam, int16_t * x, int16_t * y,
                   bool * repeat, struct TXTFILE * TXT, bool XY)
{
    int16_t c = 0, val = 0;
    bool comment;
    int16_t xx = INVALIDINT, yy = INVALIDINT;
    if (!TXTreadIdent(TXT, name))
        return false;
    /* skip the equal */
    if (!TXTgetc(TXT, &c, &val))
        return false;
    if (c != '=')
        TXTungetc(TXT);
    /* read integer */
    if (XY) {
        xx = TXTreadOptionalShort(TXT);
        yy = TXTreadOptionalShort(TXT);
    }
    Normalise(filenam, name);
    TXTreadOptionalName(TXT, filenam);
    if (XY) {
        if (xx == INVALIDINT)
            xx = TXTreadOptionalShort(TXT);
        if (yy == INVALIDINT)
            yy = TXTreadOptionalShort(TXT);
    }
    *repeat = TXTreadOptionalRepeat(TXT);
    *x = xx;
    *y = yy;
    for (comment = false;;) {
        if (!TXTgetc(TXT, &c, &val))
            break;
        if (val & NEWLINE)
            break;
        if (val & COMMENT) {    /*eat commentaries */
            comment = true;
            continue;
        }
        if (val & SPACE) {      /*eat space */
            continue;
        }
        if (!comment)
            ProgError("TR87", "%s(%ld): bad entry format",
                      fname(TXT->pathname), (long) TXT->Lines);
    }
    return true;
}

/*
** For any Writing of text files
*/
struct TXTFILE *TXTopenW(const char *file)
{                               /*open, and init if needed */
    struct TXTFILE *TXT;
    size_t pathname_len;

    /*characters */
    if (!TXTok)
        TXTinit();
    pathname_len = strlen(file);
    TXT = (struct TXTFILE *) Malloc(sizeof(struct TXTFILE) + pathname_len);
    /*some inits */
    strcpy(TXT->pathname, file);
    TXT->Lines = 1;             /*start in line 1 */
    TXT->SectionStart = 0;
    TXT->SectionEnd = 0;
    TXT->fp = fopen(file, FOPEN_RT);
    if (TXT->fp == NULL) {
        TXT->fp = fopen(file, FOPEN_WT);
    } else {
        fclose(TXT->fp);
        TXT->fp = fopen(file, FOPEN_AT);
        Warning("TW03", "%s: already exists, appending to it",
                fname(file));
    }
    if (TXT->fp == NULL)
        ProgError("TW05", "%s: %s", fname(file), strerror(errno));
    return TXT;
}

void TXTcloseW(struct TXTFILE *TXT)
{
    if (TXT == &TXTdummy)
        return;
    if (!TXTok)
        Bug("TW91", "TxtClo");
    fclose(TXT->fp);
    free(TXT);
}

/*
** To write entries
*/
void TXTaddSection(struct TXTFILE *TXT, const char *def)
{
    if (TXT == &TXTdummy)
        return;
    if (!TXTok)
        Bug("TW11", "TxtAdS");
    fprintf(TXT->fp, "[%.8s]\n", def);
}

void TXTaddEntry(struct TXTFILE *TXT, const char *name,
                 const char *filenam, int16_t x, int16_t y, bool repeat,
                 bool XY)
{
    if (TXT == &TXTdummy)
        return;
    if (!TXTok)
        Bug("TW21", "TxtAdE");
    fprintf(TXT->fp, "%.8s", name);
    if (filenam != NULL)
        fprintf(TXT->fp, "\t%.8s", filenam);
    if (XY)
        fprintf(TXT->fp, "\t%d\t%d", x, y);
    if (repeat)
        fprintf(TXT->fp, "\t*");
    fprintf(TXT->fp, "\n");
}

void TXTaddComment(struct TXTFILE *TXT, const char *text)
{
    if (TXT == &TXTdummy)
        return;
    if (!TXTok)
        Bug("TW31", "TxtAdC");
    fprintf(TXT->fp, "# %.256s\n", text);
}

void TXTaddEmptyLine(struct TXTFILE *TXT)
{
    if (TXT == &TXTdummy)
        return;
    if (!TXTok)
        Bug("TW41", "TxtAdL");
    putc('\n', TXT->fp);
}
