/*
**  Buffered file exploder for innd.
*/

#include "portable/system.h"

#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <sys/stat.h>
#include <time.h>

#include "inn/innconf.h"
#include "inn/libinn.h"
#include "inn/messages.h"
#include "inn/paths.h"
#include "inn/qio.h"

/*
**  Hash functions for hashing sitenames.
*/
#define SITE_HASH(Name, p, j)  \
    for (p = Name, j = 0; *p;) \
    j = (j << 5) + j + *p++
#define SITE_SIZE      128
#define SITE_BUCKET(j) &SITEtable[j & (SITE_SIZE - 1)]


/*
**  For the mapping of site names to file names.
*/
typedef struct _PAIR {
    char First;
    char *Key;
    char *Value;
} PAIR;


/*
**  Entry for a single active site.
*/
typedef struct _SITE {
    bool Dropped;
    const char *Name;
    int CloseLines;
    int FlushLines;
    time_t LastFlushed;
    time_t LastClosed;
    int CloseSeconds;
    int FlushSeconds;
    FILE *F;
    const char *Filename;
    char *Buffer;
} SITE;


/*
**  Site hashtable bucket.
*/
typedef struct _SITEHASH {
    int Size;
    int Used;
    SITE *Sites;
} SITEHASH;


/* Global variables. */
static char *Format;
static const char *Map;
static PAIR *MAPdata;
static PAIR *MAPend;
static int BufferMode;
static int CloseEvery;
static int FlushEvery;
static int CloseSeconds;
static int FlushSeconds;
static sig_atomic_t GotInterrupt;
static SITEHASH SITEtable[SITE_SIZE];
static time_t Now;


/*
**  Free the map.
*/
static void
MAPfree(void)
{
    PAIR *mp;

    for (mp = MAPdata; mp < MAPend; mp++) {
        free(mp->Key);
        free(mp->Value);
    }
    free(MAPdata);
    MAPdata = NULL;
}


/*
**  Read the map file.
*/
static void
MAPread(const char *name)
{
    FILE *F;
    int i;
    PAIR *mp;
    char *p;
    char buff[BUFSIZ];

    if (MAPdata != NULL)
        MAPfree();

    /* Open file, count lines. */
    if ((F = fopen(name, "r")) == NULL) {
        fprintf(stderr, "Can't open %s, %s\n", name, strerror(errno));
        exit(1);
    }
    for (i = 0; fgets(buff, sizeof buff, F) != NULL; i++)
        continue;
    mp = MAPdata = xmalloc((i + 1) * sizeof(PAIR));

    /* Read each line; ignore blank and comment lines. */
    fseeko(F, 0, SEEK_SET);
    while (fgets(buff, sizeof buff, F) != NULL) {
        if ((p = strchr(buff, '\n')) != NULL)
            *p = '\0';
        if (buff[0] == '\0' || buff[0] == '#'
            || (p = strchr(buff, ':')) == NULL)
            continue;
        *p++ = '\0';
        mp->First = buff[0];
        mp->Key = xstrdup(buff);
        mp->Value = xstrdup(p);
        mp++;
    }
    fclose(F);
    MAPend = mp;
}


/*
**  Look up a name in the map, return original value if not found.
*/
static char *
MAPname(char *p)
{
    PAIR *mp;
    char c;

    for (c = *p, mp = MAPdata; mp < MAPend; mp++)
        if (c == mp->First && strcmp(p, mp->Key) == 0)
            return mp->Value;
    return p;
}


/*
**  Set up the site information.  Basically creating empty buckets.
*/
static void
SITEsetup(void)
{
    SITEHASH *shp;

    for (shp = SITEtable; shp < ARRAY_END(SITEtable); shp++) {
        shp->Size = 3;
        shp->Sites = xmalloc(shp->Size * sizeof(SITE));
        shp->Used = 0;
    }
}


/*
**  Close a site
*/
static void
SITEclose(SITE *sp)
{
    FILE *F;

    if ((F = sp->F) != NULL) {
        if (fflush(F) == EOF || ferror(F) || fchmod((int) fileno(F), 0664) < 0
            || fclose(F) == EOF)
            syswarn("%s cannot close %s", sp->Name, sp->Filename);
        sp->F = NULL;
    }
}

/*
**  Close all open sites.
*/
static void
SITEcloseall(void)
{
    SITEHASH *shp;
    SITE *sp;
    int i;

    for (shp = SITEtable; shp < ARRAY_END(SITEtable); shp++)
        for (sp = shp->Sites, i = shp->Used; --i >= 0; sp++)
            SITEclose(sp);
}


/*
**  Open the file for a site.
*/
static void
SITEopen(SITE *sp)
{
    if ((sp->F = xfopena(sp->Filename)) == NULL
        && (errno != EACCES || chmod(sp->Filename, 0644) < 0
            || (sp->F = xfopena(sp->Filename)) == NULL)) {
        syswarn("%s cannot fopen %s", sp->Name, sp->Filename);
        if ((sp->F = fopen("/dev/null", "w")) == NULL)
            /* This really should not happen. */
            sysdie("%s cannot fopen /dev/null", sp->Name);
    } else if (fchmod((int) fileno(sp->F), 0444) < 0)
        syswarn("%s cannot fchmod %s", sp->Name, sp->Filename);

    if (BufferMode != '\0')
        setbuf(sp->F, sp->Buffer);

    /* Reset all counters. */
    sp->FlushLines = 0;
    sp->CloseLines = 0;
    sp->LastFlushed = Now;
    sp->LastClosed = Now;
    sp->Dropped = false;
}


/*
**  Find a site, possibly create if not found.
*/
static SITE *
SITEfind(char *Name, bool CanCreate)
{
    char *p;
    int i;
    unsigned int j;
    SITE *sp;
    SITEHASH *shp;
    char c;
    char buff[BUFSIZ];

    /* Look for site in the hash table. */
    SITE_HASH(Name, p, j);
    shp = SITE_BUCKET(j);
    for (c = *Name, sp = shp->Sites, i = shp->Used; --i >= 0; sp++)
        if (c == sp->Name[0] && strcasecmp(Name, sp->Name) == 0)
            return sp;
    if (!CanCreate)
        return NULL;

    /* Adding a new site -- grow hash bucket if we need to. */
    if (shp->Used == shp->Size - 1) {
        shp->Size *= 2;
        shp->Sites = xrealloc(shp->Sites, shp->Size * sizeof(SITE));
    }
    sp = &shp->Sites[shp->Used++];

    /* Fill in the structure for the new site. */
    sp->Name = xstrdup(Name);
#if __GNUC__ > 4 || defined(__llvm__) || defined(__clang__)
#    pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
    snprintf(buff, sizeof(buff), Format, Map ? MAPname(Name) : sp->Name);
#if __GNUC__ > 4 || defined(__llvm__) || defined(__clang__)
#    pragma GCC diagnostic warning "-Wformat-nonliteral"
#endif
    sp->Filename = xstrdup(buff);
    if (BufferMode == 'u')
        sp->Buffer = NULL;
    else if (BufferMode == 'b')
        sp->Buffer = xmalloc(BUFSIZ);
    SITEopen(sp);

    return sp;
}


/*
**  Flush a site -- close and re-open the file.
*/
static void
SITEflush(SITE *sp)
{
    FILE *F;

    if ((F = sp->F) != NULL) {
        if (fflush(F) == EOF || ferror(F) || fchmod((int) fileno(F), 0664) < 0
            || fclose(F) == EOF)
            syswarn("%s cannot close %s", sp->Name, sp->Filename);
        sp->F = NULL;
    }
    if (!sp->Dropped)
        SITEopen(sp);
}


/*
**  Flush all open sites.
*/
static void
SITEflushall(void)
{
    SITEHASH *shp;
    SITE *sp;
    int i;

    for (shp = SITEtable; shp < ARRAY_END(SITEtable); shp++)
        for (sp = shp->Sites, i = shp->Used; --i >= 0; sp++)
            SITEflush(sp);
}


/*
**  Write data to a site.
*/
static void
SITEwrite(char *name, char *text, size_t len)
{
    SITE *sp;

    sp = SITEfind(name, true);
    if (sp->F == NULL)
        SITEopen(sp);

    if (fwrite(text, 1, len, sp->F) != len)
        syswarn("%s cannot write", sp->Name);

    /* Bump line count; see if time to close or flush. */
    if (CloseEvery && ++(sp->CloseLines) >= CloseEvery) {
        SITEflush(sp);
        return;
    }
    if (CloseSeconds && sp->LastClosed + CloseSeconds < Now) {
        SITEflush(sp);
        return;
    }
    if (FlushEvery && ++(sp->FlushLines) >= FlushEvery) {
        if (fflush(sp->F) == EOF || ferror(sp->F))
            syswarn("%s cannot flush %s", sp->Name, sp->Filename);
        sp->LastFlushed = Now;
        sp->FlushLines = 0;
    } else if (FlushSeconds && sp->LastFlushed + FlushSeconds < Now) {
        if (fflush(sp->F) == EOF || ferror(sp->F))
            syswarn("%s cannot flush %s", sp->Name, sp->Filename);
        sp->LastFlushed = Now;
        sp->FlushLines = 0;
    }
}


/*
**  Handle a command message.
*/
static void
Process(char *p)
{
    SITE *sp;

    if (*p == 'b' && strncmp(p, "begin", 5) == 0)
        /* No-op. */
        return;

    if (*p == 'f' && strncmp(p, "flush", 5) == 0) {
        for (p += 5; ISWHITE(*p); p++)
            continue;
        if (*p == '\0')
            SITEflushall();
        else if ((sp = SITEfind(p, false)) != NULL)
            SITEflush(sp);
        /*else
            fprintf(stderr, "buffchan flush %s unknown site\n", p);*/
        return;
    }

    if (*p == 'd' && strncmp(p, "drop", 4) == 0) {
        for (p += 4; ISWHITE(*p); p++)
            continue;
        if (*p == '\0')
            SITEcloseall();
        else if ((sp = SITEfind(p, false)) == NULL)
            warn("drop %s unknown site", p);
        else {
            SITEclose(sp);
            sp->Dropped = true;
        }
        return;
    }

    if (*p == 'r' && strncmp(p, "readmap", 7) == 0) {
        MAPread(Map);
        return;
    }

    /* Other command messages -- ignored. */
    warn("unknown message %s", p);
}


/*
**  Mark that we got a signal; let two signals kill us.
*/
static void
CATCHinterrupt(int s)
{
    GotInterrupt = true;
    xsignal(s, SIG_DFL);
}


int
main(int ac, char *av[])
{
    QIOSTATE *qp;
    int i;
    int Fields;
    char *p;
    char *next;
    char *line;
    char *Directory;
    bool Redirect;
    FILE *F;
    char *ERRLOG;

    /* First thing, set up our identity. */
    message_program_name = "buffchan";

    /* Set defaults. */
    if (!innconf_read(NULL))
        exit(1);
    ERRLOG = concatpath(innconf->pathlog, INN_PATH_ERRLOG);
    Directory = NULL;
    Fields = 1;
    Format = NULL;
    Redirect = true;
    GotInterrupt = false;
    umask(NEWSUMASK);

    xsignal(SIGHUP, CATCHinterrupt);
    xsignal(SIGINT, CATCHinterrupt);
    xsignal(SIGQUIT, CATCHinterrupt);
    xsignal(SIGPIPE, CATCHinterrupt);
    xsignal(SIGTERM, CATCHinterrupt);
    xsignal(SIGALRM, CATCHinterrupt);

    /* Parse JCL. */
    while ((i = getopt(ac, av, "bc:C:d:f:l:L:m:p:rs:u")) != EOF)
        switch (i) {
        default:
            die("usage error");
            /* NOTREACHED */
        case 'b':
        case 'u':
            BufferMode = i;
            break;
        case 'c':
            CloseEvery = atoi(optarg);
            break;
        case 'C':
            CloseSeconds = atoi(optarg);
            break;
        case 'd':
            Directory = optarg;
            if (Format == NULL)
                Format = xstrdup("%s");
            break;
        case 'f':
            Fields = atoi(optarg);
            break;
        case 'l':
            FlushEvery = atoi(optarg);
            break;
        case 'L':
            FlushSeconds = atoi(optarg);
            break;
        case 'm':
            Map = optarg;
            MAPread(Map);
            break;
        case 'p':
            if ((F = fopen(optarg, "w")) == NULL)
                sysdie("cannot fopen %s", optarg);
            fprintf(F, "%ld\n", (long) getpid());
            if (ferror(F) || fclose(F) == EOF)
                sysdie("cannot fclose %s", optarg);
            break;
        case 'r':
            Redirect = false;
            break;
        case 's':
            Format = optarg;
            break;
        }
    ac -= optind;
    if (ac)
        die("usage error");

    /* Do some basic set-ups. */
    if (Redirect) {
        if (freopen(ERRLOG, "a", stderr) == NULL)
            sysdie("cannot open %s for error output", ERRLOG);
    }
    if (Format == NULL) {
        Format = concatpath(innconf->pathoutgoing, "%s");
    }
    if (Directory && chdir(Directory) < 0)
        sysdie("cannot chdir to %s", Directory);
    SITEsetup();

    /* Read input. */
    for (qp = QIOfdopen((int) fileno(stdin)); !GotInterrupt;) {
        if ((line = QIOread(qp)) == NULL) {
            if (QIOerror(qp)) {
                syswarn("cannot read");
                break;
            }
            if (QIOtoolong(qp)) {
                warn("long line");
                continue;
            }

            /* Normal EOF. */
            break;
        }

        /* Command? */
        if (*line == EXP_CONTROL && *++line != EXP_CONTROL) {
            Process(line);
            continue;
        }

        /* Skip the right number of leading fields. */
        for (i = Fields, p = line; *p; p++)
            if (*p == ' ' && --i <= 0)
                break;
        if (*p == '\0')
            /* Nothing to write.  Probably shouldn't happen. */
            continue;

        /* Add a newline, get the length of all leading fields. */
        *p++ = '\n';
        i = p - line;

        /* Update the current time. */
        Now = time(NULL);

        /* Rest of the line is space-separated list of filenames. */
        for (; *p; p = next) {
            /* Skip whitespace, get next word. */
            while (*p == ' ')
                p++;
            for (next = p; *next && *next != ' '; next++)
                continue;
            if (*next)
                *next++ = '\0';

            SITEwrite(p, line, i);
        }
    }

    SITEcloseall();
    exit(0);
    /* NOTREACHED */
}
