/***************************************************************/
/*                                                             */
/*  DOREM.C                                                    */
/*                                                             */
/*  Contains routines for parsing reminders and evaluating     */
/*  triggers.  Also contains routines for parsing OMIT         */
/*  commands.                                                  */
/*                                                             */
/*  This file is part of REMIND.                               */
/*  Copyright (C) 1992-2026 by Dianne Skoll                    */
/*  SPDX-License-Identifier: GPL-2.0-only                      */
/*                                                             */
/***************************************************************/

#include "config.h"
#include <stdio.h>
#include <ctype.h>
#include <string.h>

#include <stdlib.h>
#include <time.h>

#include "types.h"
#include "globals.h"
#include "err.h"
#include "protos.h"

static int ParseTimeTrig (ParsePtr s, TimeTrig *tim);
static int ParseLocalOmit (ParsePtr s, Trigger *t);
static int ParseScanFrom (ParsePtr s, Trigger *t, int type);
static int ParsePriority (ParsePtr s, Trigger *t);
static int ParseUntil (ParsePtr s, Trigger *t, int type);
static int ShouldTriggerBasedOnWarn (Trigger const *t, int dse, int *err);
static int ComputeTrigDuration(TimeTrig const *t);
static int ShouldQueueReminder(Trigger const *t, TimeTrig const *tim, int dse);

static int CalledEnterTimezone = 0;

int AdjustTriggerForTimeZone(Trigger *trig, int dse, TimeTrig *tim, int debug_ok)
{
    int y, m, d, hour, minute;
    int r;
    struct tm tm;
    if (!trig->tz || dse < 0) {
        /* Already local time or did not compute trigger date - no adjustments needed */
        return dse;
    }
    FromDSE(dse, &y, &m, &d);
    hour = tim->ttime_orig / 60;
    minute = tim->ttime_orig % 60;

    r = tz_convert(y, m, d, hour, minute, trig->tz, LocalTimeZone, &tm);
    if (r != 1) {
        Wprint(tr("Error adjusting trigger to local time zone"));
        return dse;
    }

    dse = DSE(tm.tm_year+1900, tm.tm_mon, tm.tm_mday);
    tim->ttime = tm.tm_hour * 60 + tm.tm_min;
    /* Adjust eventstart also */
    trig->eventstart = dse * MINUTES_PER_DAY + tim->ttime;
    SaveAllTriggerInfo(trig, tim, dse, tim->ttime, 1);
    if (debug_ok && (DebugFlag & DB_PRTTRIG)) {
        fprintf(ErrFp, "%s(%s): Trig(tz_adj %s) = %s, %d %s, %d AT %02d:%02d",
                GetCurrentFilename(), line_range(LineNoStart, LineNo), trig->tz,
                get_day_name(dse % 7), tm.tm_mday, get_month_name(tm.tm_mon),
                1900 + tm.tm_year, tim->ttime / 60, tim->ttime % 60);
        if (tim->duration != NO_TIME) {
            fprintf(ErrFp, " DURATION %02d:%02d",
                    (tim->duration / 60),
                    (tim->duration % 60));
        }
        fprintf(ErrFp, "\n");
    }
    return dse;
}

void ExitTimezone(char const *tz)
{
    if (!CalledEnterTimezone) {
        fprintf(stderr, "ExitTimezone called without EnterTimezone!!!\n");
        abort();
    }
    CalledEnterTimezone = 0;
    if (!tz || !*tz) {
        /* Nothing to do */
        return;
    }
    /* Revert to our local time zone */
    (void) tz_set_tz(LocalTimeZone);

    DSEToday = LocalDSEToday;
    SysTime = LocalSysTime;
    FromDSE(DSEToday, &CurYear, &CurMon, &CurDay);

    if (DebugFlag & DB_SWITCH_ZONE) {
        fprintf(stderr, "TZ exit %s: %04d-%02d-%02d %02d:%02d\n", tz,
                CurYear, CurMon+1, CurDay, SysTime / 3600, (SysTime/60) % 60);
    }
    return;
}


void EnterTimezone(char const *tz)
{
    struct tm tm;
    int y, m, d;
    time_t t;

    if (CalledEnterTimezone) {
        fprintf(stderr, "EnterTimezone called twice in a row!!!\n");
        abort();
    }

    CalledEnterTimezone = 1;

    if (!tz || !*tz) {
        /* Stay in local timezone */
        return;
    }

    FromDSE(LocalDSEToday, &y, &m, &d);
    tm.tm_sec   = 0;
    tm.tm_min   = (LocalSysTime/60) % 60;
    tm.tm_hour  = LocalSysTime / 3600;
    tm.tm_mday  = d;
    tm.tm_mon   = m;
    tm.tm_year  = y - 1900;
    tm.tm_wday  = 0;  /* Ignored by mktime */
    tm.tm_yday  = 0;  /* Ignored by mktime */
    tm.tm_isdst = -1; /* Information not available */

    t = mktime(&tm);    /* Convert local time to seconds */

    /* Set target timezone */
    (void) tz_set_tz(tz);

    /* Update our variables */
    (void) localtime_r(&t, &tm);

    SysTime  = tm.tm_min * 60 + (tm.tm_hour * 3600);
    CurDay   = tm.tm_mday;
    CurMon   = tm.tm_mon;
    CurYear  = tm.tm_year + 1900;

    DSEToday = DSE(CurYear, CurMon, CurDay);

    /* Adjust DSEToday back by a day if midnight in our time zone requires it */
    if (SysTime < LocalSysTime) {
        DSEToday--;
        FromDSE(DSEToday, &CurYear, &CurMon, &CurDay);
    }

    if (DebugFlag & DB_SWITCH_ZONE) {
        fprintf(stderr, "TZ enter %s: %04d-%02d-%02d %02d:%02d\n", tz,
                CurYear, CurMon+1, CurDay, SysTime / 3600, (SysTime/60) % 60);
    }
}

void remove_trailing_newlines(DynamicBuffer *buf)
{
    char *s = (char *) DBufValue(buf) + DBufLen(buf) - 1;
    while (s >= DBufValue(buf)) {
        if (*s == '\n') {
            *s = 0;
            s--;
        } else {
            break;
        }
    }
}
static int todo_filtered(Trigger const *t)
{
    if (t->is_todo && TodoFilter == ONLY_EVENTS) return 1;
    if (!t->is_todo && TodoFilter == ONLY_TODOS) return 1;
    return 0;
}

static int
get_raw_scanfrom(Trigger const *t)
{
    if (t->from != NO_DATE) {
        if (t->from > DSEToday) return t->from;
        return DSEToday;
    }
    if (t->scanfrom == NO_SCANFROM) return NO_SCANFROM;
    if (t->scanfrom > 0) return t->scanfrom;

    /* Relative SCANFROM is negative, so subtract from today() */
    return DSEToday + t->scanfrom;
}

int
get_scanfrom(Trigger const *t)
{
    int calmode = (DoSimpleCalendar || DoCalendar) ? 1 : 0;

    /* TODOs are treated just like events in calendar mode */
    if (!calmode && t->is_todo && t->from != NO_DATE) {
        if (t->complete_through != NO_DATE) {
            if (t->complete_through+1 > t->from) {
                return t->complete_through+1;
            } else {
                return t->from;
            }
        } else {
            return t->from;
        }
    }
    if (get_raw_scanfrom(t) != NO_SCANFROM) {
        if (!calmode && t->complete_through != NO_DATE) {
            if (t->complete_through+1 > get_raw_scanfrom(t)) {
                return t->complete_through+1;
            } else {
                return get_raw_scanfrom(t);
            }
        } else {
            return get_raw_scanfrom(t);
        }
    }
    if (!calmode) {
        if (t->complete_through != NO_DATE) {
            return t->complete_through+1;
        }
        if (t->is_todo) {
            /* TODO with no COMPLETE-THROUGH.
               Scan from the beginning of time */
            return 0;
        }
    }
    return DSEToday;
}

static int
ensure_expr_references_first_local_arg(expr_node *node)
{
    expr_node *other;

    if (!node) {
        return 0;
    }
    if (node->type == N_LOCAL_VAR && node->u.arg == 0) {
        return 1;
    }
    if (ensure_expr_references_first_local_arg(node->child)) {
        return 1;
    }
    other = node->sibling;
    while (other) {
        if (ensure_expr_references_first_local_arg(other)) {
            return 1;
        }
        other = other->sibling;
    }
    return 0;
}

static void
check_trigger_function(char const *fname, char const *type)
{
    UserFunc *f;
    if (!*fname) {
        return;
    }
    f = FindUserFunc(fname);
    if (!f) {
        if (strcmp(type, "WARN")) {
            /* Undefined WARN functions are diagnosed elsewhere... */
            Wprint(tr("Undefined %s function: `%s'"), type, fname);
        }
        return;
    }
    if (f->nargs != 1) {
        Wprint(tr("%s function `%s' defined at %s(%s) should take 1 argument but actually takes %d"), type, fname, f->filename, line_range(f->lineno_start, f->lineno), f->nargs);
        return;
    }
    if (ensure_expr_references_first_local_arg(f->node)) {
        return;
    }
    Wprint(tr("%s function `%s' defined at %s(%s) does not use its argument"), type, fname, f->filename, line_range(f->lineno_start, f->lineno));
}

static void
ensure_satnode_mentions_trigdate_aux(expr_node *node, int *mentioned)
{
    char const *name;
    expr_node *other;
    UserFunc *f;

    if (!node) {
        return;
    }
    if (*mentioned) {
        return;
    }

    if (node->type == N_BUILTIN_FUNC) {
        name = node->u.builtin_func->name;
        if (!strcmp(name, "trigdate") ||
            !strcmp(name, "trigdatetime")) {
            *mentioned = 1;
            return;
        }
    } else if (node->type == N_SYSVAR) {
        name = node->u.sysvar->name;
        if (!strcasecmp(name, "T") ||
            !strcasecmp(name, "Td") ||
            !strcasecmp(name, "Tm") ||
            !strcasecmp(name, "Tw") ||
            !strcasecmp(name, "Ty")) {
            *mentioned = 1;
            return;
        }
    } else if (node->type == N_SHORT_USER_FUNC || node->type == N_USER_FUNC) {
        if (node->type == N_SHORT_USER_FUNC) {
            name = node->u.name;
        } else {
            name = node->u.value.v.str;
        }
        f = FindUserFunc(name);
        if (f && !f->recurse_flag) {
            f->recurse_flag = 1;
            ensure_satnode_mentions_trigdate_aux(f->node, mentioned);
            f->recurse_flag = 0;
            if (*mentioned) {
                return;
            }
        }
    }
    ensure_satnode_mentions_trigdate_aux(node->child, mentioned);
    if (*mentioned) {
        return;
    }
    other = node->sibling;
    while (other) {
        ensure_satnode_mentions_trigdate_aux(other, mentioned);
        if (*mentioned) {
            return;
        }
        other = other->sibling;
    }
}

static void ensure_satnode_mentions_trigdate(expr_node *node)
{
    int mentioned = 0;
    char const *str;

    /* Don't bother unless warning_level is 05.00.03 */
    if (!warning_level("05.00.03")) {
        return;
    }
    if (node->type == N_CONSTANT || node->type == N_SHORT_STR) {
        if (node->type == N_CONSTANT) {
            if (node->u.value.type == INT_TYPE) {
                if (node->u.value.v.val == 0) {
                    Wprint(tr("SATISFY: constant 0 will never be true"));
                }
                return;
            }
            if (node->u.value.type != STR_TYPE) {
                return;
            }
            str = node->u.value.v.str;
        } else {
            str = node->u.name;
        }
        if (!*str) {
            Wprint(tr("SATISFY: constant \"\" will never be true"));
        }
        return;
    }

    ensure_satnode_mentions_trigdate_aux(node, &mentioned);
    if (!mentioned) {
        Wprint(tr("SATISFY: expression has no reference to trigdate() or $T..."));
    }
}


static int
ComputeTrigDuration(TimeTrig const *t)
{
    if (t->ttime == NO_TIME ||
        t->duration == NO_TIME) {
        return 0;
    }
    return (t->ttime + t->duration - 1) / MINUTES_PER_DAY;
}

/***************************************************************/
/*                                                             */
/*  DoRem                                                      */
/*                                                             */
/*  Do the REM command.                                        */
/*                                                             */
/***************************************************************/
int DoRem(ParsePtr p)
{

    Trigger trig;
    TimeTrig tim;
    int r, err;
    int dse;
    DynamicBuffer buf;
    Token tok;

    DBufInit(&buf);

    /* Parse the trigger date and time */
    if ( (r=ParseRem(p, &trig, &tim)) != OK ) {
        FreeTrig(&trig);
        return r;
    }

    if (trig.tz != NULL && tim.ttime == NO_TIME) {
        PurgeEchoLine("%s\n", CurLine);
        FreeTrig(&trig);
        return E_TZ_NO_AT;
    }

    /* An empty string for time zone is just a missing time zone */
    if (trig.tz != NULL && !*trig.tz) {
        free( (void *) trig.tz);
        trig.tz = NULL;
    }

    if (trig.complete_through != NO_DATE && !trig.is_todo) {
        PurgeEchoLine("%s\n", CurLine);
        FreeTrig(&trig);
        return E_COMPLETE_WITHOUT_TODO;
    }

    if (trig.max_overdue >= 0 && !trig.is_todo) {
        PurgeEchoLine("%s\n", CurLine);
        FreeTrig(&trig);
        return E_MAX_OVERDUE_WITHOUT_TODO;
    }

    if (trig.typ == NO_TYPE) {
        if (!Hush) {
            PurgeEchoLine("%s\n", "#!P! Cannot parse next line");
        }
        PurgeEchoLine("%s\n", CurLine);
        FreeTrig(&trig);
        return E_EOLN;
    }
    if (trig.typ == SAT_TYPE) {
        if (!Hush) {
            PurgeEchoLine("%s\n", "#!P: Cannot purge SATISFY-type reminders");
        }
        PurgeEchoLine("%s\n", CurLine);
        EnterTimezone(trig.tz);
        r=DoSatRemind(&trig, &tim, p);
        ExitTimezone(trig.tz);
        if (r) {
            if (r == E_CANT_TRIG && trig.maybe_uncomputable) {
                r = OK;
            }
            FreeTrig(&trig);
            if (r == E_EXPIRED) return OK;
            return r;
        }
        if (!LastTrigValid) {
            FreeTrig(&trig);
            return OK;
        }
        r=ParseToken(p, &buf);
        if (r) {
            FreeTrig(&trig);
            return r;
        }
        FindToken(DBufValue(&buf), &tok);
        DBufFree(&buf);
        if (tok.type == T_Empty || tok.type == T_Comment) {
            r = OK;
            if (trig.addomit) {
                r = AddGlobalOmit(LastTriggerDate);
            }
            DBufFree(&buf);
            FreeTrig(&trig);
            return r;
        }
        if (tok.type != T_RemType || tok.val == SAT_TYPE) {
            DBufFree(&buf);
            FreeTrig(&trig);
            return E_PARSE_ERR;
        }
        if (tok.val == PASSTHRU_TYPE) {
            r=ParseToken(p, &buf);
            if (r) {
                FreeTrig(&trig);
                return r;
            }
            if (!DBufLen(&buf)) {
                FreeTrig(&trig);
                DBufFree(&buf);
                return E_EOLN;
            }
            StrnCpy(trig.passthru, DBufValue(&buf), PASSTHRU_LEN);
            DBufFree(&buf);
        }
        trig.typ = tok.val;

        /* Convert some SPECIALs back to plain types */
        FixSpecialType(&trig);

        dse = LastTriggerDate;
        if (!LastTrigValid || PurgeMode) {
            FreeTrig(&trig);
            return OK;
        }
    } else {
        /* Calculate the trigger date */
        EnterTimezone(trig.tz);
        dse = ComputeTrigger(get_scanfrom(&trig), &trig, &tim, &r, 1);
        ExitTimezone(trig.tz);
        if (r) {
            if (PurgeMode) {
                if (!Hush) {
                    PurgeEchoLine("%s: %s\n", "#!P! Problem calculating trigger date", GetErr(r));
                }
                PurgeEchoLine("%s\n", CurLine);
            }
            if (r == E_CANT_TRIG && trig.maybe_uncomputable) {
                r = OK;
            }
            FreeTrig(&trig);
            return r;
        }
    }

    /* Adjust trigger date/time to time zone */
    dse = AdjustTriggerForTimeZone(&trig, dse, &tim, 1);

    /* Add to global OMITs if so indicated */
    if (trig.addomit) {
        r = AddGlobalOmit(dse);
        if (r) {
            FreeTrig(&trig);
            return r;
        }
    }
    if (PurgeMode) {
        if (trig.expired || (!trig.is_todo && dse < DSEToday)) {
            if (trig.expr_happened) {
                if (trig.nonconst_expr) {
                    if (!Hush) {
                        PurgeEchoLine("%s\n", "#!P: Next line may have expired, but contains non-constant expression");
                        PurgeEchoLine("%s\n", "#!P: or a relative SCANFROM clause");
                    }
                    PurgeEchoLine("%s\n", CurLine);
                } else {
                    if (!Hush) {
                        PurgeEchoLine("%s\n", "#!P: Next line has expired, but contains expression...  please verify");
                    }
                    PurgeEchoLine("#!P: Expired: %s\n", CurLine);
                }
            } else {
                PurgeEchoLine("#!P: Expired: %s\n", CurLine);
            }
        } else {
            PurgeEchoLine("%s\n", CurLine);
        }
        FreeTrig(&trig);
        return OK;
    }

    /* Queue the reminder, if necessary */
    if (ShouldQueueReminder(&trig, &tim, dse)) {
        QueueReminder(p, &trig, &tim, trig.sched, dse);
    }
    /* If we're in daemon mode, do nothing over here */
    if (Daemon) {
        FreeTrig(&trig);
        return OK;
    }

    r = OK;

    if (ShouldTriggerReminder(&trig, &tim, dse, &err)) {
        /* Filter unwanted events/todos */
        if (todo_filtered(&trig)) {
            FreeTrig(&trig);
            return OK;
        }

        if (JSONMode) {
            DynamicBuffer body;
            int y, m, d;
            int if_depth = get_if_pointer() - get_base_if_pointer();
            DBufInit(&body);
            int red=-1, green=-1, blue=-1;
            r=TriggerReminder(p, &trig, &tim, dse, 0, &body, &red, &green, &blue);
            if (r) {
                FreeTrig(&trig);
                return r;
            }
            /* Remove trailing newlines from body */
            remove_trailing_newlines(&body);

            if (!*DBufValue(&body)) {
                FreeTrig(&trig);
                return r;
            }
            if (JSONLinesEmitted) {
                printf("},\n");
            }
            JSONLinesEmitted++;
            FromDSE(dse, &y, &m, &d);
            printf("{\"date\":\"%04d-%02d-%02d\",", y, m+1, d);
            PrintJSONKeyPairString("filename", GetCurrentFilename());
            PrintJSONKeyPairInt("lineno", LineNo);
            if (LineNoStart != LineNo) {
                PrintJSONKeyPairInt("lineno_start", LineNoStart);
            }
            if (!NoFormat || strcasecmp(trig.passthru, "FORMATTED")) {
                PrintJSONKeyPairString("passthru", trig.passthru);
            }
            if (trig.duration_days) {
                PrintJSONKeyPairInt("duration", trig.duration_days);
            }
            if (tim.ttime != NO_TIME) {
                PrintJSONKeyPairInt("time", tim.ttime);
                if (tim.ttime_orig != tim.ttime) {
                    PrintJSONKeyPairInt("time_in_tz", tim.ttime_orig);
                }
            }
            if (trig.nonconst_expr) {
                PrintJSONKeyPairInt("nonconst_expr", 1);
            }
            if (if_depth) {
                PrintJSONKeyPairInt("if_depth", if_depth);
            }
            if (tim.delta) {
                PrintJSONKeyPairInt("tdelta", tim.delta);
            }
            if (tim.rep) {
                PrintJSONKeyPairInt("trep", tim.rep);
            }
            if (red >= 0 && red <= 255 && green >= 0 && green <= 255 && blue >=0 && blue <= 255) {
                PrintJSONKeyPairInt("r", red);
                PrintJSONKeyPairInt("g", green);
                PrintJSONKeyPairInt("b", blue);
            }

            WriteJSONTrigger(&trig, 1);
            printf("\"body\":\"");
            PrintJSONString(DBufValue(&body));
            printf("\"");
        } else {
            r=TriggerReminder(p, &trig, &tim, dse, 0, NULL, NULL, NULL, NULL);
            FreeTrig(&trig);
            return r;
        }
    } else {
        /* Parse the rest of the line to catch any potential
           expression-pasting errors */
        if (ParseUntriggered) {
            while (ParseChar(p, &r, 0)) {
                if (r != 0) {
                    break;
                }
            }
        }
    }

    FreeTrig(&trig);
    return r;
}

/***************************************************************/
/*                                                             */
/*  GetFullDate - get a full date, either YYYY-MM-DD or        */
/*                YEAR MON DAY                                 */
/*                                                             */
/*  Returns OK on success or an error code on failure.  Sets   */
/*  *dse on success.                                           */
/*                                                             */
/***************************************************************/
static int GetFullDate(ParsePtr s, char const *prefix, int *dse)
{
    Token tok;
    DynamicBuffer buf;
    int y = NO_YR, m = NO_MON, d = NO_DAY;
    int r;
    *dse = NO_DATE;

    DBufInit(&buf);

    while(1) {
        r = ParseToken(s, &buf);
        if (r) return r;
        FindToken(DBufValue(&buf), &tok);
        switch(tok.type) {
        case T_Year:
            DBufFree(&buf);
            if (y != NO_YR) {
                Eprint("%s: %s", prefix, GetErr(E_YR_TWICE));
                return E_YR_TWICE;
            }
            y = tok.val;
            break;

        case T_Month:
            DBufFree(&buf);
            if (m != NO_MON) {
                Eprint("%s: %s", prefix, GetErr(E_MON_TWICE));
                return E_MON_TWICE;
            }
            m = tok.val;
            break;

        case T_Day:
            DBufFree(&buf);
            if (d != NO_DAY) {
                Eprint("%s: %s", prefix, GetErr(E_DAY_TWICE));
                return E_DAY_TWICE;
            }
            d = tok.val;
            break;

        case T_Date:
            DBufFree(&buf);
            if (y != NO_YR) {
                Eprint("%s: %s", prefix, GetErr(E_YR_TWICE));
                return E_YR_TWICE;
            }
            if (m != NO_MON) {
                Eprint("%s: %s", prefix, GetErr(E_MON_TWICE));
                return E_MON_TWICE;
            }
            if (d != NO_DAY) {
                Eprint("%s: %s", prefix, GetErr(E_DAY_TWICE));
                return E_DAY_TWICE;
            }
            if (*dse != NO_DATE) {
                return E_DAY_TWICE;
            }
            *dse = tok.val;
            /* We're done here! */
            return OK;

        default:
            if (tok.type == T_Illegal && tok.val < 0) {
                Eprint("%s: `%s'", GetErr(-tok.val), DBufValue(&buf));
                DBufFree(&buf);
                return -tok.val;
            }
            if (*dse == NO_DATE && (y == NO_YR || m == NO_MON || d == NO_DAY)) {
                Eprint("%s: %s", prefix, GetErr(E_INCOMPLETE));
                DBufFree(&buf);
                return E_INCOMPLETE;
            }
            if (*dse != NO_DATE) {
                PushToken(DBufValue(&buf), s);
                DBufFree(&buf);
                return OK;
            }
            if (!DateOK(y, m, d)) {
                DBufFree(&buf);
                return E_BAD_DATE;
            }
            *dse = DSE(y, m, d);
            PushToken(DBufValue(&buf), s);
            DBufFree(&buf);
            return OK;
        }
    }
}

/***************************************************************/
/*                                                             */
/*  ParseRem                                                   */
/*                                                             */
/*  Given a parse pointer, parse line and fill in a            */
/*  trigger structure.                                         */
/*                                                             */
/***************************************************************/
int ParseRem(ParsePtr s, Trigger *trig, TimeTrig *tim)
{
    register int r;
    DynamicBuffer buf;
    Token tok;
    int y, m, d;
    int dse;
    int seen_delta = 0;

    DBufInit(&buf);

    trig->y = NO_YR;
    trig->m = NO_MON;
    trig->d = NO_DAY;
    trig->wd = NO_WD;
    trig->back = NO_BACK;
    trig->delta = -DefaultDelta;
    trig->until = NO_UNTIL;
    trig->rep  = NO_REP;
    trig->localomit = NO_WD;
    trig->skip = NO_SKIP;
    trig->once = NO_ONCE;
    trig->addomit = 0;
    trig->noqueue = 0;
    trig->typ = NO_TYPE;
    trig->scanfrom = NO_SCANFROM;
    trig->from = NO_DATE;
    trig->priority = DefaultPrio;
    trig->sched[0] = 0;
    trig->warn[0] = 0;
    trig->omitfunc[0] = 0;
    trig->duration_days = 0;
    trig->eventstart = NO_DATETIME;
    trig->eventstart_orig = NO_DATETIME;
    trig->eventduration = NO_TIME;
    trig->maybe_uncomputable = 0;
    DBufInit(&(trig->tags));
    trig->passthru[0] = 0;
    tim->ttime = NO_TIME;
    tim->ttime_orig = NO_TIME;
    tim->delta = DefaultTDelta;
    tim->rep   = NO_REP;
    tim->duration = NO_TIME;
    trig->need_wkday = 0;
    trig->is_todo = 0;
    trig->max_overdue = -1;
    trig->complete_through = NO_DATE;
    trig->adj_for_last = 0;
    trig->infos = NULL;
    trig->tz = NULL;
    trig->nonconst_expr = 0;
    trig->expr_happened = 0;

    int parsing = 1;
    while(parsing) {
        /* Read space-delimited string */
        r = ParseToken(s, &buf);
        if (r) return r;

        /* Figure out what we've got */
        FindToken(DBufValue(&buf), &tok);
        switch(tok.type) {
        case T_Todo:
            if (trig->is_todo) return E_TODO_TWICE;
            trig->is_todo = 1;
            break;

        case T_In:
            /* Completely ignored */
            DBufFree(&buf);
            break;

        case T_Ordinal:
            DBufFree(&buf);
            if (trig->d != NO_DAY)     return E_DAY_TWICE;
            if (tok.val < 0) {
                if (trig->back != NO_BACK) return E_BACK_TWICE;
                trig->back = -7;
                trig->d = 1;
                trig->adj_for_last = 1;
            } else {
                trig->d = 1 + 7 * tok.val;
            }
            trig->need_wkday = 1;
            break;

        case T_Date:
            DBufFree(&buf);
            if (trig->d != NO_DAY) {
                return E_DAY_TWICE;
            }
            if (trig->m != NO_MON) {
                return E_MON_TWICE;
            }
            if (trig->y != NO_YR)  {
                return E_YR_TWICE;
            }

            FromDSE(tok.val, &y, &m, &d);
            trig->y = y;
            trig->m = m;
            trig->d = d;
            break;

        case T_DateTime:
            DBufFree(&buf);
            if (trig->d != NO_DAY) return E_DAY_TWICE;
            if (trig->m != NO_MON) return E_MON_TWICE;
            if (trig->y != NO_YR)  return E_YR_TWICE;
            FromDSE(tok.val / MINUTES_PER_DAY, &y, &m, &d);
            trig->y = y;
            trig->m = m;
            trig->d = d;
            tim->ttime = (tok.val % MINUTES_PER_DAY);
            tim->ttime_orig = tim->ttime;
            break;

        case T_WkDay:
            DBufFree(&buf);
            if (trig->wd & (1 << tok.val)) return E_WD_TWICE;
            trig->wd |= (1 << tok.val);
            break;

        case T_Month:
            DBufFree(&buf);
            if (trig->m != NO_MON) return E_MON_TWICE;
            trig->m = tok.val;
            break;

        case T_MaybeUncomputable:
            DBufFree(&buf);
            trig->maybe_uncomputable = 1;
            break;

        case T_Skip:
            DBufFree(&buf);
            if (trig->skip != NO_SKIP) return E_SKIP_ERR;
            trig->skip = tok.val;
            break;

        case T_MaxOverdue:
            if (trig->max_overdue >= 0) return E_MAX_OVERDUE_TWICE;
            DBufFree(&buf);
            r = ParseToken(s, &buf);
            if (r) return r;
            FindToken(DBufValue(&buf), &tok);
            DBufFree(&buf);
            if (tok.type == T_Illegal) {
                return -tok.val;
            }
            if (tok.type != T_Day && tok.type != T_Year && tok.type != T_Number) {
                return E_EXPECTING_NUMBER;
            }
            if (tok.val < 0) {
                return E_2LOW;
            }
            trig->max_overdue = tok.val;
            break;

        case T_Priority:
            DBufFree(&buf);
            r=ParsePriority(s, trig);
            if (r) return r;
            break;

        /* A time implicitly introduces an AT if AT is not explicit */
        case T_Time:
            DBufFree(&buf);
            if (tim->ttime != NO_TIME) return E_TIME_TWICE;
            tim->ttime = tok.val;
            tim->ttime_orig = tok.val;
            r = ParseTimeTrig(s, tim);
            if (r) return r;
            trig->duration_days = ComputeTrigDuration(tim);
            break;

        case T_At:
            DBufFree(&buf);
            r=ParseTimeTrig(s, tim);
            if (r) return r;
            trig->duration_days = ComputeTrigDuration(tim);
            break;

        case T_Scanfrom:
            DBufFree(&buf);
            r=ParseScanFrom(s, trig, tok.val);
            if (r) return r;
            break;

        case T_RemType:
            DBufFree(&buf);
            trig->typ = tok.val;
            if (s->isnested) return E_CANT_NEST_RTYPE;
            if (trig->typ == PASSTHRU_TYPE) {
                r = ParseToken(s, &buf);
                if (r) return r;
                if (!DBufLen(&buf)) {
                    DBufFree(&buf);
                    return E_EOLN;
                }
                StrnCpy(trig->passthru, DBufValue(&buf), PASSTHRU_LEN);
            }
            FixSpecialType(trig);
            parsing = 0;
            break;

        case T_Through:
            DBufFree(&buf);
            if (trig->rep != NO_REP) return E_REP_TWICE;
            trig->rep = 1;
            r = ParseUntil(s, trig, tok.type);
            if (r) return r;
            break;

        case T_CompleteThrough:
            if (trig->complete_through != NO_DATE) return E_COMPLETE_THROUGH_TWICE;
            r = GetFullDate(s, "COMPLETE-THROUGH", &dse);
            if (r != OK) return r;
            trig->complete_through = dse;
            break;

        case T_Until:
            DBufFree(&buf);
            r=ParseUntil(s, trig, tok.type);
            if (r) return r;
            break;

        case T_Number:
            DBufFree(&buf);
            Eprint("`%d' is not recognized as a year (%d-%d) or a day number (1-31)",
                   tok.val, BASE, BASE+YR_RANGE);
            return E_PARSE_ERR;

        case T_Year:
            DBufFree(&buf);
            if (trig->y != NO_YR) return E_YR_TWICE;
            trig->y = tok.val;
            break;

        case T_Day:
            DBufFree(&buf);
            if (trig->d != NO_DAY) return E_DAY_TWICE;
            trig->d = tok.val;
            break;

        case T_Rep:
            DBufFree(&buf);
            if (trig->rep != NO_REP) return E_REP_TWICE;
            trig->rep = tok.val;
            break;

        case T_Delta:
            DBufFree(&buf);
            if (seen_delta) return E_DELTA_TWICE;
            seen_delta = 1;
            trig->delta = tok.val;
            break;

        case T_Back:
            DBufFree(&buf);
            if (trig->back != NO_BACK) return E_BACK_TWICE;
            trig->back = tok.val;
            break;

        case T_BackAdj:
            DBufFree(&buf);
            if (trig->back != NO_BACK) return E_BACK_TWICE;
            if (trig->d != NO_DAY) return E_DAY_TWICE;
            trig->back = tok.val;
            trig->d = 1;
            trig->adj_for_last = 1;
            break;

        case T_Once:
            DBufFree(&buf);
            if (trig->once != NO_ONCE) return E_ONCE_TWICE;
            trig->once = ONCE_ONCE;
            break;

        case T_AddOmit:
            DBufFree(&buf);
            trig->addomit = 1;
            break;

        case T_NoQueue:
            DBufFree(&buf);
            trig->noqueue = 1;
            break;

        case T_Omit:
            DBufFree(&buf);
            if (trig->omitfunc[0]) {
                Wprint(tr("Warning: OMIT is ignored if you use OMITFUNC"));
            }

            r = ParseLocalOmit(s, trig);
            if (r) return r;
            break;

        case T_Empty:
            DBufFree(&buf);
            parsing = 0;
            break;

        case T_OmitFunc:
            if (trig->localomit) {
                Wprint(tr("Warning: OMIT is ignored if you use OMITFUNC"));
            }
            r=ParseToken(s, &buf);
            if (r) return r;
            StrnCpy(trig->omitfunc, DBufValue(&buf), VAR_NAME_LEN);
            strtolower(trig->omitfunc);
            /* An OMITFUNC counts as a nonconst_expr! */
            s->expr_happened = 1;
            nonconst_debug(s->nonconst_expr, tr("OMITFUNC counts as a non-constant expression"));
            s->nonconst_expr = 1;
            trig->nonconst_expr = 1;
            DBufFree(&buf);
            break;

        case T_Warn:
            r=ParseToken(s, &buf);
            if(r) return r;
            StrnCpy(trig->warn, DBufValue(&buf), VAR_NAME_LEN);
            strtolower(trig->warn);
            DBufFree(&buf);
            break;

        case T_Tz:
            if (trig->tz) {
                return E_TZ_SPECIFIED_TWICE;
            }

            r = ParseTokenOrQuotedString(s, &buf);
            if (r != OK) {
                return r;
            }
            trig->tz = strdup(DBufValue(&buf));
            if (!trig->tz) {
                return E_NO_MEM;
            }
            DBufFree(&buf);
            break;

        case T_Info:
            r = ParseQuotedString(s, &buf);
            if (r != OK) {
                return r;
            }
            r = AppendTrigInfo(trig, DBufValue(&buf));
            DBufFree(&buf);
            if (r) return r;
            break;
        case T_Tag:
            r = ParseToken(s, &buf);
            if (r) return r;
            if (strchr(DBufValue(&buf), ',')) {
                DBufFree(&buf);
                return E_PARSE_ERR;
            }
            AppendTag(&(trig->tags), DBufValue(&buf));
            DBufFree(&buf);
            break;

        case T_Duration:
            r = ParseToken(s, &buf);
            if (r) return r;
            FindToken(DBufValue(&buf), &tok);
            DBufFree(&buf);
            switch(tok.type) {
            case T_Time:
            case T_LongTime:
            case T_Year:
            case T_Day:
            case T_Number:
                if (tok.val != 0) {
                    tim->duration = tok.val;
                } else {
                    tim->duration = NO_TIME;
                }
                trig->duration_days = ComputeTrigDuration(tim);
                break;
            default:
                return E_BAD_TIME;
            }
            break;

        case T_Sched:
            r=ParseToken(s, &buf);
            if(r) return r;
            StrnCpy(trig->sched, DBufValue(&buf), VAR_NAME_LEN);
            strtolower(trig->sched);
            DBufFree(&buf);
            break;

        case T_LongTime:
            DBufFree(&buf);
            return E_BAD_TIME;
            break;

        default:
            if (tok.type == T_Illegal && tok.val < 0) {
                Eprint("%s: `%s'", GetErr(-tok.val), DBufValue(&buf));
                DBufFree(&buf);
                return -tok.val;
            }
            PushToken(DBufValue(&buf), s);
            DBufFree(&buf);
            trig->typ = MSG_TYPE;
            if (s->isnested) return E_CANT_NEST_RTYPE;
            if (!WarnedAboutImplicit && !SuppressImplicitRemWarnings) {
                if (warning_level("05.00.03")) {
                    Wprint(tr("Missing REM type; assuming MSG"));
                }
                WarnedAboutImplicit = 1;
            }
            parsing = 0;
            break;
        }
    }

    if (s->nonconst_expr) {
        trig->nonconst_expr = 1;
    }
    if (s->expr_happened) {
        trig->expr_happened = 1;
    }
    if (trig->need_wkday && trig->wd == NO_WD) {
        Eprint("Weekday name(s) required");
        return E_PARSE_ERR;
    }

    /* Adjust month and possibly year */
    if (trig->adj_for_last) {
        if (trig->m != NO_MON) {
            trig->m++;
            if (trig->m >= 12) {
                trig->m = 0;
                if (trig->y != NO_YR) {
                    trig->y++;
                }
            }
        }
        trig->adj_for_last = 0;
    }

    /* Check for some warning conditions */
    if (warning_level("06.01.03")) {
        if (trig->addomit) {
            if (trig->y == NO_YR || trig->m == NO_MON || trig->d == NO_DAY || trig->rep != NO_REP) {
                if (trig->scanfrom == NO_SCANFROM) {
                    if (trig->y == NO_YR && trig->m != NO_MON && trig->d != NO_DAY) {
                        Wprint(tr("Warning: Consider using SCANFROM -28 with recurring ADDOMIT"));
                    } else if (trig->m == NO_MON && trig->d != NO_DAY) {
                        Wprint(tr("Warning: Consider using SCANFROM -7 with recurring ADDOMIT"));
                    } else {
                        Wprint(tr("Warning: Consider using SCANFROM with recurring ADDOMIT"));
                    }
                }
            }
        }
    }

    if (!trig->nonconst_expr) {
        if (trig->y != NO_YR && trig->m != NO_MON && trig->d != NO_DAY && trig->until != NO_UNTIL) {
            if (DSE(trig->y, trig->m, trig->d) > trig->until) {
                Wprint(tr("Warning: UNTIL/THROUGH date earlier than start date"));
            }
        }
        if (trig->from != NO_DATE) {
            if (trig->until != NO_UNTIL && trig->until < trig->from) {
                Wprint(tr("Warning: UNTIL/THROUGH date earlier than FROM date"));
            }
        } else if (get_raw_scanfrom(trig) != NO_SCANFROM) {
            if (trig->until != NO_UNTIL && trig->until < get_raw_scanfrom(trig)) {
                Wprint(tr("Warning: UNTIL/THROUGH date earlier than SCANFROM date"));
            }
        }
    }

    if (trig->y != NO_YR && trig->m != NO_MON && trig->d != NO_DAY && trig->until != NO_UNTIL && trig->rep == NO_REP) {
        Wprint(tr("Warning: Useless use of UNTIL with fully-specified date and no *rep"));
    }

    /* Check that any SCHED / WARN / OMITFUNC functions refer to
       their arguments */
    if (warning_level("05.00.03")) {
        check_trigger_function(trig->sched, "SCHED");
        check_trigger_function(trig->warn, "WARN");
        check_trigger_function(trig->omitfunc, "OMITFUNC");
    }

    /* If we have a SCHED, ignore any time delta */
    if (trig->sched[0]) {
        tim->delta = NO_DELTA;
    }

    /* Check that TZ looks plausible */
    if (trig->tz && (*trig->tz != '!')) {
        warn_if_timezone_bad(trig->tz);
    }

    /* Remove leading ! from TZ spec */
    if (trig->tz && *trig->tz == '!') {
        char *stz = (char *) trig->tz;
        while (*stz) {
            *stz = *(stz+1);
            stz++;
        }
    }
    return OK;
}

/***************************************************************/
/*                                                             */
/*  ParseTimeTrig - parse the AT part of a timed reminder      */
/*                                                             */
/***************************************************************/
static int ParseTimeTrig(ParsePtr s, TimeTrig *tim)
{
    Token tok;
    int r;
    int seen_delta = 0;
    DynamicBuffer buf;
    DBufInit(&buf);

    while(1) {
        r = ParseToken(s, &buf);
        if (r) return r;
        FindToken(DBufValue(&buf), &tok);
        switch(tok.type) {
        case T_Time:
            DBufFree(&buf);
            if (tim->ttime != NO_TIME) return E_TIME_TWICE;
            tim->ttime = tok.val;
            tim->ttime_orig = tok.val;
            break;

        case T_Delta:
            DBufFree(&buf);
            if (seen_delta) return E_DELTA_TWICE;
            seen_delta = 1;
            tim->delta = (tok.val >= 0) ? tok.val : -tok.val;
            break;

        case T_Rep:
            DBufFree(&buf);
            if (tim->rep != NO_REP) return E_REP_TWICE;
            tim->rep = tok.val;
            break;

        default:
            if (tok.type == T_Illegal && tok.val < 0) {
                Eprint("%s: `%s'", GetErr(-tok.val), DBufValue(&buf));
                DBufFree(&buf);
                return -tok.val;
            }
            if (tim->ttime == NO_TIME) return E_EXPECT_TIME;

            PushToken(DBufValue(&buf), s);
            DBufFree(&buf);
            return OK;
        }
    }
}

/***************************************************************/
/*                                                             */
/*  ParseLocalOmit - parse the local OMIT portion of a         */
/*  reminder.                                                  */
/*                                                             */
/***************************************************************/
static int ParseLocalOmit(ParsePtr s, Trigger *t)
{
    Token tok;
    int r;
    DynamicBuffer buf;
    DBufInit(&buf);

    while(1) {
        r = ParseToken(s, &buf);
        if (r) return r;
        FindToken(DBufValue(&buf), &tok);
        switch(tok.type) {
        case T_WkDay:
            DBufFree(&buf);
            t->localomit |= (1 << tok.val);
            break;

        default:
            if (t->localomit == NO_WD) {
                return E_EXPECTING_WEEKDAY;
            }
            PushToken(DBufValue(&buf), s);
            DBufFree(&buf);
            return OK;
        }
    }
}

/***************************************************************/
/*                                                             */
/*  ParseUntil - parse the UNTIL portion of a reminder         */
/*                                                             */
/***************************************************************/
static int ParseUntil(ParsePtr s, Trigger *t, int type)
{

    char const *which;
    int dse;

    if (type == T_Until) {
        which = "UNTIL";
    } else {
        which = "THROUGH";
    }

    if (t->until != NO_UNTIL) return E_UNTIL_TWICE;

    int r = GetFullDate(s, which, &dse);
    if (r != OK) {
        return r;
    }

    t->until = dse;
    return OK;
}

/***************************************************************/
/*                                                             */
/*  ParseScanFrom - parse the FROM/SCANFROM portion            */
/*                                                             */
/***************************************************************/
static int ParseScanFrom(ParsePtr s, Trigger *t, int type)
{
    int r;
    int y = NO_YR, m = NO_MON, d = NO_DAY;
    Token tok;
    DynamicBuffer buf;
    char const *word;

    DBufInit(&buf);
    if (type == SCANFROM_TYPE) {
        word = "SCANFROM";
    } else {
        word = "FROM";
    }

    if (t->scanfrom != NO_SCANFROM) return E_SCAN_TWICE;
    if (t->from != NO_DATE) return E_SCAN_TWICE;
    while(1) {
        r = ParseToken(s, &buf);
        if (r) return r;
        FindToken(DBufValue(&buf), &tok);
        switch(tok.type) {
        case T_Year:
            DBufFree(&buf);
            if (y != NO_YR) {
                Eprint("%s: %s", word, GetErr(E_YR_TWICE));
                return E_YR_TWICE;
            }
            y = tok.val;
            break;

        case T_Month:
            DBufFree(&buf);
            if (m != NO_MON) {
                Eprint("%s: %s", word, GetErr(E_MON_TWICE));
                return E_MON_TWICE;
            }
            m = tok.val;
            break;

        case T_Day:
            DBufFree(&buf);
            if (d != NO_DAY) {
                Eprint("%s: %s", word, GetErr(E_DAY_TWICE));
                return E_DAY_TWICE;
            }
            d = tok.val;
            break;

        case T_Date:
            DBufFree(&buf);
            if (y != NO_YR) {
                Eprint("%s: %s", word, GetErr(E_YR_TWICE));
                return E_YR_TWICE;
            }
            if (m != NO_MON) {
                Eprint("%s: %s", word, GetErr(E_MON_TWICE));
                return E_MON_TWICE;
            }
            if (d != NO_DAY) {
                Eprint("%s: %s", word, GetErr(E_DAY_TWICE));
                return E_DAY_TWICE;
            }
            if (type == FROM_TYPE) {
                t->from = tok.val;
            } else {
                t->scanfrom = tok.val;
            }
            return OK;

        case T_Back:
            DBufFree(&buf);
            if (type != SCANFROM_TYPE) {
                Eprint("%s: %s", word, GetErr(E_INCOMPLETE));
                return E_INCOMPLETE;
            }
            if (y != NO_YR) {
                Eprint("%s: %s", word, GetErr(E_YR_TWICE));
                return E_YR_TWICE;
            }
            if (m != NO_MON) {
                Eprint("%s: %s", word, GetErr(E_MON_TWICE));
                return E_MON_TWICE;
            }
            if (d != NO_DAY) {
                Eprint("%s: %s", word, GetErr(E_DAY_TWICE));
                return E_DAY_TWICE;
            }
            if (tok.val > 0) {
                tok.val = -tok.val;
            }
            t->scanfrom = tok.val;
            s->expr_happened = 1;
            nonconst_debug(s->nonconst_expr, tr("Relative SCANFROM counts as a non-constant expression"));
            s->nonconst_expr = 1;
            t->nonconst_expr = 1;
            return OK;

        default:
            if (tok.type == T_Illegal && tok.val < 0) {
                Eprint("%s: `%s'", GetErr(-tok.val), DBufValue(&buf));
                DBufFree(&buf);
                return -tok.val;
            }
            if (y == NO_YR || m == NO_MON || d == NO_DAY) {
                Eprint("%s: %s", word, GetErr(E_INCOMPLETE));
                DBufFree(&buf);
                return E_INCOMPLETE;
            }
            if (!DateOK(y, m, d)) {
                DBufFree(&buf);
                return E_BAD_DATE;
            }
            if (type == FROM_TYPE) {
                t->from = DSE(y, m, d);
            } else {
                t->scanfrom = DSE(y, m, d);
            }

            PushToken(DBufValue(&buf), s);
            DBufFree(&buf);
            return OK;
        }
    }
}


/***************************************************************/
/*                                                             */
/*  TriggerReminder                                            */
/*                                                             */
/*  Trigger the reminder if it's a RUN or MSG type.            */
/*                                                             */
/***************************************************************/
int TriggerReminder(ParsePtr p, Trigger *t, TimeTrig const *tim, int dse, int is_queued, DynamicBuffer *output, int *rr, int *gg, int *bb)
{
    int r, y, m, d;
    int adjusted_for_newline = 0;
    char PrioExpr[VAR_NAME_LEN+25];
    char tmpBuf[64];
    DynamicBuffer buf, calRow;
    DynamicBuffer pre_buf;
    char const *s;
    char const *msg_command = NULL;
    char const *url;
    Value v;

    if (MsgCommand) {
        msg_command = MsgCommand;
    }
    if (is_queued && QueuedMsgCommand) {
        msg_command = QueuedMsgCommand;
    }

    /* A null command is no command */
    if (msg_command && !*msg_command) {
        msg_command = NULL;
    }

    int red = -1, green = -1, blue = -1;
    int is_color = 0;

    DBufInit(&buf);
    DBufInit(&calRow);
    DBufInit(&pre_buf);
    if (t->typ == RUN_TYPE && RunDisabled) return E_RUN_DISABLED;
    if (t->typ == PASSTHRU_TYPE && !strcasecmp(t->passthru, "FORMATTED")) {
        t->typ = FORMATTED_TYPE;
    }
    if ((t->typ == PASSTHRU_TYPE && strcasecmp(t->passthru, "COLOR") && strcasecmp(t->passthru, "COLOUR")) ||
        t->typ == CAL_TYPE ||
        t->typ == PS_TYPE ||
        t->typ == PSF_TYPE)
        return OK;

    /* Handle COLOR types */
    if (t->typ == PASSTHRU_TYPE && (!strcasecmp(t->passthru, "COLOR") || !strcasecmp(t->passthru, "COLOUR"))) {
        /* Strip off three tokens */
        r = ParseToken(p, &buf);
        sscanf(DBufValue(&buf), "%d", &red);
        if (!NextMode) {
            DBufPuts(&pre_buf, DBufValue(&buf));
            DBufPutc(&pre_buf, ' ');
        }
        DBufFree(&buf);
        if (r) return r;
        r = ParseToken(p, &buf);
        sscanf(DBufValue(&buf), "%d", &green);
        if (!NextMode) {
            DBufPuts(&pre_buf, DBufValue(&buf));
            DBufPutc(&pre_buf, ' ');
        }
        DBufFree(&buf);
        if (r) return r;
        r = ParseToken(p, &buf);
        sscanf(DBufValue(&buf), "%d", &blue);
        if (!NextMode) {
            DBufPuts(&pre_buf, DBufValue(&buf));
            DBufPutc(&pre_buf, ' ');
        }
        DBufFree(&buf);
        if (r) return r;
        t->typ = MSG_TYPE;

        if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
            red = -1;
            green = -1;
            blue = -1;
            t->passthru[0] = 0;
        }
    }
/* If it's a MSG-type reminder, and no -k option was used, issue the banner. */
    if ((t->typ == MSG_TYPE || t->typ == MSF_TYPE || t->typ == FORMATTED_TYPE)
        && !DidMsgReminder && !NextMode && !msg_command && !is_queued) {
        DynamicBuffer buf2;
        DBufInit(&buf2);
        DidMsgReminder = 1;
        if (!DoSubstFromString(DBufValue(&Banner), &buf2,
                               DSEToday, NO_TIME) &&
            DBufLen(&buf2)) {
            if (!JSONMode) {
                printf("%s\n", DBufValue(&buf2));
            } else {
                if (JSONLinesEmitted) {
                    printf("},\n");
                }
                JSONLinesEmitted++;
                printf("{\"banner\":\"");
                remove_trailing_newlines(&buf2);
                PrintJSONString(DBufValue(&buf2));
                printf("\"");
            }
        }
        DBufFree(&buf2);
    }

/* If it's NextMode, process as a ADVANCE_MODE-type entry, and issue
   simple-calendar format. */
    if (NextMode) {
        if ( (r=DoSubst(p, &buf, t, tim, dse, ADVANCE_MODE)) ) return r;
        if (!DBufLen(&buf)) {
            DBufFree(&buf);
            DBufFree(&pre_buf);
            return OK;
        }
        FromDSE(dse, &y, &m, &d);
        snprintf(tmpBuf, sizeof(tmpBuf), "%04d/%02d/%02d ", y, m+1, d);
        if (DBufPuts(&calRow, tmpBuf) != OK) {
            DBufFree(&calRow);
            DBufFree(&pre_buf);
            return E_NO_MEM;
        }
        /* If DoSimpleCalendar==1, output *all* simple calendar fields */
        if (DoSimpleCalendar) {
            /* ignore passthru field when in NextMode */
            if (DBufPuts(&calRow, "* ") != OK) {
                DBufFree(&calRow);
                DBufFree(&pre_buf);
                return E_NO_MEM;
            }
            if (*DBufValue(&(t->tags))) {
                DBufPuts(&calRow, DBufValue(&(t->tags)));
                DBufPutc(&calRow, ' ');
            } else {
                DBufPuts(&calRow, "* ");
            }
            if (tim->duration != NO_TIME) {
                snprintf(tmpBuf, sizeof(tmpBuf), "%d ", tim->duration);
            } else {
                snprintf(tmpBuf, sizeof(tmpBuf), "* ");
            }
            if (DBufPuts(&calRow, tmpBuf) != OK) {
                DBufFree(&calRow);
                DBufFree(&pre_buf);
                return E_NO_MEM;
            }
            if (tim->ttime != NO_TIME) {
                snprintf(tmpBuf, sizeof(tmpBuf), "%d ", tim->ttime);
            } else {
                snprintf(tmpBuf, sizeof(tmpBuf), "* ");
            }
            if (DBufPuts(&calRow, tmpBuf) != OK) {
                DBufFree(&calRow);
                DBufFree(&pre_buf);
                return E_NO_MEM;
            }
        }
        if (DBufPuts(&calRow, SimpleTime(tim->ttime)) != OK) {
            DBufFree(&calRow);
            DBufFree(&pre_buf);
            return E_NO_MEM;
        }

        r = OK;
        if (output) {
            if (DBufPuts(output, DBufValue(&calRow)) != OK) r = E_NO_MEM;
            if (DBufPuts(output, DBufValue(&pre_buf)) != OK) r = E_NO_MEM;
            if (DBufPuts(output, DBufValue(&buf)) != OK) r = E_NO_MEM;
        } else {
            printf("%s%s%s\n", DBufValue(&calRow), DBufValue(&pre_buf), DBufValue(&buf));
        }
        DBufFree(&buf);
        DBufFree(&pre_buf);
        DBufFree(&calRow);
        return r;
    }

    /* Correct colors */
    if (JSONMode || UseVTColors) {
        if (red == -1 && green == -1 && blue == -1) {
            if (DefaultColorR != -1 && DefaultColorG != -1 && DefaultColorB != -1) {
                red = DefaultColorR;
                green = DefaultColorG;
                blue = DefaultColorB;
            }
        }
        if (red >= 0 && green >= 0 && blue >= 0) {
            is_color = 1;
            if (red > 255) red = 255;
            if (green > 255) green = 255;
            if (blue > 255) blue = 255;
        }
        if (rr) *rr = red;
        if (gg) *gg = green;
        if (bb) *bb = blue;

        /* Don't ANSI-colorize JSON output! */
        if (JSONMode) {
            is_color = 0;
            red = -1;
            green = -1;
            blue = -1;
        }
    }

    /* Put the substituted string into the substitution buffer */

    /* Don't use msgprefix() on RUN-type reminders */
    if (t->typ != RUN_TYPE) {
        if (UserFuncExists("msgprefix") == 1) {
            snprintf(PrioExpr, sizeof(PrioExpr), "msgprefix(%d)", t->priority);
            s = PrioExpr;
            r = EvalExpr(&s, &v, NULL);
            if (!r) {
                if (!DoCoerce(STR_TYPE, &v)) {
                    if (is_color) {
                        DBufPuts(&buf, Colorize(red, green, blue, 0, 1));
                    }
                    if (DBufPuts(&buf, v.v.str) != OK) {
                        DBufFree(&buf);
                        DestroyValue(v);
                        return E_NO_MEM;
                    }
                }
                DestroyValue(v);
            }
        }
    }

    if (is_color) {
        DBufPuts(&buf, Colorize(red, green, blue, 0, 1));
    }
    if ( (r=DoSubst(p, &buf, t, tim, dse, NORMAL_MODE)) ) return r;

    if (t->typ != RUN_TYPE) {
        if (UserFuncExists("msgsuffix") == 1) {
            snprintf(PrioExpr, sizeof(PrioExpr), "msgsuffix(%d)", t->priority);
            s = PrioExpr;
            r = EvalExpr(&s, &v, NULL);
            if (!r) {
                if (!DoCoerce(STR_TYPE, &v)) {
                    /* Hack: If MsgSuffix starts with a backspace, move it before the newline! */
                    if (*v.v.str == '\b') {
                        if (DBufLen(&buf)) {
                            if (DBufValue(&buf)[DBufLen(&buf)-1] == '\n') {
                                /* VIOLATION of encapsulation! */
                                DBufValue(&buf)[DBufLen(&buf)-1] = 0;
                                buf.len--;
                                adjusted_for_newline = 1;
                            }
                        }
                    }
                    if (is_color) {
                        DBufPuts(&buf, Colorize(red, green, blue, 0, 1));
                    }
                    if (*v.v.str == '\b') {
                        r = DBufPuts(&buf, v.v.str+1);
                    } else {
                        r = DBufPuts(&buf, v.v.str);
                    }
                    if (r != OK) {
                        DBufFree(&buf);
                        DestroyValue(v);
                        return E_NO_MEM;
                    }
                }
                DestroyValue(v);
            }
        }
    }

    if (is_color) {
        DBufPuts(&buf, Decolorize());
    }
    if (adjusted_for_newline) {
        DBufPutc(&buf, '\n');
    }

    if ((!msg_command && t->typ == MSG_TYPE) || t->typ == MSF_TYPE) {
        if (DBufPutc(&buf, '\n') != OK) {
            DBufFree(&buf);
            return E_NO_MEM;
        }
    }

    /* If this is a dupe and we are de-duping, do nothing */
    if (DedupeReminders) {
        if (ShouldDedupe(dse, tim->ttime, DBufValue(&buf))) {
            DBufFree(&buf);
            return OK;
        }
    }

    /* Get the url if terminal hyperlinks are enabled */
    if (TerminalHyperlinks) {
        url = FindTrigInfo(t, "url");
    } else {
        url = NULL;
    }

    /* If we are sorting, just queue it up in the sort buffer */
    if (SortByDate) {
        if (InsertIntoSortBuffer(dse, tim->ttime, url, DBufValue(&buf),
                                 t->typ, t->priority) == OK) {
            DBufFree(&buf);
            NumTriggered++;
            return OK;
        }
    }

/* If we didn't insert the reminder into the sort buffer, issue the
   reminder now. */
    r = OK;
    switch(t->typ) {
    case MSG_TYPE:
    case PASSTHRU_TYPE:
        if (msg_command) {
            DoMsgCommand(msg_command, DBufValue(&buf), is_queued);
        } else {
            if (output) {
                DBufPuts(output, DBufValue(&buf));
            } else {
                /* Add a space before "NOTE endreminder" */
                if (IsServerMode() && !strncmp(DBufValue(&buf), "NOTE endreminder", 16)) {
                    printf(" %s", DBufValue(&buf));
                } else {
                    if (url) {
                        printf("\x1B]8;;%s\x1B\\", url);
                    }
                    printf("%s", DBufValue(&buf));
                    if (url) {
                        printf("\x1B]8;;\x1B\\");
                    }
                }
            }
        }
        break;

    case MSF_TYPE:
        FillParagraph(url, DBufValue(&buf), output);
        break;

    case FORMATTED_TYPE:
        if (!JSONMode) {
            r = OutputFormattedReminder(url, DBufValue(&buf), output);
        } else {
            if (output) {
                if (NoFormat) {
                    r = OutputFormattedReminder(NULL, DBufValue(&buf), output);
                } else {
                    DBufPuts(output, DBufValue(&buf));
                }
            }
        }
        break;

    case RUN_TYPE:
        System(DBufValue(&buf), is_queued);
        break;

    default: /* Unknown/illegal type? */
        DBufFree(&buf);
        return E_SWERR;
    }

    DBufFree(&buf);
    if (r == OK) {
        NumTriggered++;
    }
    return r;
}

/***************************************************************/
/*                                                             */
/*  ShouldTriggerReminder                                      */
/*                                                             */
/*  Return 1 if we should trigger a reminder, based on today's */
/*  date and the trigger.  Return 0 if reminder should not be  */
/*  triggered.  Sets *err non-zero in event of an error.       */
/*                                                             */
/***************************************************************/
int ShouldTriggerReminder(Trigger const *t, TimeTrig const *tim, int dse, int *err)
{
    int r, omit;
    int calmode = (DoSimpleCalendar || DoCalendar) ? 1 : 0;
    if (HideCompletedTodos) calmode = 0;

    *err = 0;

    /* Handle the ONCE modifier in the reminder. */
    if (!IgnoreOnce && t->once !=NO_ONCE && GetOnceDate() == DSEToday)
        return 0;

    /* TODOs are handled differently */
    if (t->is_todo && !calmode) {
        /* Do NOT trigger if TODO has been completed through today (or later) */
        if (t->complete_through != NO_DATE && t->complete_through >= DSEToday && dse <= t->complete_through) {
            return 0;
        }
        /* DO trigger if has not been completed through trigger date */
        if (t->complete_through == NO_DATE || t->complete_through < dse) {
            /* Trigger date is in the past - overdue,  Trigger unless we're
               more than max_overdue days late */
            if (dse < DSEToday) {
                if (t->max_overdue >= 0) {
                    if (dse + t->max_overdue < DSEToday) {
                        return 0;
                    }
                }
                return 1;
            }
            /* Trigger date in future - use normal Remind rules */
        } else {
            /* We're complete as of trigger date */
            return 0;
        }
    } else {
        if (dse < DSEToday) return 0;
    }

    /* Don't trigger timed reminders if DontIssueAts is true, and if the
       reminder is for today */
    if (dse == DSEToday && DontIssueAts && tim->ttime != NO_TIME) {
        if (DontIssueAts > 1) {
            /* If two or more -a options, then *DO* issue ats that are in the
               future */
            if (tim->ttime < MinutesPastMidnight(0)) {
                return 0;
            }
        } else {
            return 0;
        }
    }

    /* If "infinite delta" option is chosen, always trigger future reminders */
    if (InfiniteDelta || NextMode) return 1;

    /* If there's a "warn" function, it overrides any deltas except
     * DeltaOverride*/
    if (t->warn[0] != 0) {
        if (DeltaOverride > 0) {
            if (dse <= DSEToday + DeltaOverride) {
                return 1;
            }
        }
        return ShouldTriggerBasedOnWarn(t, dse, err);
    }

    /* Zero delta */
    if (DeltaOverride < 0) {
        return dse == DSEToday;
    }

    /* Move back by delta days, if any */
    if (DeltaOverride) {
        /* A positive DeltaOverride takes precedence over everything */
        dse = dse - DeltaOverride;
    } else if (t->delta != NO_DELTA) {
        if (t->delta < 0)
            dse = dse + t->delta;
        else {
            int iter = 0;
            int max = MaxSatIter;
            r = t->delta;
            if (max < r*2) max = r*2;
            while(iter++ < max) {
                if (!r || (dse <= DSEToday)) {
                    break;
                }
                dse--;
                *err = IsOmitted(dse, t->localomit, t->omitfunc, &omit);
                if (*err) return 0;
                if (!omit) r--;
            }
            if (iter > max) {
                *err = E_CANT_TRIG;
                Eprint("Delta: Bad OMITFUNC? %s", GetErr(E_CANT_TRIG));
                return 0;
            }
        }
    }

    /* Should we trigger the reminder? */
    return (dse <= DSEToday);
}

/***************************************************************/
/*                                                             */
/*  DoSatRemind                                                */
/*                                                             */
/*  Do the "satisfying..." remind calculation.                 */
/*                                                             */
/***************************************************************/
int DoSatRemind(Trigger *trig, TimeTrig *tt, ParsePtr p)
{
    int iter, dse, r, start;
    Value v;
    expr_node *sat_node;
    int nonconst = 0;

    sat_node = ParseExpr(p, &r);
    if (r != OK) {
        return r;
    }
    if (!sat_node) {
        return E_SWERR;
    }

    /* Diagnose if SAT_NODE does not reference trigdate */
    ensure_satnode_mentions_trigdate(sat_node);

    iter = 0;
    start = get_scanfrom(trig);
    while (iter++ < MaxSatIter) {
        dse = ComputeTriggerNoAdjustDuration(start, trig, tt, &r, 1, 0);
        if (r) {
            free_expr_tree(sat_node);
            if (r == E_CANT_TRIG) return OK; else return r;
        }
        if (dse != start && trig->duration_days) {
            dse = ComputeTriggerNoAdjustDuration(start, trig, tt, &r, 1, trig->duration_days);
            if (r) {
                free_expr_tree(sat_node);
                if (r == E_CANT_TRIG) return OK; else return r;
            }
        } else if (dse == start) {
            if (tt->ttime != NO_TIME) {
                trig->eventstart = MINUTES_PER_DAY * r + tt->ttime;
                trig->eventstart_orig = trig->eventstart;
                if (tt->duration != NO_TIME) {
                    trig->eventduration = tt->duration;
                }
            }
            SaveAllTriggerInfo(trig, tt, dse, tt->ttime, 1);
        }
        if (dse == -1) {
            free_expr_tree(sat_node);
            LastTrigValid = 0;
            LastTriggerDate = -1;
            return E_EXPIRED;
        }
        r = evaluate_expression(sat_node, NULL, &v, &nonconst);
        if (r) {
            free_expr_tree(sat_node);
            return r;
        }
        if (v.type != INT_TYPE && v.type != STR_TYPE) {
            free_expr_tree(sat_node);
            return E_BAD_TYPE;
        }
        if ((v.type == INT_TYPE && v.v.val) ||
            (v.type == STR_TYPE && *v.v.str)) {
            AdjustTriggerForDuration(get_scanfrom(trig), dse, trig, tt, 1);
            if (DebugFlag & DB_PRTTRIG) {
                int y, m, d;
                FromDSE(LastTriggerDate, &y, &m, &d);
                fprintf(ErrFp, "%s(%s): Trig(satisfied) = %s, %d %s, %d",
                        GetCurrentFilename(), line_range(LineNoStart, LineNo),
                        get_day_name(LastTriggerDate % 7),
                        d,
                        get_month_name(m),
                        y);
                if (tt->ttime != NO_TIME) {
                    fprintf(ErrFp, " AT %02d:%02d",
                            (tt->ttime / 60),
                            (tt->ttime % 60));
                    if (tt->duration != NO_TIME) {
                        fprintf(ErrFp, " DURATION %02d:%02d",
                                (tt->duration / 60),
                                (tt->duration % 60));
                    }
                }
                fprintf(ErrFp, "\n");
            }
            free_expr_tree(sat_node);
            return OK;
        }
        if (dse+trig->duration_days < start) {
            start++;
        } else {
            start = dse+trig->duration_days+1;
        }
    }
    LastTrigValid = 0;
    free_expr_tree(sat_node);
    return E_CANT_TRIG;
}

/***************************************************************/
/*                                                             */
/*  ParsePriority - parse the PRIORITY portion of a reminder   */
/*                                                             */
/***************************************************************/
static int ParsePriority(ParsePtr s, Trigger *t)
{
    int p, r;
    char const *u;
    DynamicBuffer buf;
    DBufInit(&buf);

    r = ParseToken(s, &buf);
    if(r) return r;
    u = DBufValue(&buf);

    if (!Isdigit(*u)) {
        DBufFree(&buf);
        return E_EXPECTING_NUMBER;
    }
    p = 0;
    while (Isdigit(*u)) {
        p = p*10 + *u - '0';
        u++;
    }
    if (*u) {
        DBufFree(&buf);
        return E_EXPECTING_NUMBER;
    }

    DBufFree(&buf);

/* Tricky!  The only way p can be < 0 is if overflow occurred; thus,
   E2HIGH is indeed the appropriate error message. */
    if (p<0 || p>9999) return E_2HIGH;
    t->priority = p;
    return OK;
}

/***************************************************************/
/*                                                             */
/*  DoMsgCommand                                               */
/*                                                             */
/*  Execute the '-k' command, escaping shell chars in message. */
/*                                                             */
/***************************************************************/
int DoMsgCommand(char const *cmd, char const *msg, int is_queued)
{
    int r;
    int i, l;
    DynamicBuffer execBuffer;

    DynamicBuffer buf;

    DBufInit(&buf);
    DBufInit(&execBuffer);

    /* Escape shell characters in msg */
    if (ShellEscape(msg, &buf) != OK) {
        r = E_NO_MEM;
        goto finished;
    }

    msg = DBufValue(&buf);

    /* Do "%s" substitution */
    l = strlen(cmd);
    for (i=0; i<l; i++) {
        if (cmd[i] == '%' && cmd[i+1] == 's') {
            ++i;
            if (DBufPuts(&execBuffer, msg) != OK) {
                r = E_NO_MEM;
                goto finished;
            }
        } else {
            if (DBufPutc(&execBuffer, cmd[i]) != OK) {
                r = E_NO_MEM;
                goto finished;
            }
        }
    }
    r = OK;

    System(DBufValue(&execBuffer), is_queued);

finished:
    DBufFree(&buf);
    DBufFree(&execBuffer);
    return r;
}

/***************************************************************/
/*                                                             */
/*  ShouldTriggerBasedOnWarn                                   */
/*                                                             */
/*  Determine whether to trigger a reminder based on its WARN  */
/*  function.                                                  */
/*                                                             */
/***************************************************************/
static int ShouldTriggerBasedOnWarn(Trigger const *t, int dse, int *err)
{
    char buffer[VAR_NAME_LEN+32];
    int i;
    char const *s;
    int r, omit;
    Value v;
    int lastReturnVal = 0; /* Silence compiler warning */
    int calmode = (DoSimpleCalendar || DoCalendar) ? 1 : 0;

    /* TODOs are handled differently */
    if (t->is_todo && !calmode) {
        /* Do NOT trigger if TODO has been completed through today (or later) */
        if (t->complete_through != NO_DATE && t->complete_through >= DSEToday) {
            return 0;
        }
        /* DO trigger if has not been completed through trigger date */
        if (t->complete_through == NO_DATE || t->complete_through < dse) {
            /* Trigger date is in the past - overdue,  Trigger unless we're
               more than max_overdue days late */
            if (dse < DSEToday) {
                if (t->max_overdue >= 0) {
                    if (dse + t->max_overdue < DSEToday) {
                        return 0;
                    }
                }
                return 1;
            }
            /* Trigger date in future - use normal Remind rules */
        } else {
            /* We're complete as of trigger date */
            return 0;
        }
    }

    /* If no proper function exists, barf... */
    if (UserFuncExists(t->warn) != 1) {
        Eprint("%s: `%s'", GetErr(M_BAD_WARN_FUNC), t->warn);
        return (dse == DSEToday);
    }
    for (i=1; ; i++) {
        snprintf(buffer, sizeof(buffer), "%s(%d)", t->warn, i);
        s = buffer;
        r = EvalExpr(&s, &v, NULL);
        if (r) {
            Eprint("%s: `%s': %s", GetErr(M_BAD_WARN_FUNC),
                   t->warn, GetErr(r));
            return (dse == DSEToday);
        }
        if (v.type != INT_TYPE) {
            DestroyValue(v);
            Eprint("%s: `%s': %s", GetErr(M_BAD_WARN_FUNC),
                   t->warn, GetErr(E_BAD_TYPE));
            return (dse == DSEToday);
        }

        /* If absolute value of return is not monotonically
           decreasing, exit */
        if (i > 1 && abs(v.v.val) >= lastReturnVal) {
            return (dse == DSEToday);
        }

        lastReturnVal = abs(v.v.val);
        /* Positive values: Just subtract.  Negative values:
           skip omitted days. */
        if (v.v.val >= 0) {
            if (DSEToday + v.v.val == dse) return 1;
        } else {
            int j = dse;
            int iter = 0;
            int max = MaxSatIter;
            if (max < v.v.val * 2) max = v.v.val*2;
            while(iter++ <= max) {
                j--;
                *err = IsOmitted(j, t->localomit, t->omitfunc, &omit);
                if (*err) {
                    return (dse == DSEToday);
                }
                if (!omit) v.v.val++;
                if (!v.v.val) {
                    break;
                }
            }
            if (iter > max) {
                Eprint("Delta: Bad OMITFUNC? %s", GetErr(E_CANT_TRIG));
                return (dse == DSEToday);
            }
            if (j == DSEToday) return 1;
        }
    }
}

void FixSpecialType(Trigger *t)
{
    if (t->typ == FORMATTED_TYPE) {
        strcpy(t->passthru, "FORMATTED");
        return;
    }
    if (t->typ != PASSTHRU_TYPE) {
        return;
    }

    /* Convert SPECIAL MSG / MSF / RUN / CAL to just plain MSG / MSF / etc */
    if (!strcasecmp(t->passthru, "MSG")) {
        t->typ = MSG_TYPE;
    } else if (!strcasecmp(t->passthru, "MSF")) {
        t->typ = MSF_TYPE;
    } else if (!strcasecmp(t->passthru, "RUN")) {
        t->typ = RUN_TYPE;
    } else if (!strcasecmp(t->passthru, "CAL")) {
        t->typ = CAL_TYPE;
    } else if (!strcasecmp(t->passthru, "PS")) {
        t->typ = PS_TYPE;
    } else if (!strcasecmp(t->passthru, "PSFILE")) {
        t->typ = PSF_TYPE;
    }
}

static int
ShouldQueueReminder(Trigger const *t, TimeTrig const *tim, int dse)
{
    int sched_is_close_enough = 0;
    int r;

    if (DontQueue) {
        return 0;
    }
    if (t->noqueue) {
        return 0;
    }
    if (tim->ttime == NO_TIME) {
        return 0;
    }
    if (t->typ == CAL_TYPE) {
        return 0;
    }
    if (t->typ == RUN_TYPE && RunDisabled) {
        return 0;
    }

    if (dse * MINUTES_PER_DAY + tim->ttime < SystemDateTime(0)) {
        return 0;
    }

    /* If we have a sched function, calculate how far back it goes */
    if (t->sched[0] && UserFuncExists(t->sched) == 1) {
        char exprBuf[VAR_NAME_LEN+32];
        char const *s = exprBuf;
        Value v;
        int advance_dse;

        snprintf(exprBuf, sizeof(exprBuf), "%s(1)", t->sched);
        if (RunDisabled) {
            r = EvalExprRunDisabled(&s, &v, NULL);
        } else {
            r = EvalExpr(&s, &v, NULL);
        }
        if (r == 0) {
            if (v.type == TIME_TYPE || v.type == INT_TYPE) {
                advance_dse = dse * MINUTES_PER_DAY + v.v.val;
            } else {
                advance_dse = dse * MINUTES_PER_DAY + tim->ttime;
            }
            DestroyValue(v);
            if (advance_dse <= DSEToday * MINUTES_PER_DAY + MINUTES_PER_DAY - 1) {
                sched_is_close_enough = 1;
            }
        }
    }
    if (!sched_is_close_enough &&
        dse * MINUTES_PER_DAY + tim->ttime - tim->delta > DSEToday * MINUTES_PER_DAY + MINUTES_PER_DAY - 1) {
        /* Too far in the future */
        return 0;
    }

    if (todo_filtered(t)) {
        return 0;
    }
    if (IgnoreOnce) {
        return 1;
    }
    if (t->once == NO_ONCE || GetOnceDate() != DSEToday) {
        return 1;
    }
    return 0;
}
