/***************************************************************/
/*                                                             */
/*  TOKEN.C                                                    */
/*                                                             */
/*  Contains routines for parsing the reminder file and        */
/*  classifying the tokens parsed.                             */
/*                                                             */
/*  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 <string.h>
#include <ctype.h>

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

/* The macro PARSENUM parses a char pointer as an integer.  It simply
   executes 'return' if an initial non-numeric char is found. */
#define PARSENUM(var, string) \
if (!isdigit(*(string))) return; \
var = 0; \
while (isdigit(*(string))) { \
    var *= 10; \
    var += *(string) - '0'; \
    string++; \
}

/* The big array holding all recognized (literal) tokens in reminder file.
   Keep this array sorted, or software will not work. */
Token TokArray[] = {
    /* NAME          MINLEN      TYPE           VALUE */
    { "addomit",        7,      T_AddOmit,      0 },
    { "after",		5,	T_Skip,	AFTER_SKIP },
    { "april",		3,	T_Month,	3 },
    { "at",		2,	T_At,		0 },
    { "august",		3,	T_Month,	7 },
    { "banner",		3,	T_Banner,	0 },
    { "before",	        6,	T_Skip,	        BEFORE_SKIP },
    { "cal",		3,	T_RemType,	CAL_TYPE },
    { "clear-omit-context", 5,   T_Clr,         0 },
    { "debug",          5,      T_Debug,        0 },
    { "december",	3,	T_Month,       11 },
    { "do",     	2,	T_IncludeR,	0 },
    { "dumpvars",       4,      T_Dumpvars,     0 },
    { "duration",       8,      T_Duration,     0 },
    { "else",		4,	T_Else,	        0 },
    { "endif",		5,	T_EndIf,	0 },
    { "errmsg",         6,      T_ErrMsg,       0 },
    { "exit",		4,	T_Exit,		0 },
    { "expr",           4,      T_Expr,         0 },
    { "february",	3,	T_Month,	1 },
    { "first",          5,      T_Ordinal,      0 },
    { "flush",		5,	T_Flush,	0 },
    { "fourth",         6,      T_Ordinal,      3 },
    { "friday",		3,	T_WkDay,	4 },
    { "from",		4,	T_Scanfrom,	FROM_TYPE },
    { "fset",		4,	T_Fset,		0 },
    { "funset",         6,      T_Funset,       0 },
    { "if",		2,	T_If,		0 },
    { "iftrig",		6,	T_IfTrig,	0 },
    { "in",             2,      T_In,           0 },
    { "include",	3,	T_Include,	0 },
    { "includecmd",     10,     T_IncludeCmd,   0 },
    { "january",	3,	T_Month,	0 },
    { "july",		3,	T_Month,	6 },
    { "june",		3,	T_Month,	5 },
    { "last",           4,      T_Ordinal,     -1 },
    { "lastday",        7,      T_BackAdj,     -1 },
    { "lastworkday",    11,     T_BackAdj,      1 },
    { "march",		3,	T_Month,	2 },
    { "may",		3,	T_Month,	4 },
    { "maybe-uncomputable", 5,  T_MaybeUncomputable, 0},
    { "monday",		3,	T_WkDay,	0 },
    { "msf",		3,	T_RemType,	MSF_TYPE },
    { "msg",		3,	T_RemType,	MSG_TYPE },
    { "noqueue",        7,      T_NoQueue,      0 },
    { "november",	3,	T_Month,	10 },
    { "october",	3,	T_Month,	9 },
    { "omit",		4,	T_Omit,		0 },
    { "omitfunc",       8,      T_OmitFunc,     0 },
    { "once",		4,	T_Once,		0 },
    { "pop-omit-context", 3,	T_Pop,		0 },
    { "preserve",       8,      T_Preserve,     0 },
    { "priority",	8,	T_Priority,	0 },
    { "ps",		2,	T_RemType,	PS_TYPE },
    { "psfile",	6,	T_RemType,	PSF_TYPE },
    { "push-omit-context", 4,	T_Push,		0 },
    { "rem",		3,	T_Rem,		0 },
    { "run",		3,	T_RemType,	RUN_TYPE },
    { "satisfy",	7,	T_RemType,      SAT_TYPE },
    { "saturday",	3,	T_WkDay,	5 },
    { "scanfrom",	4,	T_Scanfrom,	SCANFROM_TYPE },
    { "sched",		5,	T_Sched,	0 },
    { "second",         6,      T_Ordinal,      1 },
    { "september",	3,	T_Month,	8 },
    { "set",		3,	T_Set,		0 },
    { "skip",		4,	T_Skip,	SKIP_SKIP },
    { "special",        7,      T_RemType,      PASSTHRU_TYPE },
    { "sunday",		3,	T_WkDay,	6 },
    { "tag",            3,      T_Tag,          0 },
    { "third",          5,      T_Ordinal,      2 },
    { "through",        7,      T_Through,      0 },
    { "thursday",	3,	T_WkDay,	3 },
    { "tuesday",	3,	T_WkDay,	1 },
    { "unset",		5,	T_UnSet,	0 },
    { "until",		5,	T_Until,	0 },
    { "warn",           4,      T_Warn,         0 },
    { "wednesday",	3,	T_WkDay,	2 }
};

static int TokStrCmp (Token const *t, char const *s);

static void
init_token(Token *t)
{
    t->name = NULL;
    t->type = T_Illegal;
    t->val = 0;
}

static void
token_error(Token *t, int errcode)
{
    t->type = T_Illegal;
    t->val = -errcode;
}

/***************************************************************/
/*                                                             */
/*  FindInitialToken                                           */
/*                                                             */
/*  Find the initial token on the command line.  If it's a     */
/*  left square bracket, return a T_Illegal type.              */
/*                                                             */
/***************************************************************/
char const *FindInitialToken(Token *tok, char const *s)
{
    DynamicBuffer buf;
    DBufInit(&buf);

    init_token(tok);

    while (isempty(*s)) s++;

    while (*s && !isempty(*s)) {
	if (DBufPutc(&buf, *s++) != OK) return s;
    }

    FindToken(DBufValue(&buf), tok);
    DBufFree(&buf);

    return s;
}


/***************************************************************/
/*                                                             */
/*  FindToken                                                  */
/*                                                             */
/*  Given a string, which token is it?                         */
/*                                                             */
/***************************************************************/
void FindToken(char const *s, Token *tok)
{
    int top, bot, mid, r, max;
    int l;

    init_token(tok);
    if (! *s) {
	tok->type = T_Empty;
	return;
    }

    if (*s == '#' || *s == ';') {
	tok->type = T_Comment;
	return;
    }

    /* Quickly give up the search if first char not a letter */
    if ( ! isalpha(*s)) {
	FindNumericToken(s, tok);
	return;
    }

    l = strlen(s);

    /* Ignore trailing commas */
    if (l > 0 && s[l-1] == ',') {
	l--;
    }
    bot = 0;
    top = sizeof(TokArray) / sizeof(TokArray[0]) - 1;
    max = sizeof(TokArray) / sizeof(TokArray[0]);

    while(top >= bot) {
	mid = (top + bot) / 2;
	r = TokStrCmp(&TokArray[mid], s);
	if (!r) {
	    if (l >= TokArray[mid].MinLen) {
		tok->type = TokArray[mid].type;
		tok->val  = TokArray[mid].val;
		return;
	    } else {
		while (mid && !TokStrCmp(&TokArray[mid-1],s)) mid--;
		while (mid < max &&
		       !TokStrCmp(&TokArray[mid], s) &&
		       l < TokArray[mid].MinLen) {
		    mid++;
		}
		if (mid < max &&
		    !TokStrCmp(&TokArray[mid], s)) {
		    tok->type = TokArray[mid].type;
		    tok->val = TokArray[mid].val;
		    return;
		}
	    }
	    break;
	}
	if (r > 0) top = mid-1; else bot=mid+1;
    }

    return;
}

/***************************************************************/
/*                                                             */
/*  FindNumericToken                                           */
/*                                                             */
/*  Parse a numeric token:                                     */
/*  Year - number between 1990 and 2085, or 90-99.             */
/*  Day - number between 1 and 31                              */
/*  Delta - +[+]n                                              */
/*  Back - -[-]n                                               */
/*  Rep - *n                                                   */
/*                                                             */
/***************************************************************/
void FindNumericToken(char const *s, Token *t)
{
    int mult = 1, hour, min;
    char const *s_orig = s;
    int ampm = 0;
    int r;

    init_token(t);
    if (isdigit(*s)) {
	PARSENUM(t->val, s);

	/* If we hit a '-' or a '/', we may have a date or a datetime */
	if (*s == '-' || *s == '/') {
	    char const *p = s_orig;
	    int dse, tim;
            r = ParseLiteralDateOrTime(&p, &dse, &tim);
	    if (r == OK) {
		if (*p) {
                    if (tim == NO_TIME) {
                        t->val = -E_BAD_DATE;
                    } else {
                        t->val = -E_BAD_TIME;
                    }
                    return;
                }
		if (tim == NO_TIME) {
		    t->type = T_Date;
		    t->val = dse;
		    return;
		}
                if (dse == NO_DATE) {
                    t->type = T_Time;
                    t->val = tim;
                    return;
                }
		t->type = T_DateTime;
		t->val = MINUTES_PER_DAY * dse + tim;
	    } else {
                token_error(t, r);
            }
	    return;
	}

	/* If we hit a comma, swallow it.  This allows stuff
	   like Jan 6, 1998 */
	if (*s == ',' && *(s+1) == 0) {
	    /* Classify the number we've got */
	    if (t->val >= BASE && t->val <= BASE+YR_RANGE) t->type = T_Year;
	    else if (t->val >= 1 && t->val <= 31) t->type = T_Day;
	    else t->type = T_Number;
	    return;
	}
	/* If we hit a colon or a period, we've probably got a time hr:min */
	if (*s == ':' || *s == '.' || *s == TimeSep) {
	    s++;
	    hour = t->val;
            if (!isdigit(*s)) {
                token_error(t, E_BAD_TIME);
                return;
            }
	    PARSENUM(min, s);
	    if (min > 59) {
                /* Illegal time */
                token_error(t, E_BAD_TIME);
                return;
            }
	    /* Check for p[m] or a[m] */
	    if (*s == 'A' || *s == 'a' || *s == 'P' || *s == 'p') {
		ampm = tolower(*s);
		s++;
		if (*s == 'm' || *s == 'M') {
		    s++;
		}
	    }
	    if (*s) {
                token_error(t, E_BAD_TIME);
                return;
            }
	    if (ampm) {
		if (hour < 1 || hour > 12) {
                    token_error(t, E_BAD_TIME);
                    return;
                }
		if (ampm == 'a') {
		    if (hour == 12) {
			hour = 0;
		    }
		} else if (ampm == 'p') {
		    if (hour < 12) {
			hour += 12;
		    }
		}
	    }

	    t->val = hour*60 + min;  /* Convert to minutes past midnight */
	    if (hour <= 23) {
		t->type = T_Time;
	    } else {
		t->type = T_LongTime;
	    }
	    return;
	}

	/* If we hit a non-digit, error! */
	if (*s) {
            token_error(t, E_BAD_NUMBER);
            return;
        }

	/* Classify the number we've got */
	if (t->val >= BASE && t->val <= BASE+YR_RANGE) t->type = T_Year;
	else if (t->val >= 1 && t->val <= 31) t->type = T_Day;
	else t->type = T_Number;
	return;
    }

    switch (*s) {
    case '*':
	s++;
	PARSENUM(t->val, s);
	if (*s) {
            /* Illegal token if followed by non-numeric char */
            token_error(t, E_BAD_NUMBER);
            return;
        }
	t->type = T_Rep;
	return;

    case '+':
	s++;
	if (*s == '+') { mult = -1; s++; }
	PARSENUM(t->val, s);
	if (*s) {
            /* Illegal token if followed by non-numeric char */
            token_error(t, E_BAD_NUMBER);
            return;
        }
	t->type = T_Delta;
	t->val *= mult;
	return;
    case '-':
	s++;
	if (*s == '-') { mult = -1; s++; }
	PARSENUM(t->val, s);
	if (*s) {
            /* Illegal token if followed by non-numeric char */
            token_error(t, E_BAD_NUMBER);
            return;
        }
	t->type = T_Back;
	t->val *= mult;
	return;

    case '~':
	s++;
	if (*s == '~') { mult = -1; s++; }
	PARSENUM(t->val, s);
	if (*s) {
            /* Illegal token if followed by non-numeric char */
            token_error(t, E_BAD_NUMBER);
            return;
        }
	t->type = T_BackAdj;
	t->val *= mult;
	return;

    default: return;
    }
}


/***************************************************************/
/*                                                             */
/*  TokStrCmp                                                  */
/*                                                             */
/*  Compare a token to a string.                               */
/*                                                             */
/***************************************************************/
static int TokStrCmp(Token const *t, char const *s)
{
    register int r;
    char const *tk = t->name;
    while(*tk && *s && !(*s == ',' && *(s+1) == 0)) {
        /* t->name is already lower-case */
	r = *tk - tolower(*s);
	tk++;
	s++;
	if (r) return r;
    }
    /* Ignore trailing commas on s */

    if (!*s || (*s == ',' && !*(s+1))) return 0;
    return (*tk - tolower(*s));
}

static void
print_token(Token *tok)
{
    if (tok->MinLen < (int) strlen(tok->name)) {
        printf("%.*s\n", tok->MinLen, tok->name);
    }
    printf("%s\n", tok->name);
}

void
print_remind_tokens(void)
{
    int i;
    Token *tok;
    int num = (int) (sizeof(TokArray) / sizeof(TokArray[0]));
    printf("# Remind Tokens\n\n");
    for (i=0; i<num; i++) {
        tok = &TokArray[i];
        if (tok->type != T_Month && tok->type != T_WkDay) {
            print_token(tok);
        }
    }

    printf("\n# Month Names\n\n");
    for (i=0; i<num; i++) {
        tok = &TokArray[i];
        if (tok->type == T_Month) {
            print_token(tok);
        }
    }

    printf("\n# Weekdays\n\n");
    for (i=0; i<num; i++) {
        tok = &TokArray[i];
        if (tok->type == T_WkDay) {
            print_token(tok);
        }
    }
}
