/*
 * DisMan Schedule MIB:
 *     Core implementation of the schedule handling behaviour
 */

#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-features.h>
#include <net-snmp/net-snmp-includes.h>
#include <net-snmp/agent/net-snmp-agent-includes.h>
#include "disman/schedule/schedCore.h"
#include "utilities/iquery.h"

netsnmp_feature_require(iquery);

netsnmp_feature_child_of(sched_nextrowtime, netsnmp_unused);

netsnmp_tdata *schedule_table;


#if !defined(HAVE_LOCALTIME_R) && !defined(localtime_r)
/*
 * localtime_r() replacement for older MinGW versions.
 * Note: this implementation is not thread-safe, while it should.
 */
struct tm      *
localtime_r(const time_t * timer, struct tm *result)
{
    struct tm      *result_p;

    result_p = localtime(timer);
    if (result && result_p)
        *result = *result_p;
    return result_p;
}
#endif


    /*
     * Initialize the container for the schedule table,
     * regardless of which initialisation routine is called first.
     */
void
init_schedule_container(void)
{
    DEBUGMSGTL(("disman:schedule:init", "init schedule container\n"));
    if (!schedule_table) {
        schedule_table = netsnmp_tdata_create_table("schedTable", 0);
        DEBUGMSGTL(("disman:schedule:init",
                        "create schedule container(%p)\n", schedule_table));
    }
}

/** Initializes the schedCore module */
void
init_schedCore(void)
{
    /*
     * Create a table structure for the schedule table
     * This will typically be registered by the schedTable module
     */
    DEBUGMSGTL(("disman:schedule:init", "Initializing core module\n"));
    init_schedule_container();
}


/*
 * Callback to invoke a scheduled action
 */
static void
_sched_callback( unsigned int reg, void *magic )
{
    struct schedTable_entry *entry = (struct schedTable_entry *)magic;
    int ret;
    netsnmp_variable_list assign;

    if ( !entry ) {
        DEBUGMSGTL(("disman:schedule:callback", "missing entry\n"));
        return;
    }
    entry->schedLastRun = time(NULL);
    entry->schedTriggers++;

    DEBUGMSGTL(( "disman:schedule:callback", "assignment "));
    DEBUGMSGOID(("disman:schedule:callback", entry->schedVariable,
                                             entry->schedVariable_len));
    DEBUGMSG((   "disman:schedule:callback", " = %ld\n", entry->schedValue));

    memset(&assign, 0, sizeof(netsnmp_variable_list));
    snmp_set_var_objid(&assign, entry->schedVariable, entry->schedVariable_len);
    snmp_set_var_typed_value(&assign, ASN_INTEGER,
                                     (u_char *)&entry->schedValue,
                                         sizeof(entry->schedValue));

    ret = netsnmp_query_set( &assign, entry->session );
    if ( ret != SNMP_ERR_NOERROR ) {
        DEBUGMSGTL(( "disman:schedule:callback",
                     "assignment failed (%d)\n", ret));
        entry->schedFailures++;
        entry->schedLastFailure = ret;
        time ( &entry->schedLastFailed );
    }

    sched_nextTime( entry );
}


    /*
     * Internal utility routines to help interpret
     *  calendar-based schedule bit strings
     */
static u_char _masks[] = { /* 0xff, */ 0x7f, 0x3f, 0x1f,
                           0x0f, 0x07, 0x03, 0x01, 0x00 };
static u_char _bits[]  = { 0x80, 0x40, 0x20, 0x10,
                           0x08, 0x04, 0x02, 0x01 };

/*
 * Are any of the bits set?
 */
static int
_bit_allClear(const u_char *pattern, int len) {
    int i;

    for (i=0; i<len; i++) {
        if ( pattern[i] != 0 )
            return 0;    /* At least one bit set */
    }
    return 1;  /* All bits clear */ 
}

/*
 * Is a particular bit set?
 */
static int
_bit_set(const u_char *pattern, int bit) {
    int major, minor;

    major = bit/8;
    minor = bit%8;
    if ( pattern[major] & _bits[minor] ) {
        return 1; /* Specified bit is set */
    }
    return 0;     /* Bit not set */
}

/*
 * What is the next bit set?
 *   (after a specified point)
 */
static int
_bit_next(const u_char *pattern, int current, size_t len) {
    char buf[ 8 ];
    int major, minor, i, j;

        /* Make a working copy of the bit pattern */
    memset( buf, 0, 8 );
    memcpy( buf, pattern, len );

        /*
         * If we're looking for the first bit after some point,
         * then clear all earlier bits from the working copy.
         */
    if ( current > -1 ) {
        major = current/8;
        minor = current%8;
        for ( i=0; i<major; i++ )
            buf[i]=0;
        buf[major] &= _masks[minor];
    }

        /*
         * Look for the first bit that's set
         */
    for ( i=0; i<(int)len; i++ ) {
        if ( buf[i] != 0 ) {
            major = i*8;
            for ( j=0; j<8; j++ ) {
                if ( buf[i] & _bits[j] ) {
                    return major+j;
                }
            }
        }
    }
    return -1;     /* No next bit */
}


static int _daysPerMonth[] = { 31, 28, 31, 30,
                               31, 30, 31, 31,
                               30, 31, 30, 31, 29 };

static u_char _truncate[] = { 0xfe, 0xf0, 0xfe, 0xfc,
                              0xfe, 0xfc, 0xfe, 0xfe,
                              0xfc, 0xfe, 0xfc, 0xfe, 0xf8 };

/*
 * What is the next day with a relevant bit set?
 *
 * Merge the forward and reverse day bits into a single
 *   pattern relevant for this particular month,
 *   and apply the standard _bit_next() call.
 * Then check this result against the day of the week bits.
 */
static int
_bit_next_day(const u_char *day_pattern, const u_char *weekday_pattern,
               int day, int month, int year ) {
    u_char buf[4];
    union {
        char buf2[4];
        int int_val;
    } rev;
    int next_day, i;
    struct tm tm_val;

        /* Make a working copy of the forward day bits ... */
    memset( buf,  0, 4 );
    memcpy( buf,  day_pattern, 4 );

        /* ... and another (right-aligned) of the reverse day bits */
    memset( rev.buf2, 0, 4 );
    memcpy( rev.buf2, day_pattern+4, 4 );
    rev.int_val >>= 2;
    if ( buf[3] & 0x01 )
        rev.buf2[0] |= 0x40;
    if ( month == 3 || month == 5 ||
         month == 8 || month == 10 )
        rev.int_val >>= 1;  /* April, June, September, November */
    if ( month == 1 )
        rev.int_val >>= 3;  /* February */
    if ( month == 12 )
        rev.int_val >>= 2;  /* February (leap year) */

        /* Combine the two bit patterns, and truncate to the month end */
    for ( i=0; i<4; i++ ) {
        if ( rev.buf2[i] & 0x80 ) buf[3-i] |= 0x01;
        if ( rev.buf2[i] & 0x40 ) buf[3-i] |= 0x02;
        if ( rev.buf2[i] & 0x20 ) buf[3-i] |= 0x04;
        if ( rev.buf2[i] & 0x10 ) buf[3-i] |= 0x08;
        if ( rev.buf2[i] & 0x08 ) buf[3-i] |= 0x10;
        if ( rev.buf2[i] & 0x04 ) buf[3-i] |= 0x20;
        if ( rev.buf2[i] & 0x02 ) buf[3-i] |= 0x40;
        if ( rev.buf2[i] & 0x01 ) buf[3-i] |= 0x80;
    }

    buf[3] &= _truncate[ month ];

    next_day = day-1;  /* tm_day is 1-based, not 0-based */
    do {
        next_day = _bit_next( buf, next_day, 4 );
        if ( next_day < 0 )
            return -1;

            /*
             * Calculate the day of the week, and
             * check this against the weekday pattern
             */
        memset( &tm_val,  0, sizeof(struct tm));
        tm_val.tm_mday = next_day+1;
        tm_val.tm_mon  = month;
        tm_val.tm_year = year;
        if (mktime(&tm_val) == (time_t)-1)
            return -1;
    } while ( !_bit_set( weekday_pattern, tm_val.tm_wday ));
    return next_day+1; /* Convert back to 1-based list */
}


/*
 * determine the time for the next scheduled action of a given entry
 */
void
sched_nextTime( struct schedTable_entry *entry )
{
    time_t now;
    struct tm now_tm, next_tm;
    int rev_day, mon;

    time( &now );

    if ( !entry ) {
        DEBUGMSGTL(("disman:schedule:time", "missing entry\n"));
        return;
    }

    if ( entry->schedCallbackID )
        snmp_alarm_unregister( entry->schedCallbackID );

    if (!(entry->flags & SCHEDULE_FLAG_ENABLED) ||
        !(entry->flags & SCHEDULE_FLAG_ACTIVE))  {
        DEBUGMSGTL(("disman:schedule:time", "inactive entry\n"));
        return;
    }

    switch ( entry->schedType ) {
    case SCHED_TYPE_PERIODIC:
        if ( !entry->schedInterval ) {
            DEBUGMSGTL(("disman:schedule:time", "periodic: no interval\n"));
            return;
        }
        if ( entry->schedLastRun ) {
             entry->schedNextRun = entry->schedLastRun +
                                   entry->schedInterval;
        } else {
             entry->schedNextRun = now + entry->schedInterval;
        }
        DEBUGMSGTL(("disman:schedule:time", "periodic: (%ld) %s",
                    (long) entry->schedNextRun, ctime(&entry->schedNextRun)));
        break;

    case SCHED_TYPE_ONESHOT:
        if ( entry->schedLastRun ) {
            DEBUGMSGTL(("disman:schedule:time", "one-shot: expired (%ld) %s",
                        (long) entry->schedNextRun,
                        ctime(&entry->schedNextRun)));
            return;
        }
        DEBUGMSGTL(("disman:schedule:time", "one-shot: fallthrough\n"));
        /* FALL THROUGH */
    case SCHED_TYPE_CALENDAR:
        /*
         *  Check for complete time specification
         *  If any of the five fields have no bits set,
         *    the entry can't possibly match any time.
         */
        if ( _bit_allClear( entry->schedMinute, 8 ) ||
             _bit_allClear( entry->schedHour,   3 ) ||
             _bit_allClear( entry->schedDay,  4+4 ) ||
             _bit_allClear( entry->schedMonth,  2 ) ||
             _bit_allClear( entry->schedWeekDay, 1 )) {
            DEBUGMSGTL(("disman:schedule:time", "calendar: incomplete spec\n"));
            return;
        }

        /*
         *  Calculate the next run time:
         *
         *  If the current Month, Day & Hour bits are set
         *    calculate the next specified minute
         *  If this fails (or the current Hour bit is not set)
         *    use the first specified minute,
         *    and calculate the next specified hour
         *  If this fails (or the current Day bit is not set)
         *    use the first specified minute and hour
         *    and calculate the next specified day (in this month)
         *  If this fails (or the current Month bit is not set)
         *    use the first specified minute and hour
         *    calculate the next specified month, and
         *    the first specified day (in that month)
         */

        (void) localtime_r( &now, &now_tm );
        (void) localtime_r( &now, &next_tm );

        next_tm.tm_mon=-1;
        next_tm.tm_mday=-1;
        next_tm.tm_hour=-1;
        next_tm.tm_min=-1;
        next_tm.tm_sec=0;
        if ( _bit_set( entry->schedMonth, now_tm.tm_mon )) {
            next_tm.tm_mon = now_tm.tm_mon;
            rev_day = _daysPerMonth[ now_tm.tm_mon ] - now_tm.tm_mday;
            if ( _bit_set( entry->schedWeekDay, now_tm.tm_wday ) &&
                (_bit_set( entry->schedDay, now_tm.tm_mday-1 ) ||
                 _bit_set( entry->schedDay, 31+rev_day ))) {
                next_tm.tm_mday = now_tm.tm_mday;

                if ( _bit_set( entry->schedHour, now_tm.tm_hour )) {
                    next_tm.tm_hour = now_tm.tm_hour;
                    /* XXX - Check Fall timechange */
                    next_tm.tm_min = _bit_next( entry->schedMinute,
                                                  now_tm.tm_min, 8 );
                } else {
                    next_tm.tm_min = -1;
                }
   
                if ( next_tm.tm_min == -1 ) {
                    next_tm.tm_min  = _bit_next( entry->schedMinute, -1, 8 );
                    next_tm.tm_hour = _bit_next( entry->schedHour,
                                                   now_tm.tm_hour, 3 );
                }
            } else {
                next_tm.tm_hour = -1;
            }

            if ( next_tm.tm_hour == -1 ) {
                next_tm.tm_min  = _bit_next( entry->schedMinute, -1, 8 );
                next_tm.tm_hour = _bit_next( entry->schedHour,   -1, 3 );
                    /* Handle leap years */
                mon = now_tm.tm_mon;
                if ( mon == 1 && (now_tm.tm_year%4 == 0) )
                    mon = 12;
                next_tm.tm_mday = _bit_next_day( entry->schedDay,
                                                 entry->schedWeekDay,
                                                 now_tm.tm_mday,
                                                 mon, now_tm.tm_year );
            }
        } else {
            next_tm.tm_min  = _bit_next( entry->schedMinute, -1, 2 );
            next_tm.tm_hour = _bit_next( entry->schedHour,   -1, 3 );
            next_tm.tm_mday = -1;
            next_tm.tm_mon  = now_tm.tm_mon;
        }

        while ( next_tm.tm_mday == -1 ) {
            next_tm.tm_mon  = _bit_next( entry->schedMonth,
                                          next_tm.tm_mon, 2 );
            if ( next_tm.tm_mon == -1 ) {
                next_tm.tm_year++;
                next_tm.tm_mon  = _bit_next( entry->schedMonth,
                                             -1, 2 );
            }
                /* Handle leap years */
            mon = next_tm.tm_mon;
            if ( mon == 1 && (next_tm.tm_year%4 == 0) )
                mon = 12;
            next_tm.tm_mday = _bit_next_day( entry->schedDay,
                                             entry->schedWeekDay,
                                             -1, mon, next_tm.tm_year );
            /* XXX - catch infinite loop */
        }

        /* XXX - Check for Spring timechange */

        /*
         * 'next_tm' now contains the time for the next scheduled run
         */
        entry->schedNextRun = mktime( &next_tm );
        DEBUGMSGTL(("disman:schedule:time", "calendar: (%ld) %s",
                    (long) entry->schedNextRun, ctime(&entry->schedNextRun)));
        return;

    default:
        DEBUGMSGTL(("disman:schedule:time", "unknown type (%ld)\n",
                                             entry->schedType));
        return;
    }
    entry->schedCallbackID = snmp_alarm_register(
                                entry->schedNextRun - now,
                                0, _sched_callback, entry );
    return;
}

#ifndef NETSNMP_FEATURE_REMOVE_SCHED_NEXTROWTIME
void
sched_nextRowTime( netsnmp_tdata_row *row )
{
    sched_nextTime((struct schedTable_entry *) row->data );
}
#endif /* NETSNMP_FEATURE_REMOVE_SCHED_NEXTROWTIME */

/*
 * create a new row in the table 
 */
netsnmp_tdata_row *
schedTable_createEntry(const char *schedOwner, const char *schedName)
{
    struct schedTable_entry *entry;
    netsnmp_tdata_row *row;
    int len;

    DEBUGMSGTL(("disman:schedule:entry", "creating entry (%s, %s)\n",
                                          schedOwner, schedName));
    entry = SNMP_MALLOC_TYPEDEF(struct schedTable_entry);
    if (!entry)
        return NULL;

    row = netsnmp_tdata_create_row();
    if (!row) {
        SNMP_FREE(entry);
        return NULL;
    }
    row->data = entry;
    /*
     * Set the indexing for this entry, both in the row
     *  data structure, and in the table_data helper.
     */
    if (schedOwner) {
        len = strlen(schedOwner);
        if (len > sizeof(entry->schedOwner))
            len = sizeof(entry->schedOwner);
        memcpy(entry->schedOwner, schedOwner, len);
        netsnmp_tdata_row_add_index(row, ASN_OCTET_STR, entry->schedOwner, len);
    }
    else
        netsnmp_tdata_row_add_index(row, ASN_OCTET_STR, "", 0 );

    len = strlen(schedName);
    if (len > sizeof(entry->schedName))
        len = sizeof(entry->schedName);
    memcpy(entry->schedName, schedName, len);
    netsnmp_tdata_row_add_index(row, ASN_OCTET_STR, entry->schedName, len);
    /*
     * Set the (non-zero) default values in the row data structure.
     */
    entry->schedType         = SCHED_TYPE_PERIODIC;
    entry->schedVariable_len = 2;   /* .0.0 */

    netsnmp_tdata_add_row(schedule_table, row);
    return row;
}


/*
 * remove a row from the table 
 */
void
schedTable_removeEntry(netsnmp_tdata_row *row)
{
    struct schedTable_entry *entry;

    if (!row || !row->data) {
        DEBUGMSGTL(("disman:schedule:entry", "remove: missing entry\n"));
        return;                 /* Nothing to remove */
    }
    entry = (struct schedTable_entry *)
        netsnmp_tdata_remove_and_delete_row(schedule_table, row);
    if (entry) {
        DEBUGMSGTL(("disman:schedule:entry", "remove entry (%s, %s)\n",
                                     entry->schedOwner, entry->schedName));
        SNMP_FREE(entry);
    }
}
