/***************************************************************/
/*                                                             */
/*  TRIGGER.C                                                  */
/*                                                             */
/*  Routines for figuring out the trigger date of a reminder   */
/*                                                             */
/*  This file is part of REMIND.                               */
/*  Copyright (C) 1992-2024 by Dianne Skoll                    */
/*  SPDX-License-Identifier: GPL-2.0-only                      */
/*                                                             */
/***************************************************************/

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

#include <stdlib.h>
#include "types.h"
#include "protos.h"
#include "globals.h"
#include "err.h"

#define GOT_DAY 1
#define GOT_MON 2
#define GOT_YR 4
#define GOT_WD 8

#define ADVANCE_TO_WD(x, wd) while (! ((wd) & (1 << ((x)%7)))) (x)++

static int DSEYear(int dse);
static int DSEMonth(int dse);
static int NextSimpleTrig(int startdate, Trigger *trig, int *err);
static int GetNextTriggerDate(Trigger *trig, int start, int *err, int *nextstart);

/***************************************************************/
/*                                                             */
/*  NextSimpleTrig                                             */
/*                                                             */
/*  Compute the "simple" trigger date, taking into account     */
/*  ONLY the day of week, day, month and year components.      */
/*  Normally, returns -1 if the trigger has expired.  As a     */
/*  special case, if D, M, Y [WD] are specified, returns the   */
/*  DSE date, regardless of whether it's expired.  This is     */
/*  so that dates with a REP can be handled properly.          */
/*                                                             */
/***************************************************************/
static int NextSimpleTrig(int startdate, Trigger *trig, int *err)
{
    int typ = 0;
    int d, m, y, j, d2, m2, y2;

    *err = 0;
    FromDSE(startdate, &y, &m, &d);
    d2 = d;
    m2 = m;
    y2 = y;

    if (trig->d != NO_DAY) typ |= GOT_DAY;
    if (trig->m != NO_MON) typ |= GOT_MON;
    if (trig->y != NO_YR) typ |= GOT_YR;
    if (trig->wd != NO_WD) typ |= GOT_WD;
    switch(typ) {
    case 0:
        return startdate;

    case GOT_WD:
	ADVANCE_TO_WD(startdate, trig->wd);
	return startdate;

    case GOT_DAY:
	if (d > trig->d) {
	    m++;
	    if (m == 12) { m = 0; y++; }
	}
	while (trig->d > DaysInMonth(m, y)) {
            m++;
            if (m == 12) { m = 0; y++; }
        }
	j = DSE(y, m, trig->d);
	return j;

    case GOT_MON:
	if (m == trig->m) return startdate;
	else if (m > trig->m) return DSE(y+1, trig->m, 1);
	else return DSE(y, trig->m, 1);

    case GOT_YR:
	if (y == trig->y) return startdate;
	else if (y < trig->y) return DSE(trig->y, 0, 1);
	else return -1;

    case GOT_DAY+GOT_MON:
	if (trig->d > MonthDays[trig->m]) {
	    *err = E_BAD_DATE;
	    return -1;
	}

	if (m > trig->m || (m == trig->m && d > trig->d)) y++;
	/* Take care of Feb. 29 */
	while (trig->d > DaysInMonth(trig->m, y)) y++;
	return DSE(y, trig->m, trig->d);

    case GOT_DAY+GOT_YR:
	if (y < trig->y) return DSE(trig->y, 0, trig->d);
	else if (y > trig->y) return -1;

	if (d > trig->d) {
	    m++;
	    if (m == 12) return -1;
	}
	while (trig->d > DaysInMonth(m, trig->y)) m++;
	return DSE(trig->y, m, trig->d);

    case GOT_MON+GOT_YR:
	if (y > trig->y || (y == trig->y && m > trig->m)) return -1;
	if (y < trig->y) return DSE(trig->y, trig->m, 1);
	if (m == trig->m) return startdate;
	return DSE(trig->y, trig->m, 1);

    case GOT_DAY+GOT_MON+GOT_YR:
	if (trig->d > DaysInMonth(trig->m, trig->y)) {
	    *err = E_BAD_DATE;
	    return -1;
	}
	return DSE(trig->y, trig->m, trig->d);

    case GOT_YR+GOT_WD:
	if (y > trig->y) return -1;
	if (y < trig->y) j = DSE(trig->y, 0, 1);
	else j = startdate;
        ADVANCE_TO_WD(j, trig->wd);
	if (DSEYear(j) > trig->y) return -1;
	return j;

    case GOT_MON+GOT_WD:
	if (m == trig->m) {
	    j = startdate;
            ADVANCE_TO_WD(j, trig->wd);
	    if (DSEMonth(j) == trig->m) return j;
	}
	if (m >= trig->m) j = DSE(y+1, trig->m, 1);
	else j = DSE(y, trig->m, 1);
        ADVANCE_TO_WD(j, trig->wd);
	return j; /* Guaranteed to be within the month */

    case GOT_DAY+GOT_WD:
	if (m !=0 || y > BASE) {
	    m2 = m-1;
	    if (m2 < 0) { y2 = y-1; m2 = 11; }

	    /* If there are fewer days in previous month, no match */
	    if (trig->d <= DaysInMonth(m2, y2)) {
		j = DSE(y2, m2, trig->d);
                ADVANCE_TO_WD(j, trig->wd);
		if (j >= startdate) return j;

	    }
	}

	/* Try this month */
	if (trig->d <= DaysInMonth(m, y)) {
	    j = DSE(y, m, trig->d);
            ADVANCE_TO_WD(j, trig->wd);
	    if (j >= startdate) return j;
	}

	/* Argh!  Try next avail. month */
	m2 = m+1;
	if (m2 > 11) { m2 = 0; y++; }
	while (trig->d > DaysInMonth(m2, y)) m2++;
	j = DSE(y, m2, trig->d);
        ADVANCE_TO_WD(j, trig->wd);
	return j;

    case GOT_WD+GOT_YR+GOT_DAY:
	if (y > trig->y+1 || (y > trig->y && m>0)) return -1;
	if (y > trig->y) {
	    j = DSE(trig->y, 11, trig->d);
            ADVANCE_TO_WD(j, trig->wd);
	    if (j >= startdate) return j;
	} else if (y < trig->y) {
	    j = DSE(trig->y, 0, trig->d);
            ADVANCE_TO_WD(j, trig->wd);
	    return j;
	} else {
	    /* Try last month */
	    if (m > 0) {
		m2 = m-1;
		while (trig->d > DaysInMonth(m2, trig->y)) m2--;
		j = DSE(trig->y, m2, trig->d);
                ADVANCE_TO_WD(j, trig->wd);
                if (DSEYear(j) > trig->y) return -1;
		if (j >= startdate) return j;
	    }
	}
	/* Try this month */
	if (trig->d <= DaysInMonth(m, trig->y)) {
	    j = DSE(trig->y, m, trig->d);
            ADVANCE_TO_WD(j, trig->wd);
            if (DSEYear(j) > trig->y) return -1;
	    if (j >= startdate) return j;
	}

	/* Must be next month */
	if (m == 11) return -1;
	m++;
	while (trig->d > DaysInMonth(m, trig->d)) m++;
	j = DSE(trig->y, m, trig->d);
        ADVANCE_TO_WD(j, trig->wd);
        if (DSEYear(j) > trig->y) return -1;
	return j;

    case GOT_DAY+GOT_MON+GOT_WD:
	if (trig->d > MonthDays[trig->m]) {
	    *err = E_BAD_DATE;
	    return -1;
	}
	/* Back up a year in case we'll cross a year boundary*/
	if (y > BASE) {
	    y--;
	}

	/* Move up to the first valid year */
	while (trig->d > DaysInMonth(trig->m, y)) y++;

	/* Try last year */
	j = DSE(y, trig->m, trig->d);
        ADVANCE_TO_WD(j, trig->wd);
	if (j >= startdate) return j;

	/* Try this year */
	y++;
	while (trig->d > DaysInMonth(trig->m, y)) y++;
	j = DSE(y, trig->m, trig->d);
        ADVANCE_TO_WD(j, trig->wd);
	if (j >= startdate) return j;

	/* Must be next year */
	y++;
	while (trig->d > DaysInMonth(trig->m, y)) y++;
	j = DSE(y, trig->m, trig->d);
        ADVANCE_TO_WD(j, trig->wd);
	return j;

    case GOT_WD+GOT_MON+GOT_YR:
	if (y > trig->y || (y == trig->y && m > trig->m)) return -1;
        /* cppcheck-suppress knownConditionTrueFalse */
	if (trig->y > y || (trig->y == y && trig->m > m)) {
	    j = DSE(trig->y, trig->m, 1);
            ADVANCE_TO_WD(j, trig->wd);
	    return j;
	} else {
	    j = startdate;
            ADVANCE_TO_WD(j, trig->wd);
	    FromDSE(j, &y2, &m2, &d2);
	    if (m2 == trig->m) return j; else return -1;
	}

    case GOT_WD+GOT_DAY+GOT_MON+GOT_YR:
	if (trig->d > DaysInMonth(trig->m, trig->y)) {
	    *err = E_BAD_DATE;
	    return -1;
	}
	j = DSE(trig->y, trig->m, trig->d);
        ADVANCE_TO_WD(j, trig->wd);
	return j;

    default:
	Eprint("NextSimpleTrig %s %d", ErrMsg[E_SWERR], typ);
	*err = E_SWERR;
	return -1;
    }
}

/***************************************************************/
/*                                                             */
/*  DSEMonth - Given a DSE date, what's the month?             */
/*                                                             */
/***************************************************************/
static int DSEMonth(int dse)
{
    int y, m, d;
    FromDSE(dse, &y, &m, &d);
    return m;
}

/***************************************************************/
/*                                                             */
/*  DSEYear - Given a DSE date, what's the year?               */
/*                                                             */
/***************************************************************/
static int DSEYear(int dse)
{
    int y, m, d;
    FromDSE(dse, &y, &m, &d);
    return y;
}

/***************************************************************/
/*                                                             */
/*  GetNextTriggerDate                                         */
/*                                                             */
/*  Given a trigger, compute the next trigger date.            */
/*                                                             */
/*  Returns the DSE date of next trigger, -1 if                */
/*  expired, -2 if can't compute trigger date.                 */
/*                                                             */
/***************************************************************/
static int GetNextTriggerDate(Trigger *trig, int start, int *err, int *nextstart)
{
    int simple, mod, omit;

/* First:  Have we passed the UNTIL date? */
    if (trig->until != NO_UNTIL &&
	trig->until < start) {
	trig->expired = 1;
	return -1; /* expired */
    }

/* Next: If it's an "AFTER"-type skip, back up
   until we're at the start of a block of holidays */
    if (trig->skip == AFTER_SKIP) {
	int iter = 0;
	while (iter++ <= MaxSatIter) {
	    *err = IsOmitted(start-1, trig->localomit, trig->omitfunc, &omit);
	    if (*err) return -2;
	    if (!omit) {
		break;
	    }
	    start--;
            if (start < 0) {
                break;
            }
	}
	if (start < 0 || iter > MaxSatIter) {
	    /* omitfunc must have returned "true" too often */
	    *err = E_CANT_TRIG;
	    return -2;
	}
    }

/* Find the next simple trigger */
    simple = NextSimpleTrig(start, trig, err);

/* Problems? */
    if (*err || (simple == -1)) return -1;

/* Suggested starting point for next attempt */
    *nextstart = simple+1;

/* If there's a BACK, back up... */
    if (trig->back != NO_BACK) {
	mod = trig->back;
	if (mod < 0) {
	    simple += mod;
	}
	else {
	    int iter = 0;
	    int max = MaxSatIter;
	    if (max < mod*2) {
		max = mod*2;
	    }
	    while(iter++ <= max) {
		if (!mod) {
		    break;
		}
		simple--;
		*err = IsOmitted(simple, trig->localomit, trig->omitfunc, &omit);
		if (*err) return -2;
		if (!omit) mod--;
	    }
	    if (iter > max) {
	        *err = E_CANT_TRIG;
		return -2;
	    }
	}
    }

/* If there's a REP, calculate the next occurrence */
    if (trig->rep != NO_REP) {
	if (simple < start) {
	    mod = (start - simple) / trig->rep;
	    simple = simple + mod * trig->rep;
	    if (simple < start) simple += trig->rep;
	}
    }

/* If it's a "BEFORE"-type skip, back up */
    if (trig->skip == BEFORE_SKIP) {
	int iter = 0;
	while(iter++ <= MaxSatIter) {
	    *err = IsOmitted(simple, trig->localomit, trig->omitfunc, &omit);
	    if (*err) return -2;
	    if (!omit) {
		break;
	    }
	    simple--;
            if (simple < 0) {
                *err = E_CANT_TRIG;
                return -2;
            }
	}
	if (iter > MaxSatIter) {
	    *err = E_CANT_TRIG;
	    return -2;
	}
    }

/* If it's an "AFTER"-type skip, jump ahead */
    if (trig->skip == AFTER_SKIP) {
	int iter = 0;
	while (iter++ <= MaxSatIter) {
	    *err = IsOmitted(simple, trig->localomit, trig->omitfunc, &omit);
	    if (*err) return -2;
	    if (!omit) {
		break;
	    }
	    simple++;
	}
	if (iter > MaxSatIter) {
	    *err = E_CANT_TRIG;
	    return -2;
	}
    }

/* Return the date */
    return simple;
}

int
AdjustTriggerForDuration(int today, int r, Trigger *trig, TimeTrig *tim, int save_in_globals)
{
    int y, m, d;
    /* If we have an AT, save the original event start */
    if (tim->ttime != NO_TIME) {
	trig->eventstart = MINUTES_PER_DAY * r + tim->ttime;
	if (tim->duration != NO_TIME) {
	    trig->eventduration = tim->duration;
	}
    }

    /* Now potentially adjust */
    if (r < today && r + trig->duration_days >= today) {
	/* Adjust duration down */
	tim->duration -= (today - r) * MINUTES_PER_DAY;
	tim->duration += tim->ttime;

	/* Start at midnight */
	tim->ttime = 0;

	/* Change trigger date to today */
	r = today;
	if (DebugFlag & DB_PRTTRIG) {
	    FromDSE(r, &y, &m, &d);
	    fprintf(ErrFp, "%s(%d): Trig(adj) = %s, %d %s, %d",
		    FileName, LineNo,
		    get_day_name(r % 7),
		    d,
		    get_month_name(m),
		    y);
	    if (tim->ttime != NO_TIME) {
		fprintf(ErrFp, " AT %02d:%02d",
			(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");
	}

    }
    if (save_in_globals) {
	SaveAllTriggerInfo(trig, tim, r, tim->ttime, 1);
    }
    return r;
}

/***************************************************************/
/*                                                             */
/*  ComputeTrigger                                             */
/*                                                             */
/*  The main function.  Compute the next trigger date given    */
/*  today's date.                                              */
/*                                                             */
/***************************************************************/
int ComputeTrigger(int today, Trigger *trig, TimeTrig *tim,
		   int *err, int save_in_globals)
{
    int r = ComputeTriggerNoAdjustDuration(today, trig, tim, err, save_in_globals, 0);
    if (*err != OK) {
	return r;
    }
    if (r == today) {
	if (tim->ttime != NO_TIME) {
	    trig->eventstart = MINUTES_PER_DAY * r + tim->ttime;
	    if (tim->duration != NO_TIME) {
		trig->eventduration = tim->duration;
	    }
	}
	if (save_in_globals) {
	    SaveAllTriggerInfo(trig, tim, r, tim->ttime, 1);
	}
	return r;
    }

    if (trig->duration_days) {
	r = ComputeTriggerNoAdjustDuration(today, trig, tim, err, save_in_globals, trig->duration_days);
	if (*err != OK) {
	    return r;
	}
    }
    r = AdjustTriggerForDuration(today, r, trig, tim, save_in_globals);
    return r;
}

/***************************************************************/
/*                                                             */
/*  ComputeTriggerNoAdjustDuration                             */
/*                                                             */
/*  Compute a trigger, but do NOT adjust the time trigger      */
/*  duration.                                                  */
/*                                                             */
/***************************************************************/
int ComputeTriggerNoAdjustDuration(int today, Trigger *trig, TimeTrig *tim,
				   int *err, int save_in_globals, int duration_days)
{
    int nattempts = 0,
	start = today - duration_days,
	nextstart = 0,
	y, m, d, omit,
	result;

    trig->expired = 0;
    if (save_in_globals) {
	LastTrigValid = 0;
    }

    /* Assume everything works */
    *err = OK;

    /* But check for obvious problems... */
    if ((WeekdayOmits | trig->localomit) == 0x7F) {
	*err = E_2MANY_LOCALOMIT;
	return -1;
    }

    if (start < 0) {
	*err = E_DATE_OVER;
	return -1;
    }

    if (tim->duration != NO_TIME && tim->ttime == NO_TIME) {
	*err = E_DURATION_NO_AT;
	return -1;
    }

    if (trig->rep != NO_REP &&
	(trig->d == NO_DAY ||
	 trig->m == NO_MON ||
	 trig->y == NO_YR)) {
	Eprint("%s", ErrMsg[E_REP_FULSPEC]);
	*err = E_REP_FULSPEC;
	return -1;
    }


    /* Save the trigger */
    if (save_in_globals) {
	SaveLastTrigger(trig);
    }

    while (nattempts++ < TRIG_ATTEMPTS) {
	result = GetNextTriggerDate(trig, start, err, &nextstart);

	/* If there's an error, die immediately */
	if (*err) return -1;
	if (result == -1) {
	    trig->expired = 1;
	    if (DebugFlag & DB_PRTTRIG) {
		fprintf(ErrFp, "%s(%d): %s\n",
			FileName, LineNo, ErrMsg[E_EXPIRED]);
	    }
	    return -1;
	}

	/* If result is >= today, great! */
	if (trig->skip == SKIP_SKIP) {
	    *err = IsOmitted(result, trig->localomit, trig->omitfunc, &omit);
	    if (*err) return -1;
	} else {
	    omit = 0;
	}

	/** FIXME: Fix bad interaction with SATISFY... need to rethink!!! */
	if (result+duration_days >= today &&
	    (trig->skip != SKIP_SKIP || !omit)) {
	    if (save_in_globals) {
		LastTriggerDate = result;  /* Save in global var */
		LastTrigValid = 1;
	    }
	    if (DebugFlag & DB_PRTTRIG) {
		FromDSE(result, &y, &m, &d);
		fprintf(ErrFp, "%s(%d): Trig = %s, %d %s, %d",
			FileName, LineNo,
			get_day_name(result % 7),
			d,
			get_month_name(m),
			y);
		if (tim->ttime != NO_TIME) {
		    fprintf(ErrFp, " AT %02d:%02d",
			    (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 result;
	}

	/* If it's a simple trigger, no point in rescanning */
	if (trig->back == NO_BACK &&
	    trig->skip == NO_SKIP &&
	    trig->rep == NO_REP) {
	    trig->expired = 1;
	    if (DebugFlag & DB_PRTTRIG) {
		fprintf(ErrFp, "%s(%d): %s\n",
			FileName, LineNo, ErrMsg[E_EXPIRED]);
	    }
            if (save_in_globals) {
                LastTriggerDate = result;
                LastTrigValid = 1;
            }
	    return -1;
	}

	if (trig->skip == SKIP_SKIP &&
	    omit &&
	    nextstart <= start &&
	    result >= start) {
	    nextstart = result + 1;
	}

	/* Keep scanning... unless there's no point in doing it.*/
	if (nextstart <= start) {
            if (save_in_globals) {
                LastTriggerDate = result;
                LastTrigValid = 1;
            }
	    trig->expired = 1;
	    if (DebugFlag & DB_PRTTRIG) {
		fprintf(ErrFp, "%s(%d): %s\n",
			FileName, LineNo, ErrMsg[E_EXPIRED]);
	    }
	    return -1;
	}
	else start = nextstart;

    }

    /* We failed - too many attempts or trigger has expired*/
    *err = E_CANT_TRIG;
    return -1;
}

