/*****************************************************************************/
/*                                                                           */
/*                                 ICCRON.CC                                 */
/*                                                                           */
/* (C) 1996     Ullrich von Bassewitz                                        */
/*              Wacholderweg 14                                              */
/*              D-70597 Stuttgart                                            */
/* EMail:       uz@ibb.schwaben.com                                          */
/*                                                                           */
/*****************************************************************************/



// $Id$
//
// $Log$
//
//



#include "bitset.h"
#include "chartype.h"
#include "coll.h"
#include "str.h"
#include "strcvt.h"
#include "strparse.h"
#include "stdmsg.h"
#include "progutil.h"

#include "icconst.h"
#include "icmsg.h"
#include "icevents.h"
#include "icdlog.h"
#include "icerror.h"
#include "iccom.h"
#include "iccprint.h"
#include "icconfig.h"
#include "icfile.h"
#include "iccron.h"



/*****************************************************************************/
/*                             Message Constants                             */
/*****************************************************************************/



const u16 msOpenError                   = MSGBASE_ICCRON +  0;
const u16 msSyntaxError                 = MSGBASE_ICCRON +  1;
const u16 msCmdError                    = MSGBASE_ICCRON +  2;
const u16 msCronJobProcessing           = MSGBASE_ICCRON +  3;



/*****************************************************************************/
/*                      Explicit template instantiation                      */
/*****************************************************************************/



#ifdef EXPLICIT_TEMPLATES
template class Collection<class CronEvent>;
#endif



/*****************************************************************************/
/*                                   Data                                    */
/*****************************************************************************/



// Name of the cron file, default is the empty string
String CronFile = "";



// Names of internal commands and there IDs
enum {
    icPrintCharges,
    icClearCharges,
    icLoadConfig,
    icSwitchConfig,
    icRing,
    icCount
};

static const char* InternalCommands [icCount] = {
    "PRINTCHARGES",
    "CLEARCHARGES",
    "LOADCONFIG",
    "SWITCHCONFIG",
    "RING"
};



/*****************************************************************************/
/*                              class CronEvent                              */
/*****************************************************************************/



class CronEvent: public Streamable {

private:
    BitSet      Minute;                 // Time
    BitSet      Hour;
    BitSet      WeekDay;
    BitSet      MonthDay;
    BitSet      Month;
    int         Cmd;                    // Which command to execute?

    // Arguments
    String      Filename;               // Filename argument
    u32         Device;                 // Device argument
    u32         Duration;               // Duration argument
    unsigned    DayNight;               // Day/night argument


    static int FindCmdID (String Cmd);
    // Search for the given command in the command table and return the ID. If
    // the command is not found, -1 is returned.

    static int InBounds (const BitSet& B, i32 Num);
    // Check if a given number is in bounds for the bitfield. Return YES or NO.

    int ParseRingArgs (const String& Args);
    // Parse the numeric arguments for the RING command. Return SUCCESS or
    // FAILURE.

    static int ParseTimeExpr (BitSet& B, const String& S);
    // Parse a cron time expression in the string S and set the time in the
    // bitset B. It is assumed that S is non empty. The function returns
    // SUCCESS or FAILURE.

    int ParseTime (BitSet& B, const String& S);
    // Parse a cron time expression in the string S and set the time in the
    // bitset B. It is assumed that S is non empty. The function returns
    // SUCCESS or FAILURE.

    String ExpandFilename (const Time& T);
    // Expand the file name spec to a real file name.

    int CronPrintCharges (const Time& T);
    // Execute the PrintCharges command. Return SUCCESS or FAILURE.

    int CronClearCharges ();
    // Execute the ClearCharges command. Return SUCCESS or FAILURE.

    int CronLoadConfig (const Time& T);
    // Execute the LoadConfig command. Return SUCCESS or FAILURE.

    int CronSwitchConfig ();
    // Execute the SwitchConfig command. Return SUCCESS or FAILURE.

    int CronRing ();
    // Execute the Ring command. Return SUCCESS or FAILURE.

public:
    CronEvent ();
    // Constructor

    int TimeMatch (unsigned aMinute, unsigned aHour, unsigned aMonthDay,
                   unsigned aMonth, unsigned aWeekDay);
    // Return true if the cron event has a time match for the given time
    // values. Return YES or NO.

    int ParseTimeSpec (String& Line);
    // Parse the time spec of the given cron file line. If the time spec is
    // valid, the internal time fields of the CronEvent object are filled
    // with the retrieved values, the time spec is removed from Line, and the
    // function returns SUCCESS. If the time spec is invalid, the function
    // returns FAILURE.

    int ParseCmdSpec (const String& Line);
    // Parse the command specification of the given cron command. If the
    // command and its parameters are valid, the internal variables of the
    // object are set to these values and the function returns SUCCESS. If the
    // command and/or the parameters are invalid, the function returns FAILURE.

    int Execute (const Time& T);
    // Execute a cron event. The error code (SUCCESS/FAILURE) of the command
    // actually executed is returned. The function does *no* time checks!
};



CronEvent::CronEvent ():
    Minute (60),
    Hour (24),
    WeekDay (7),
    MonthDay (31, 1),
    Month (12, 1),
    Cmd (icCount)               // To be shure it's wrong
{
}



int CronEvent::FindCmdID (String Cmd)
// Search for the given command in the command table and return the ID. If
// the command is not found, -1 is returned.
{
    // Make the command upper case
    Cmd.ToUpper ();

    // This is a linear search, since the count of commands is very small
    for (int I = 0; I < icCount; I++) {
        if (strcmp (InternalCommands [I], Cmd.GetStr ()) == 0) {
            // Found
            return I;
        }
    }

    // Not found
    return -1;
}



int CronEvent::InBounds (const BitSet& B, i32 Num)
// Check if a given number is in bounds for the bitfield
{
    return (Num >= B.Low () && Num <= B.High ())? YES : NO;
}



int CronEvent::ParseRingArgs (const String& Args)
// Parse the numeric arguments for the RING command. The function returns
// SUCCESS or FAILURE.
{
    // Create a string parser
    StringParser SP (Args, StringParser::SkipWS);

    // Read the device
    if (SP.GetU32 (Device) != 0 || Device < FirstDev || Device > LastDev) {
        // Device parameter invalid
        return FAILURE;
    }

    // Read the duration
    if (SP.GetU32 (Duration) != 0 || Duration == 0 || Duration > 5*60) {
        // Duration parameter invalid
        return FAILURE;
    }

    // Success
    return SUCCESS;
}



int CronEvent::ParseTimeExpr (BitSet& B, const String& S)
// Parse a cron time expression in the string S and set the time in the
// bitset B. It is assumed that S is non empty. The function returns
// SUCCESS or FAILURE.

{
    PRECONDITION (!S.IsEmpty ());

    // Set up a string parser
    StringParser SP (S, 0);

    // Read a number
    u32 Start;
    if (SP.GetU32 (Start) != 0 || !InBounds (B, Start)) {
        return FAILURE;
    }

    // Check if end of string is reached, if yes, set the bit and bail out
    if (SP.EOS ()) {
        B += (int) Start;
        return SUCCESS;
    }

    // If the string did not end, we expect a range expression
    if (S [SP.GetPos ()] != '-') {
        // Some error
        return FAILURE;
    }
    SP.SetPos (SP.GetPos () + 1);

    // Read the end of the range
    u32 End;
    if (SP.GetU32 (End) != 0 || !InBounds (B, End)) {
        return FAILURE;
    }

    // End must be greater than Start
    if (End < Start) {
        return FAILURE;
    }

    // Maybe we have an additional space operator
    u32 Space = 1;
    if (!SP.EOS ()) {

        if (S [SP.GetPos ()] != '/') {
            // Some error
            return FAILURE;
        }
        SP.SetPos (SP.GetPos () + 1);

        // Read the space number
        if (SP.GetU32 (Space) != 0) {
            return FAILURE;
        }

        // String must be empty now
        if (!SP.EOS ()) {
            return FAILURE;
        }
    }

    // Now insert the range into the bitset
    while (Start <= End) {
        B += (int) Start;
        Start += Space;
    }

    // Done
    return SUCCESS;
}



int CronEvent::ParseTime (BitSet& B, const String& S)
// Parse a cron time expression in the string S and set the time in the
// bitset B. It is assumed that S is non empty. The function returns
// SUCCESS or FAILURE.
{
    PRECONDITION (!S.IsEmpty ());

    // Handle non-digit expression '*'
    if (S == "*") {
        // '* Means: At each full X
        B.SetAll ();
        return SUCCESS;
    }

    // Set up a string parser
    StringParser SP (S, 0);

    // Read a list of tokens separated by ',' and evaluate each of those
    // tokens
    String Tok;
    CharSet Separator = ",";
    while (SP.GetToken (Tok, Separator) == 0) {
        // Parse the token
        if (ParseTimeExpr (B, Tok) == FAILURE) {
            // Parse error
            return FAILURE;
        }
        // Skip the token separators
        SP.Skip (Separator);
    }

    // String must be empty now
    if (!SP.EOS ()) {
        return FAILURE;
    }

    // Done with success
    return SUCCESS;
}



inline String CronEvent::ExpandFilename (const Time& T)
// Expand the file name spec to a real file name.
{
    return T.DateTimeStr (Filename);
}



int CronEvent::CronPrintCharges (const Time& T)
// Execute the PrintCharges command. Return SUCCESS or FAILURE.
{
    // Assume an error
    int Result = FAILURE;

    // Expand the filename
    String Name = ExpandFilename (T);

    // Print the charges
    String Msg = PrintCharges (Name);
    if (Msg.IsEmpty ()) {
        // No error
        Msg = "Done";
        Result = SUCCESS;
    }

    // Log the result
    WriteDebugLog (FormatStr ("CRON PrintCharges (%s): %s",
                              Name.GetStr (),
                              Msg.GetStr()));

    // Return the result
    return Result;
}



int CronEvent::CronClearCharges ()
// Execute the ClearCharges command. Return SUCCESS or FAILURE.
{
    // Create an empty charge object
    IstecCharges Charges;

    // Send the charges
    IstecPutCharges (Charges);

    // Log the action to the debug log
    WriteDebugLog ("CRON ClearCharges (): Done");

    // We return always SUCCESS here since the command for clearing the
    // charges is put out in the background and we have no way of checking
    // if the Istec did accept it.
    return SUCCESS;
}



int CronEvent::CronLoadConfig (const Time& T)
// Execute the LoadConfig command. Return SUCCESS or FAILURE.
{
    // Assume an error
    int Result = FAILURE;

    // Expand the filename
    String Name = ExpandFilename (T);

    // Try to load the config file with the given name
    IstecConfig Config;
    String Msg = IstecLoadFile (Name, Config);
    if (Msg.IsEmpty ()) {
        // No error. Send the stuff to the istec
        int Res = IstecPutConfig (Config);

        // Make an error message from the return code
        if (Res == ieDone) {
            Msg = "Done";
            Result = SUCCESS;
        } else {
            Msg = FormatStr ("Error #%d", Res);
        }
    }

    // Log the result
    WriteDebugLog (FormatStr ("CRON LoadConfig (%s): %s",
                              Name.GetStr (),
                              Msg.GetStr ()));

    // Return the result
    return Result;
}



int CronEvent::CronSwitchConfig ()
// Execute the SwitchConfig command. Return SUCCESS or FAILURE.
{
    String Msg;

    // Assume an error
    int Result = FAILURE;

    // Switch configuration, post an event, make an error message from the
    // return code
    int RC = IstecPutDayNight (DayNight);

    if (RC == ieDone) {
        PostEvent (evDayNightChange, DayNight);
        Msg = "Done";
        Result = SUCCESS;
    } else {
        Msg = FormatStr ("Error #%d", RC);
    }

    // Log the result
    WriteDebugLog (FormatStr ("CRON SwitchConfig: %s", Msg.GetStr ()));

    // Return the result
    return Result;
}



int CronEvent::CronRing ()
// Execute the Ring command. Return SUCCESS or FAILURE.
{
    // Make one long from the device and duration
    u32 Parm = Duration * 256 + Device;

    // Posts an event and hope someone will notice :-)
    PostEvent (evForcedRing, Parm);

    // Log the succes of the action
    WriteDebugLog (FormatStr ("CRON Ring (%d, %d): Done", Device, Duration));

    // We return always SUCCESS here since the command is executed in the
    // background and we have no way checking if the Istec did accept it.
    return SUCCESS;
}



int CronEvent::TimeMatch (unsigned aMinute, unsigned aHour, unsigned aMonthDay,
                          unsigned aMonth, unsigned aWeekDay)
// Return true if the cron event has a time match for the given time
// values. Return YES or NO.
{
    return Minute [aMinute]     != 0 &&
           Hour [aHour]         != 0 &&
           MonthDay [aMonthDay] != 0 &&
           Month [aMonth]       != 0 &&
           WeekDay [aWeekDay]   != 0;
}



int CronEvent::ParseTimeSpec (String& Line)
// Parse the time spec of the given cron file line. If the time spec is
// valid, the internal time fields of the CronEvent object are filled
// with the retrieved values, the time spec is removed from Line, and the
// function returns SUCCESS. If the time spec is invalid, the function
// returns FAILURE.
{
    // Set up a string parser for the string
    StringParser SP (Line, StringParser::SkipWS);

    // Extract the tokens in the order minute, hour, day, month, weekday
    String SMinute, SHour, SMonthDay, SMonth, SWeekDay;
    if (SP.GetToken (SMinute)        != 0 ||
        SP.GetToken (SHour)          != 0 ||
        SP.GetToken (SMonthDay)      != 0 ||
        SP.GetToken (SMonth)         != 0 ||
        SP.GetToken (SWeekDay)       != 0) {
        // Error
        return FAILURE;
    }

    // Parse the time items and set the time bitsets
    if (ParseTime (Minute,   SMinute)     == FAILURE ||
        ParseTime (Hour,     SHour)       == FAILURE ||
        ParseTime (MonthDay, SMonthDay)   == FAILURE ||
        ParseTime (Month,    SMonth)      == FAILURE ||
        ParseTime (WeekDay,  SWeekDay)    == FAILURE) {
        // Error
        return FAILURE;
    }

    // Successful. Skip additional white space, then remove the time spec
    // from the string
    SP.SkipWhite ();
    Line.Del (0, SP.GetPos ());

    // Done
    return SUCCESS;
}



int CronEvent::ParseCmdSpec (const String& Line)
// Parse the command specification of the given cron command. If the
// command and its parameters are valid, the internal variables of the
// object are set to these values and the function returns SUCCESS. If the
// command and/or the parameters are invalid, the function returns FAILURE.
{
    // Set up a string parser for the string
    StringParser SP (Line, StringParser::SkipWS);

    // Read the next token from the line, this must be the command
    String SCommand;
    if (SP.GetToken (SCommand) != 0) {
        // No token there
        return FAILURE;
    }

    // The remainder of the line are arguments
    SP.SkipWhite ();
    String Args = Line.Cut (SP.GetPos (), Line.Len () - SP.GetPos ());

    // Search for the command in the internal command table, translate it to
    // a command id which is simpler to handle
    Cmd = FindCmdID (SCommand);

    // Parse the commands for the command
    switch (Cmd) {

        case icPrintCharges:
        case icLoadConfig:
            // Commands with a filename argument. Remove leading and
            // trailing spaces
            Args.Remove (" \t", rmLeading | rmTrailing);
            if (Args.IsEmpty ()) {
                return FAILURE;
            }
            Filename = Args;
            break;

        case icSwitchConfig:
            // One argument, "day" or "night". Remove whitespace
            Args.Remove (" \t", rmLeading | rmTrailing);
            Args.ToUpper ();
            if (Args == "DAY") {
                DayNight = 0;
            } else if (Args == "NIGHT") {
                DayNight = 1;
            } else {
                return FAILURE;
            }
            break;

        case icClearCharges:
            // Commands with no arguments
            break;

        case icRing:
            // Two numeric arguments
            if (ParseRingArgs (Args) == FAILURE) {
                return FAILURE;
            }
            break;

        case -1:
            // Unknown command
            return FAILURE;

    }

    // Success
    return SUCCESS;
}



int CronEvent::Execute (const Time& T)
// Execute a cron event. The error code (SUCCESS/FAILURE) of the command
// actually executed is returned. The function does *no* time checks!
{
    // Execute the function
    switch (Cmd) {
        case icPrintCharges:    return CronPrintCharges (T);
        case icClearCharges:    return CronClearCharges ();
        case icLoadConfig:      return CronLoadConfig (T);
        case icSwitchConfig:    return CronSwitchConfig ();
        case icRing:            return CronRing ();
        default:                FAIL ("CronEvent::Execute: Unknown event code");
    }

    // Never reached...
    return FAILURE;
}



/*****************************************************************************/
/*                            class CronEventColl                            */
/*****************************************************************************/



class CronEventColl: public Collection<CronEvent> {

public:
    CronEventColl ();
    // Constructor

};



CronEventColl::CronEventColl ():
    Collection <CronEvent> (25, 25, 1)
{
}



/*****************************************************************************/
/*                                   Data                                    */
/*****************************************************************************/



static CronEventColl CronEvents;



/*****************************************************************************/
/*                                   Code                                    */
/*****************************************************************************/



void HandleCronEvent (const Time& T)
// Execute all events that match the given time
{
    // Lock the function against recursive calls that may happen if the
    // execution of one command lasts too long
    static int Running = 0;
    if (Running) {
        return;
    }
    Running = 1;

    // Convert the given time to separate values
    unsigned Sec, Minute, Hour, WeekDay, MonthDay, Month, Year;
    T.GetDateTime (Year, Month, MonthDay, WeekDay, Hour, Minute, Sec);

    // A window notifying the user
    Window* Win = 0;

    // Loop through all events, checking if there is a match
    for (int I = 0; I < CronEvents.GetCount (); I++) {

        // Get the specific event
        CronEvent* E = CronEvents [I];

        // Ignore the event if we don't have a match
        if (!E->TimeMatch (Minute, Hour, MonthDay, Month, WeekDay)) {
            // No match, continue
            continue;
        }

        // If this is the first match, pop up a window, notifying the user
        if (Win == 0) {
            Win = MsgWindow (LoadAppMsg (msCronJobProcessing), "", paCyan);
        }

        // Execute the function
        E->Execute (T);

    }

    // If we have a window open, close it
    delete Win;

    // Unlock the function
    Running = 0;
}



void ReadCronFile ()
// Delete all existing cron events and reread the cron file. The function
// does nothing if there is no cron file defined.
{
    // Bail out if there is no valid cron file name
    if (CronFile.IsEmpty ()) {
        return;
    }

    // Delete all existing aliases
    CronEvents.DeleteAll ();

    // Try to open the file
    FILE* F = fopen (CronFile.GetStr (), "rt");
    if (F == NULL) {
        // OOPS, file open error
        IstecError (msOpenError);
        return;
    }

    // Ok, file is open now, read it
    unsigned LineCount = 0;
    char Buf [512];
    while (fgets (Buf, sizeof (Buf), F) != NULL) {

        // Got a new line
        LineCount++;

        // Put the line into a string and convert it to the internally used
        // character set
        String S (Buf);
        S.InputCvt ();

        // Delete the trailing newline if any
        int Len = S.Len ();
        if (Len > 0 && S [Len-1] == '\n') {
            Len--;
            S.Trunc (Len);
        }

        // Ignore empty and comment lines
        if (S.IsEmpty () || S [0] == ';') {
            continue;
        }

        // Create a new cron event
        CronEvent* E = new CronEvent;

        // Parse the time specification in the line. When successful, this
        // will remove the time spec just parsed.
        if (E->ParseTimeSpec (S) == FAILURE) {
            // Some sort of error
            ErrorMsg (FormatStr (LoadAppMsg (msSyntaxError).GetStr (), LineCount));
            delete E;
            continue;
        }

        // Parse the command in the line
        if (E->ParseCmdSpec (S) == FAILURE) {
            // Error
            ErrorMsg (FormatStr (LoadAppMsg (msCmdError).GetStr (), LineCount));
            delete E;
            continue;
        }

        // Insert the cron event into the collection
        CronEvents.Insert (E);

    }

    // Close the file
    fclose (F);
}



int ExecuteCronEvent (const String& Event)
// Execute the cron event (without time spec!) given in event. If there are
// parse errors on Event, the function returns FAILURE and the event is not
// executed. Otherwise the event is executed and the function returns the
// result of the execution.
{
    // Create a new cron event
    CronEvent E;

    // Parse the given command
    if (E.ParseCmdSpec (Event) == FAILURE) {
        // Error
        return FAILURE;
    }

    // Execute the command
    return E.Execute (Now ());
}



