///
/// @file schedule.c
/// @authors k. Headley
/// @date 05 dec 2019

/// Cron-like schedule parsing, library and inspection

/// @sa doxygen-examples.c for more examples of Doxygen markup


/////////////////////////
// Terms of use
/////////////////////////
/*
 Copyright Information

 Copyright 2002-2019 MBARI
 Monterey Bay Aquarium Research Institute, all rights reserved.

 Terms of Use

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 3 of the License, or
 (at your option) any later version. You can access the GPLv3 license at
 http://www.gnu.org/licenses/gpl-3.0.html

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details
 (http://www.gnu.org/licenses/gpl-3.0.html)

 MBARI provides the documentation and software code "as is", with no warranty,
 express or implied, as to the software, title, non-infringement of third party
 rights, merchantability, or fitness for any particular purpose, the accuracy of
 the code, or the performance or results which you may obtain from its use. You
 assume the entire risk associated with use of the code, and you agree to be
 responsible for the entire cost of repair or servicing of the program with
 which you are using the code.

 In no event shall MBARI be liable for any damages, whether general, special,
 incidental or consequential damages, arising out of your use of the software,
 including, but not limited to, the loss or corruption of your data or damages
 of any kind resulting from use of the software, any prohibited use, or your
 inability to use the software. You agree to defend, indemnify and hold harmless
 MBARI and its officers, directors, and employees against any claim, loss,
 liability or expense, including attorneys' fees, resulting from loss of or
 damage to property or the injury to or death of any person arising out of the
 use of the software.

 The MBARI software is provided without obligation on the part of the
 Monterey Bay Aquarium Research Institute to assist in its use, correction,
 modification, or enhancement.

 MBARI assumes no responsibility or liability for any third party and/or
 commercial software required for the database or applications. Licensee agrees
 to obtain and maintain valid licenses for any additional third party software
 required.
 */

/////////////////////////
// Headers
/////////////////////////
#include <stdio.h>
#include <string.h>

#include "cwg-shared.h"
#include "rtc.h"
#include "timers.h"
#include "ioserial.h"
#include "iowfile.h"

#include "schedule.h"

/////////////////////////
// Macros
/////////////////////////

// These macros should only be defined for
// application main files rather than general C files
/*
/// @def PRODUCT
/// @brief header software product name
#define PRODUCT "carbonwg"

/// @def COPYRIGHT
/// @brief header software copyright info
#define COPYRIGHT "Copyright 2002-2019 MBARI Monterey Bay Aquarium Research Institute, all rights reserved."
/// @def NOWARRANTY
/// @brief header software terms of use
#define NOWARRANTY  \
"This program is distributed in the hope that it will be useful,\n"\
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"\
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"\
"GNU General Public License for more details (http://www.gnu.org/licenses/gpl-3.0.html)\n"
*/

#define WD_OFS 187
#define MO_OFS 175
#define MD_OFS 144
#define H_OFS  120
#define M_OFS  60
#define S_OFS  0

#define SCH_SPEC_FIELDS 6

#define MO_IMAX 11
#define MD_IMAX 30
#define WD_IMAX  6
#define H_IMAX  23
#define M_IMAX  59
#define S_IMAX  59

#define MO_BITS 12
#define MD_BITS 31
#define WD_BITS  7
#define H_BITS  24
#define M_BITS  60
#define S_BITS  60

#define MO_SPAN_0 1
#define MO_SPAN_1 12
#define MD_SPAN_0 1
#define MD_SPAN_1 31
#define H_SPAN_0  0
#define H_SPAN_1  23
#define M_SPAN_0  0
#define M_SPAN_1  59
#define S_SPAN_0  0
#define S_SPAN_1  59
#define WD_SPAN_0 0
#define WD_SPAN_1 6

#define SCH_BIT(ofs,x)  (((ofs + (x))%8))
#define SCH_BYTE(ofs,x) (((ofs + (x))/8))

#define MO_BIT(x)  (SCH_BIT(MO_OFS,x))
#define MO_BYTE(x) (SCH_BYTE(MO_OFS,x))
#define MD_BIT(x)  (SCH_BIT(MD_OFS,x))
#define MD_BYTE(x) (SCH_BYTE(MD_OFS,x))
#define WD_BIT(x)  (SCH_BIT(WD_OFS,x))
#define WD_BYTE(x) (SCH_BYTE(WD_OFS,x))
#define H_BIT(x)   (SCH_BIT(H_OFS,x))
#define H_BYTE(x)  (SCH_BYTE(H_OFS,x))
#define M_BIT(x)   (SCH_BIT(M_OFS,x))
#define M_BYTE(x)  (SCH_BYTE(M_OFS,x))
#define S_BIT(x)   (SCH_BIT(S_OFS,x))
#define S_BYTE(x)  (SCH_BYTE(S_OFS,x))

/////////////////////////
// Declarations
/////////////////////////
typedef int (* sch_idx_fn)(int x);

/////////////////////////
// Imports
/////////////////////////

/////////////////////////
// Module Global Variables
/////////////////////////
char *sch_wd_names[]={"SU","MO","TU","WE","TH","FR","SA"};
char *sch_mo_names[]={"JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"};

/////////////////////////
// Function Definitions
/////////////////////////

static int s_sch_mo_byte(int x){return MO_BYTE(x);}
static int s_sch_mo_bit(int x){return MO_BIT(x);}
static int s_sch_md_byte(int x){return MD_BYTE(x);}
static int s_sch_md_bit(int x){return MD_BIT(x);}
static int s_sch_h_byte(int x){return H_BYTE(x);}
static int s_sch_h_bit(int x){return H_BIT(x);}
static int s_sch_m_byte(int x){return M_BYTE(x);}
static int s_sch_m_bit(int x){return M_BIT(x);}
static int s_sch_s_byte(int x){return S_BYTE(x);}
static int s_sch_s_bit(int x){return S_BIT(x);}
static int s_sch_wd_byte(int x){return WD_BYTE(x);}
static int s_sch_wd_bit(int x){return WD_BIT(x);}

static int s_sched_parse_range(sched_range_t *dest, char *src)
{
    int retval=-1;

    if(NULL!=dest && NULL!=src){
        int test[3]={-1,-1,-1};
        char rmnem[2][4]={{0},{0}};

        // be careful ordering expressions
        // order matters for disambiguation
        if(strcmp(src,"-")==0){
            // none (reverse range prevents processing)
            dest->range[0]=dest->span[1];
            dest->range[1]=dest->span[0];
            retval=0;
        }else if(strcmp(src,"*")==0){
            // all
            dest->range[0]=dest->span[0];
            dest->range[1]=dest->span[1];
            retval=0;
        }else if(sscanf(src,"%d-%d/%u",&test[0],&test[1],&test[2])==3){
            // range w/ modulus
            if(SCH_CHK_RANGE(test[0],dest->span[0],dest->span[1]) &&
                    SCH_CHK_RANGE(test[2],dest->span[0],dest->span[1]) &&
                    SCH_CHK_RANGE(test[2],1,dest->span[1]) ){
                dest->range[0]=test[0];
                dest->range[1]=test[1];
                dest->range[2]=test[2];
                // modulus range offset (adjusted for zero/one indexed)
                dest->xo=dest->range[0]-dest->xz;
                retval=0;
            }
        }else if(sscanf(src,"%u-%u",&test[0],&test[1])==2){
            // range
            if( SCH_CHK_RANGE(test[0],dest->span[0],dest->span[1]) &&
                    SCH_CHK_RANGE(test[1],dest->span[0],dest->span[1]) ){
                dest->range[0]=test[0];
                dest->range[1]=test[1];
                retval=0;
            }// else range error
        }else if(sscanf(src,"%u",&test[0])==1){
            // value
            if(SCH_CHK_RANGE(test[0],dest->span[0],dest->span[1])){
                dest->range[0]=test[0];
                dest->range[1]=test[0];
                retval=0;
            }
        }else if(sscanf(src,"*/%u",&test[0])==1){
            // modulus
            if(SCH_CHK_RANGE(test[0],1,dest->span[1])){
                dest->range[2]=test[0];
                dest->range[0]=dest->span[0];
                dest->range[1]=dest->span[1];
                // modulus range offset (adjusted for zero/one indexed)
                // zero here, modulus is relative to start of range
                dest->xo=0;
                retval=0;
            }
        }else if(sscanf(src,"%3s-%3s/%u",rmnem[0],rmnem[1],&test[2])==3){
            int i=0;
            rmnem[0][3]='\0';
            rmnem[1][3]='\0';
            bool valid=false;
             // validate range names (compare to sch_wd_names)
            for(i=0;i<12;i++){
                if(strcmp(rmnem[0],sch_mo_names[i])==0)test[0]=i+1;
                if(strcmp(rmnem[1],sch_mo_names[i])==0)test[1]=i+1;
                if(test[0]>=0 && test[1]>=0){
                    valid=true;
                    break;
                }
            }
            // validate modulus
            valid=SCH_CHK_RANGE(test[2],1,dest->span[1]);

            if(valid){
                dest->range[0]=test[0];
                dest->range[1]=test[1];
                // modulus
                dest->range[2]=test[2];
                // modulus range offset (adjusted for zero/one indexed)
                dest->xo=dest->range[0]-dest->xz;;
                retval=0;
            }
        }else if(sscanf(src,"%2s-%2s/%u",rmnem[0],rmnem[1],&test[2])==3){
            int i=0;
            rmnem[0][2]='\0';
            rmnem[1][2]='\0';
            bool valid=false;
            // validate range names (compare to sch_wd_names)
            for(i=0;i<7;i++){
                if(strcmp(rmnem[0],sch_wd_names[i])==0)test[0]=i;
                if(strcmp(rmnem[1],sch_wd_names[i])==0)test[1]=i;
                if(test[0]>=0 && test[1]>=0){
                    valid=true;
                    break;
                }
            }
            // validate modulus
            valid=SCH_CHK_RANGE(test[2],1,dest->span[1]);

            if(valid){
                dest->range[0]=test[0];
                dest->range[1]=test[1];
                // modulus
                dest->range[2]=test[2];
                // modulus range offset (adjusted for zero/one indexed)
                dest->xo=dest->range[0]-dest->xz;;
                retval=0;
            }
        }else if(sscanf(src,"%3s-%3s",rmnem[0],rmnem[1])==2){
            int i=0;
            rmnem[0][3]='\0';
            rmnem[1][3]='\0';
            // validate range names (compare to sch_wd_names)
            // and get indices
            for(i=0;i<12;i++){
                if(strcmp(rmnem[0],sch_mo_names[i])==0)test[0]=i+1;
                if(strcmp(rmnem[1],sch_mo_names[i])==0)test[1]=i+1;
                if(test[0]>=0 && test[1]>=0){
                    dest->range[0]=test[0];
                    dest->range[1]=test[1];
                    retval=0;
                    break;
                }
            }
        }else if(sscanf(src,"%2s-%2s",rmnem[0],rmnem[1])==2){
            int i=0;
            rmnem[0][2]='\0';
            rmnem[1][2]='\0';
            // validate range names (compare to sch_wd_names)
            // and get indices
            for(i=0;i<7;i++){
                if(strcmp(rmnem[0],sch_wd_names[i])==0)test[0]=i;
                if(strcmp(rmnem[1],sch_wd_names[i])==0)test[1]=i;
                if(test[0]>=0 && test[1]>=0){
                    dest->range[0]=test[0];
                    dest->range[1]=test[1];
                    retval=0;
                    break;
                }
            }
        }else if(strlen(src)==3 && sscanf(src,"%3s",&rmnem[0][0])==1){
            int i=0;
            rmnem[0][3]='\0';
            // validate range names (compare to sch_mo_names)
            for(i=0;i<12;i++){
                if(strcmp(rmnem[0],sch_mo_names[i])==0)test[0]=i+1;
                if(test[0]>=0){
                    dest->range[0]=test[0];
                    dest->range[1]=test[0];
                    retval=0;
                    break;
                }
            }
        }else if(strlen(src)==2 && sscanf(src,"%2s",rmnem[0])==1){
            int i=0;
            rmnem[0][2]='\0';
            // validate range names (compare to sch_wd_names)
            for(i=0;i<7;i++){
                if(strcmp(rmnem[0],sch_wd_names[i])==0)test[0]=i;
                if(test[0]>=0){
                    dest->range[0]=test[0];
                    dest->range[1]=test[0];
                    retval=0;
                    break;
                }
            }
        }else {
            // invalid
            EPRINT("invalid month spec [%s]\r\n",src);
        }
    }

    return retval;
}

// parse schedule specifier
int sched_parse(schedule_t dest, char *spec)
{
    int retval=-1;

    if(NULL!=spec && NULL!=dest){
//        OUT_CTTY("parsing spec [%s]\r\n",spec);

        // start by parsing spec fields (space-delimited)
        // M[1-12] MD[1-31] H[0-23] M[0-59] S[0-59] WD[0-6]
        char *fields[SCH_SPEC_FIELDS]={0};
        int ntoks=tokenize(spec,fields," .",6);
        // field names (for debug output)
        char *fnames[SCH_SPEC_FIELDS]={"MO","MD","H","M","S","WD"};

        // these are parsing parameters
        // set for each field
        sch_idx_fn bit_fn=NULL;
        sch_idx_fn byte_fn=NULL;
        sched_range_t srange={{0},{0},0,0}, *ps=&srange;
        char *ftoks[32]={0};
        int fspan[2]={-1,-1};
        int fxz=-1;
        char *fname=NULL;
        bool err_flag=true;

        // iterate over fields, setting parsing parameters
        // then parsing (comma-delimited) range specifiers
        // *     - all
        // n     - single value
        // n-m   - values n:m inclusive
        // */n   - every nth
        // i-j/n - every nth value from i-j inclusive

        int i=0;
        retval=0;
        for(i=0;i<ntoks;i++){
            MPRINT(MOD_PRO,"Field tok [%d/%d] '%s'\r\n",i+1,ntoks,fields[i]);
            if(i==0){
                MPRINT(MOD_PRO," >> MONTH\r\n");
                // months
                fspan[0]=MO_SPAN_0;
                fspan[1]=MO_SPAN_1;
                fxz=1;
                bit_fn=s_sch_mo_bit;
                byte_fn=s_sch_mo_byte;
                fname=fnames[i];
                err_flag=false;
            }else if(i==1){
                MPRINT(MOD_PRO," >> MONTH DAY\r\n");
                // month days
                fspan[0]=MD_SPAN_0;
                fspan[1]=MD_SPAN_1;
                fxz=1;
                bit_fn=s_sch_md_bit;
                byte_fn=s_sch_md_byte;
                fname=fnames[i];
                err_flag=false;
           }else if(i==2){
                MPRINT(MOD_PRO," >> HOUR\r\n");
                // hours
                fspan[0]=H_SPAN_0;
                fspan[1]=H_SPAN_1;
                fxz=0;
                bit_fn=s_sch_h_bit;
                byte_fn=s_sch_h_byte;
                fname=fnames[i];
                err_flag=false;
            }else if(i==3){
                MPRINT(MOD_PRO," >> MINUTE\r\n");
                // minutes
                fspan[0]=M_SPAN_0;
                fspan[1]=M_SPAN_1;
                fxz=0;
                bit_fn=s_sch_m_bit;
                byte_fn=s_sch_m_byte;
                fname=fnames[i];
                err_flag=false;
            }else if(i==4){
                MPRINT(MOD_PRO," >> SECOND\r\n");
                // seconds
                fspan[0]=S_SPAN_0;
                fspan[1]=S_SPAN_1;
                fxz=0;
                bit_fn=s_sch_s_bit;
                byte_fn=s_sch_s_byte;
                fname=fnames[i];
                err_flag=false;
           }else if(i==5){
                MPRINT(MOD_PRO," >> WEEK DAY\r\n");
                // week day
                fspan[0]=WD_SPAN_0;
                fspan[1]=WD_SPAN_1;
                fxz=0;
                bit_fn=s_sch_wd_bit;
                byte_fn=s_sch_wd_byte;
                fname=fnames[i];
                err_flag=false;
            }// else invalid field

            if(!err_flag){
                // parse field range tokens
                int rtoks=tokenize(fields[i],ftoks,",",fspan[1]);
                int j=0;
                for(j=0;j<rtoks;j++){
                    MPRINT(MOD_PRO,"  >> Range tok[%d/%d] '%s'\r\n",j+1,rtoks,ftoks[j]);

                    // set up field
                    ps->span[0]=fspan[0];
                    ps->span[1]=fspan[1];
                    ps->range[0]=-1;
                    ps->range[1]=-1;
                    ps->range[2]=1;
                    ps->xz=fxz;
                    ps->xo=0;

                    if(s_sched_parse_range(ps,ftoks[j])==0){
                        // apply range
                        int k=0;
                        MPRINT(MOD_PRO,"   range[%d:%d] mod[%d] xz[%d] xo[%d]\r\n",ps->range[0],ps->range[1],ps->range[2],ps->xz,ps->xo);
                        for(k=ps->range[0];k<=ps->range[1];k++){
                            // logical bit number
                            // natural range - xz (xz=0/1, to adjust for zero/one-indexed ranges)
                            int ibit = (k-ps->xz);
                            // note xo=0:span[1]], which is >0 to offset the
                            // logical bit when modulus specs are used, i.e.
                            // so that the modulus is relative to the start
                            // of the modulus range
                            if( (ps->range[2]>0)  && ((ibit-ps->xo)%(ps->range[2]))==0){
                                dest[byte_fn(ibit)] |= (1 << bit_fn(ibit));
                                MPRINT(MOD_PRO,"    setting %s [%d:%d]\r\n",fname,byte_fn(ibit),bit_fn(ibit));
                            }
                        }// for range
                        MPRINT(MOD_PRO,"\r\n");
                    }else{
                        // else parse error
                        retval=-1;
                        break;
                    }
                }// for range tokens
            }else{
                retval=-1;
                break;
            }// if err_flag
        }// for field tokens
    }// else invalid arg

    return retval;
}

bool sched_match_tm(schedule_t dest, Time *ptm)
{
    bool retval=true;

    // offsets: 0:BYTE, 1:BIT
    int mo_ofs[2]={MO_BYTE(ptm->tm_mon),MO_BIT(ptm->tm_mon)};
    int md_ofs[2]={MD_BYTE(ptm->tm_mday-1),MD_BIT(ptm->tm_mday-1)};
    int wd_ofs[2]={WD_BYTE(ptm->tm_wday),WD_BIT(ptm->tm_wday)};
    int h_ofs[2]={H_BYTE(ptm->tm_hour),H_BIT(ptm->tm_hour)};
    int m_ofs[2]={M_BYTE(ptm->tm_min),M_BIT(ptm->tm_min)};
    int s_ofs[2]={S_BYTE(ptm->tm_sec),S_BIT(ptm->tm_sec)};
//    OUT_CTTY("tm_wday[%d] MO[%2d/%2d] mask[%02X] dest[%02X]\r\n",ptm->tm_mon,mo_ofs[0],mo_ofs[1],(1<<mo_ofs[1]),dest[mo_ofs[0]]);
//    OUT_CTTY("tm_wday[%d] MD[%2d/%2d] mask[%02X] dest[%02X]\r\n",ptm->tm_mday,md_ofs[0],md_ofs[1],(1<<md_ofs[1]),dest[md_ofs[0]]);
//    OUT_CTTY("tm_wday[%d]  H[%2d/%2d] mask[%02X] dest[%02X]\r\n",ptm->tm_hour,h_ofs[0],h_ofs[1],(byte)(1<<h_ofs[1]),dest[h_ofs[0]]);
//    OUT_CTTY("tm_wday[%d]  M[%2d/%2d] mask[%02X] dest[%02X]\r\n",ptm->tm_min,m_ofs[0],m_ofs[1],(byte)(1<<m_ofs[1]),dest[m_ofs[0]]);
//    OUT_CTTY("tm_wday[%d]  S[%2d/%2d] mask[%02X] dest[%02X]\r\n",ptm->tm_sec,s_ofs[0],s_ofs[1],(byte)(1<<s_ofs[1]),dest[s_ofs[0]]);
//    OUT_CTTY("tm_wday[%d] WD[%2d/%2d] mask[%02X] dest[%02X]\r\n",ptm->tm_wday,wd_ofs[0],wd_ofs[1],(1<<wd_ofs[1]),dest[wd_ofs[0]]);

    if( ((dest[mo_ofs[0]] & (byte)(1<<mo_ofs[1])) == 0)) return false;
    // either mday or wday may match
    if( ((dest[md_ofs[0]] & (byte)(1<<md_ofs[1])) == 0) &&
        ((dest[wd_ofs[0]] & (byte)(1<<wd_ofs[1])) == 0)) return false;
    if( ((dest[h_ofs[0]]  & (byte)(1<<h_ofs[1])) == 0)) return false;
    if( ((dest[m_ofs[0]]  & (byte)(1<<m_ofs[1])) == 0)) return false;
    if( ((dest[s_ofs[0]]  & (byte)(1<<s_ofs[1])) == 0)) return false;

    return retval;
}

// return true if schedule matches, false otherwise
bool sched_match_tt(schedule_t dest, time_t *tpos)
{
    bool retval=false;

    if(NULL!=dest && NULL!=tpos){
        Time *tm = gmtime(tpos);
        retval=sched_match_tm(dest,tm);
    }

    return retval;
}

// searches for next matching schedule field from specified starting point.
// wraps around at end; returns when match found or all bits checked.
// returns distance to next match, including start (>=0) or -1 if no matches found (error).
// A match may not be found for wday or mday, though one or both should have a matching value
static int s_next_x(schedule_t self, int start, int cycles, int *lim, sch_idx_fn by_fn, sch_idx_fn bi_fn)
{
    int retval=-1;
    int i=0;
    int cur[2]={0};
    int ofs=start;
    if(cycles>=0 && NULL!=lim && NULL!=bi_fn && NULL!=by_fn){
        MPRINT(MOD_AUX,">> start[%d] cycles[%d] lim[%d,%d] ofs[%d]\r\n",start,cycles,lim[0],lim[1],ofs);
        for(i=0;i<cycles;i++){

             MPRINT(MOD_AUX,"** i[%2d] ofs[%2d] cur[%2d,%2d] \r\n",i,ofs,cur[0],cur[1]);
             if(cur[0]>=lim[0] && cur[1]>=lim[1]){
                // wrap around if at end of cycles
                // reset cursor byte, bit
                cur[0]=by_fn(0);
                cur[1]=bi_fn(0);
                // reset starting offset
                ofs=0;
            }

            cur[0]=by_fn(ofs);
            cur[1]=bi_fn(ofs);

            if( (self[cur[0]]  & (byte)(1<<cur[1])) != 0 ){
               // match found number til next (including start)
               retval=i;
               MPRINT(MOD_AUX,"-> i[%2d] ofs[%2d] cur[%2d,%2d] ret[%2d]\r\n",i,ofs,cur[0],cur[1],retval);
                break;
            }

            ofs++;
        }
        MPRINT(MOD_AUX,"<< i[%2d] ofs[%2d] cur[%2d,%2d] ret[%2d]\r\n",i,ofs,cur[0],cur[1],retval);
    }
    return retval;
}

// return seconds until next event
// state machine makes a working copy of struct tm
// starts with most coarse time unit and advances it
// until everything matches (day,h,m,s), keeping a
// running total of advances along the way.
// When a non-match is encountered:
// - search for next match
// - if next match AFTER start of next greater time unit, add time until
//   next greater time unit, advance working time, then iterate. Such
//   rollovers can trigger additional rollovers upstream, however
//   it should only occur once per time unit.
// - if next match BEFORE start of next greater time unit, advance working time
//   by number of units to next match, advance total time by same less any
//   elapsed time.
// The state machine exits when all units match (seconds may be pending, may
// not be in rollover state).
//
time_t sched_next(schedule_t self, Time *ptm)
{
    time_t retval=(time_t)-1;
    time_t total=0;

    if(NULL!=self && NULL!=ptm){

        // make working copy of struct tm (aka Time))
        Time wtms={0},*wtm=&wtms;
        memcpy(wtm,ptm,sizeof(Time));

        // match search parameters
        int start=0;
        int cycles=0;
        int lim[2]={0,0};
        int32_t md_count=0;
        int32_t wd_count=0;
        int32_t h_count=0;
        int32_t m_count=0;
        int32_t s_count=0;
        // running totals
        int32_t day_sum=0;
        int32_t h_sum=0;
        int32_t m_sum=0;
        int32_t s_sum=0;
        int32_t carry=0;
        // state machine exit flag
         bool done=false;
        // time string (debug output)
        char tstr[32]={0};
        typedef enum{EDAY=0,MDAY,WDAY}day_type_t;

        if(MMOD_IS_EN(MOD_SCH)){
            int32_t sec_since_mid=(int32_t)(3600UL*ptm->tm_hour+60UL*ptm->tm_min+ptm->tm_sec);
            int32_t sec_to_mid=86400UL-sec_since_mid;
            TM2ISO8601(ptm, tstr);
            MPRINT(MOD_SCH,"ptm[%s] wd[%d] ly[%c] stm[%ld] ssm[%ld]\r\n",tstr,ptm->tm_wday,BOOLCHAR(SCH_IS_LEAPYEAR(ptm->tm_year+1900)),sec_to_mid,sec_since_mid);
        }
        int k=0;
        while(!done){

            TM2ISO8601(wtm, tstr);
            MPRINT(MOD_SCH,"cycle[%d] wtm[%s]\r\n",k++,tstr);

           // get next wday match
            start=wtm->tm_wday;
            cycles=WD_BITS;
            lim[0]=WD_BYTE(WD_IMAX);
            lim[1]=WD_BIT(WD_IMAX);
            wd_count=s_next_x(self, start, cycles, lim, s_sch_wd_byte, s_sch_wd_bit);

            // get next mday match
            // mday is 1-indexed in struct tm
            // schedule is 0 indexed
            start=wtm->tm_mday-1;
            if(start<0)start=0;
            // adjust number of cycles and limits
            // accounting for number of days in month
            switch(wtm->tm_mon){
            case 0://jan
            case 2://mar
            case 4://may
            case 6://jul
            case 7://aug
            case 9://oct
            case 11://dec
                cycles=31;
                lim[0]=MD_BYTE(30);
                lim[1]=MD_BIT(30);
                break;
            case 1://feb
                if(SCH_IS_LEAPYEAR(wtm->tm_year+1900)){
                 cycles=29;
                lim[0]=MD_BYTE(28);
                lim[1]=MD_BIT(28);
                }else{
                 cycles=28;
                lim[0]=MD_BYTE(27);
                lim[1]=MD_BIT(27);
                }
               break;
            case 3://apr
            case 5://jun
            case 8://sep
            case 10://nov
                cycles=30;
                lim[0]=MD_BYTE(29);
                lim[1]=MD_BIT(29);
                break;
            default:
                //ERROR
                TRACE();
                break;
            }
            md_count=s_next_x(self, start, cycles, lim, s_sch_md_byte, s_sch_md_bit);

            if(MMOD_IS_EN(MOD_SCH)){
              TM2ISO8601(wtm, tstr);
              MPRINT(MOD_SCH,"wtm[%s] mdc[%ld] wdc[%ld]\r\n",tstr,md_count,wd_count);
            }

            day_type_t use_day=EDAY;

            // use nearest day match (mday, wday)
            if(md_count>=0){
                if(wd_count>=0){
                    // both valid, use least
                    // (if equal, use WDAY for faster searches)
                    if(md_count<wd_count){
                        use_day=MDAY;
                    }else{
                        use_day=WDAY;
                    }
                }else{
                    // only mday valid
                    use_day=MDAY;
                }
            }else if(wd_count>=0){
                // only wday valid
                use_day=WDAY;
            }else{
                // ERR - neither valid
                TRACE();
            }


            if(use_day==MDAY){
                //md closest day
                if(md_count==0){
                    // currently matches
                    // check h:m:s below
                    // for match in this day
                    // (may roll over)
                    MPRINT(MOD_SCH,"MD_NOW\r\n");
                }else if(md_count>0){
                    // next matching day in future
                    // advance to the day
                    // reset h:m:s to 0:0:0
                    MPRINT(MOD_SCH,"MD_ROLL md[%d]\r\n",md_count);
                    carry=3600L*wtm->tm_hour+60L*wtm->tm_min+wtm->tm_sec;
                    time_t tt=mktime(wtm);
                    tt+=86400L*md_count;
                    Time *gtm = gmtime(&tt);
                    gtm->tm_hour=0;
                    gtm->tm_min=0;
                    gtm->tm_sec=0;
                    memcpy(wtm,gtm,sizeof(Time));
                    day_sum+=86400L*md_count-carry;
                }else{
                    // ERROR
                    TRACE();
                }
            }else if(use_day==WDAY){
                //wd closest day
                if(wd_count==0){
                    // currently matches
                    // check h:m:s below
                    // for match in this day
                    // (may roll over)
                    MPRINT(MOD_SCH,"WD_NOW\r\n");
                }else if(wd_count>0){
                    // next matching day in future
                    // advance to the day
                    // reset h:m:s to 0:0:0
                    MPRINT(MOD_SCH,"WD_ROLL wd[%ld]\r\n",wd_count);
                    carry=3600L*wtm->tm_hour+60L*wtm->tm_min+wtm->tm_sec;
                    time_t tt=mktime(wtm);
                    tt+=86400L*wd_count;
                    Time *gtm = gmtime(&tt);
                    gtm->tm_hour=0;
                    gtm->tm_min=0;
                    gtm->tm_sec=0;
                    memcpy(wtm,gtm,sizeof(Time));
                    day_sum+=86400L*wd_count-carry;
                }else{
                    // ERROR
                    TRACE();
                }
            }else{
                //ERROR
                TRACE();
            }

            if(MMOD_IS_EN(MOD_SCH)){
                TM2ISO8601(wtm, tstr);
                MPRINT(MOD_SCH,"wtm[%s] d_sum[%6ld] carry[%6ld]\r\n",tstr,day_sum,carry);
            }

            // check for next hour match
            start=wtm->tm_hour;
            cycles=H_BITS;
            lim[0]=H_BYTE(H_IMAX);
            lim[1]=H_BIT(H_IMAX);
            h_count=s_next_x(self, start, cycles, lim, s_sch_h_byte, s_sch_h_bit);

            if(h_count==0){
                // NOW: hour matches
                // check m:s below for match
                // (may rollover)
                MPRINT(MOD_SCH,"H_NOW\r\n");
            }else if(h_count>0){
                if(wtm->tm_hour+h_count<24){
                    // PENDING: match remaining
                    // advance to start of matching hour
                    MPRINT(MOD_SCH,"H_PENDING hc[%ld]\r\n",h_count);
                    carry=(60L*wtm->tm_min+wtm->tm_sec);
                    time_t tt=mktime(wtm);
                    tt+=3600L*h_count;
                    Time *gtm = gmtime(&tt);
                    gtm->tm_min=0;
                    gtm->tm_sec=0;
                    memcpy(wtm,gtm,sizeof(Time));
                    h_sum+=(3600L*h_count-carry);
                }else if(wtm->tm_hour+h_count>=24){
                    // ROLLOVER: no match remaining:
                    // advance to start of next day and start again
                    MPRINT(MOD_SCH,"H_ROLL hc[%ld]\r\n",h_count);
                    carry=86400L-(3600L*wtm->tm_hour+60*wtm->tm_min+wtm->tm_sec);
                    time_t tt=mktime(wtm);
                    tt+=86400L;
                    Time *gtm = gmtime(&tt);
                    gtm->tm_hour=0;
                    gtm->tm_min=0;
                    gtm->tm_sec=0;
                    memcpy(wtm,gtm,sizeof(Time));
                    h_sum+=carry;
                    TM2ISO8601(wtm, tstr);
                    MPRINT(MOD_SCH,"wtm[%s] h_sum[%6ld] carry[%6ld]\r\n",tstr,h_sum,carry);
                    continue;
                }
            }else{
                // ERROR
                TRACE();
            }

            TM2ISO8601(wtm, tstr);
            MPRINT(MOD_SCH,"wtm[%s] h_sum[%6ld] carry[%6ld]\r\n",tstr,h_sum,carry);

            // check for next min match
            start=wtm->tm_min;
            cycles=M_BITS;
            lim[0]=M_BYTE(M_IMAX);
            lim[1]=M_BIT(M_IMAX);
            m_count=s_next_x(self, start, cycles, lim, s_sch_m_byte, s_sch_m_bit);

            if(m_count==0){
                // NOW: min matches
                // check m:s below for match
                // (may rollover)
                MPRINT(MOD_SCH,"M_NOW\r\n");
            }else if(m_count>0){
                if(wtm->tm_min+m_count<60){
                    // PENDING: match remaining
                    // advance to start of matching hour
                    MPRINT(MOD_SCH,"M_PENDING mc[%ld]\r\n",m_count);
                    carry=wtm->tm_sec;
                    time_t tt=mktime(wtm);
                    tt+=60L*m_count;
                    Time *gtm = gmtime(&tt);
                    gtm->tm_sec=0;
                    memcpy(wtm,gtm,sizeof(Time));
                    m_sum+=(60L*m_count-carry);
                }else if(wtm->tm_min+m_count>=60){
                    // ROLLOVER: no match remaining:
                    // advance to start of next hour and start again
                    MPRINT(MOD_SCH,"M_ROLL mc[%ld]\r\n",m_count);
                    carry=3600L-(60L*wtm->tm_min+wtm->tm_sec);
                    time_t tt=mktime(wtm);
                    tt+=3600L;
                    Time *gtm = gmtime(&tt);
                    gtm->tm_min=0;
                    gtm->tm_sec=0;
                    memcpy(wtm,gtm,sizeof(Time));
                    m_sum+=carry;
                    TM2ISO8601(wtm, tstr);
                    MPRINT(MOD_SCH,"wtm[%s] m_sum[%6ld] carry[%6ld]\r\n",tstr,m_sum,carry);
                    continue;
                }
            }else{
                // ERROR
                TRACE();
            }

            TM2ISO8601(wtm, tstr);
            MPRINT(MOD_SCH,"wtm[%s] m_sum[%6ld] carry[%6ld]\r\n",tstr,m_sum,carry);

            // check for next sec match
            start=wtm->tm_sec;
            cycles=S_BITS;
            lim[0]=S_BYTE(S_IMAX);
            lim[1]=S_BIT(S_IMAX);
            s_count=s_next_x(self, start, cycles, lim, s_sch_s_byte, s_sch_s_bit);

            if(s_count==0){
                // NOW: sec matches
                // check m:s below for match
                // (may rollover)
                MPRINT(MOD_SCH,"S_NOW\r\n");
                done=1;
            }else if(s_count>0){
                if(wtm->tm_sec+s_count<60){
                    // PENDING: match remaining
                    // advance to start of matching min
                    MPRINT(MOD_SCH,"S_PENDING sc[%ld]\r\n",s_count);
                    s_sum+=s_count;
                    done=true;
                }else if(wtm->tm_sec+s_count>=60){
                    // ROLLOVER: no match remaining:
                    // advance to start of next min and start again
                    MPRINT(MOD_SCH,"S_ROLL sc[%ld]\r\n",s_count);
                    carry=60L-wtm->tm_sec;
                    time_t tt=mktime(wtm);
                    tt+=60L;
                    Time *gtm = gmtime(&tt);
                    gtm->tm_sec=0;
                    memcpy(wtm,gtm,sizeof(Time));
                    s_sum+=carry;
                    TM2ISO8601(wtm, tstr);
                    MPRINT(MOD_SCH,"wtm[%s] s_sum[%6ld] carry[%6ld]\r\n",tstr,s_sum,carry);
                    continue;
                }
                TM2ISO8601(wtm, tstr);
                MPRINT(MOD_SCH,"wtm[%s] s_sum[%6ld] carry[%6ld]\r\n",tstr,s_sum,carry);
            }else{
                // ERROR
                TRACE();
            }

            // update total of advances (sec)
            total=day_sum+h_sum+m_sum+s_sum;
            MPRINT(MOD_SCH,"total[%ld]\r\n",total);

            // for debug: use iow_get_char here

        }// while !done

        // set return value
        retval=total;
    }
    return retval;
}

static void s_sched_out_range(schedule_t self, int size,sch_idx_fn bi_fn, sch_idx_fn by_fn, int zofs, char **names)
{
    int i=0,rval=-1;

    for(i=0;i<size;i++){
        int iby=by_fn(i);
        byte by=self[iby];
        int bi=bi_fn(i);
//            OUT_CTTY("i[%d] by[%d/%02X] bi[%d/%02X]\r\n",i,iby,by,bi,((1<<bi)));
        if( ((1<<bi)&by)!=0 ){

            // bit is set
            if(rval<0){
                // save first value in set range
                rval=i;
            }
        }else{
            // bit not set
            if(rval>=0){
                // end a range of set values
                if((i-1)>rval){
                    if(NULL!=names){
                        // names always zero-indexed
                        OUT_CTTY(" %s-%s",names[rval],names[(i-1)]);
                    }else{
                        OUT_CTTY(" %d-%d",rval+zofs,(i-1+zofs));
                    }
                }else{
                    if(NULL!=names){
                        // names always zero-indexed
                        OUT_CTTY(" %s",names[rval]);
                    }else{
                        OUT_CTTY(" %d",rval+zofs);
                    }
                }
                rval=-1;

            }
        }
    }
    if(rval>=0){
        if((i-1)>rval){
            if(NULL!=names){
                // names always zero-indexed
                OUT_CTTY(" %s-%s",names[rval],names[(i-1)]);
            }else{
                OUT_CTTY(" %d-%d",rval+zofs,(i-1+zofs));
            }
        }else{
            if(NULL!=names){
                // names always zero-indexed
                OUT_CTTY(" %s",names[rval]);
            }else{
                OUT_CTTY(" %d",rval+zofs);
            }
        }
    }
    OUT_CTTY("\r\n");
    return;
}

static void s_sched_out_bits(schedule_t self, int size,sch_idx_fn bi_fn, sch_idx_fn by_fn)
{
    int i=0;
    for(i=size-1;i>=0;i--){
        byte by=self[by_fn(i)];
        int bi=bi_fn(i);
        OUT_CTTY("%c",(((1<<bi)&by)==0?'0':'1') );
    }
}

int sched_r_show(schedule_t self,int indent)
{
    int retval=0;

    if(NULL!=self){
        int wkey=8;
//        int wval=20;
        // Month
        OUT_CTTY("%*s%*s",indent,"",wkey,"Mon: ");
//        s_sched_out_range( self, 12 ,s_sch_mo_bit, s_sch_mo_byte,1,NULL);
        s_sched_out_range( self, 12 ,s_sch_mo_bit, s_sch_mo_byte,0,sch_mo_names);
        // Mon Day
        OUT_CTTY("%*s%*s",indent,"",wkey,"Mday: ");
        s_sched_out_range( self, 31 ,s_sch_md_bit, s_sch_md_byte,1,NULL);
        // Hour
        OUT_CTTY("%*s%*s",indent,"",wkey,"Hour: ");
        s_sched_out_range( self, 24 ,s_sch_h_bit, s_sch_h_byte,0,NULL);
        // Min
        OUT_CTTY("%*s%*s",indent,"",wkey,"Min: ");
        s_sched_out_range( self, 60 ,s_sch_m_bit, s_sch_m_byte,0,NULL);
        // Sec
        OUT_CTTY("%*s%*s",indent,"",wkey,"Sec: ");
        s_sched_out_range( self, 60 ,s_sch_s_bit, s_sch_s_byte,0,NULL);
        // Wday
        OUT_CTTY("%*s%*s",indent,"",wkey,"Wday: ");
        s_sched_out_range( self, 7 ,s_sch_wd_bit, s_sch_wd_byte,0,sch_wd_names);
    }
    return retval;
}

int sched_b_show(schedule_t self,int indent)
{
    int retval=0;

    if(NULL!=self){
        int wkey=8;
//        int wval=20;
        // Month
        OUT_CTTY("%*s%*s",indent,"",wkey,"Mon: ");
        s_sched_out_bits( self,12,s_sch_mo_bit, s_sch_mo_byte);
        OUT_CTTY("\r\n");
        // Mon Day
        OUT_CTTY("%*s%*s",indent,"",wkey,"Mday: ");
        s_sched_out_bits( self,31,s_sch_md_bit, s_sch_md_byte);
        OUT_CTTY("\r\n");
        // Hour
        OUT_CTTY("%*s%*s",indent,"",wkey,"Hour: ");
        s_sched_out_bits( self,24,s_sch_h_bit, s_sch_h_byte);
        OUT_CTTY("\r\n");
        // Minute
        OUT_CTTY("%*s%*s",indent,"",wkey,"Min: ");
        s_sched_out_bits( self,60,s_sch_m_bit, s_sch_m_byte);
        OUT_CTTY("\r\n");
        // Second
        OUT_CTTY("%*s%*s",indent,"",wkey,"Sec: ");
        s_sched_out_bits( self,60,s_sch_s_bit, s_sch_s_byte);
        OUT_CTTY("\r\n");
        // WeekDay
        OUT_CTTY("%*s%*s",indent,"",wkey,"Wday: ");
        s_sched_out_bits( self,7,s_sch_wd_bit, s_sch_wd_byte);
        OUT_CTTY("\r\n");
    }
    return retval;
}
