notification_scheduler.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2014 - License: CC BY 2.0      //
//  Please see: https://creativecommons.org/licenses/by/2.0 for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2011 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
list wasListReverse(list lst) {
    if(llGetListLength(lst)<=1) return lst;
    return wasListReverse(
        llList2List(lst, 1, llGetListLength(lst))
    ) + llList2List(lst,0,0);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasDayOfWeek(integer year, integer month, integer day) {
    return llList2String(
        [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", 
            "Saturday", "Sunday" ],
        (
            day
            + ((153 * (month + 12 * ((14 - month) / 12) - 3) + 2) / 5)
            + (365 * (year + 4800 - ((14 - month) / 12)))              
            + ((year + 4800 - ((14 - month) / 12)) / 4)
            - ((year + 4800 - ((14 - month) / 12)) / 100)
            + ((year + 4800 - ((14 - month) / 12)) / 400)
            - 32045
        ) % 7
    );
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
integer wasGetYearDays(integer year) {
    integer leap = (year % 4 == 0 && year % 100 != 0) || 
            (year % 400 == 0);
    if(leap == TRUE) {
        return 366;
    }
    return 365;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
integer wasGetMonthDays(integer month, integer year) {
    if (month == 4 || month == 6 || month == 9 || month == 11) {
        return 30;
    }
    if(month == 2) {
        integer leap = (year % 4 == 0 && year % 100 != 0) || 
            (year % 400 == 0);
        if(leap == TRUE) {
            return 29;
        }
        return 28;
    }
    return 31;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasUnixTimeToStamp(integer unix) {
    integer year = 1970;
    integer dayno = unix / 86400;
    do {
        dayno -= wasGetYearDays(year);
        ++year;
    } while (dayno >= wasGetYearDays(year));
    integer month = 1;
    do {
        dayno -= wasGetMonthDays(month, year);
        ++month;
    } while (dayno >= wasGetMonthDays(month, year));
    return (string)year + "-" +
           (string)month + "-" +
           (string)(dayno + 1) + "T" +
           (string)((unix % 86400) / 3600) + ":" +
           (string)(((unix % 86400) % 3600) / 60) + ":" +
           (string)(unix % 60) + ".0Z";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
integer wasMenuIndex = 0;
list wasDialogMenu(list input, list actions, string direction) {
    integer cut = 11-wasListCountExclude(actions, [""]);
    if(direction == ">" &&  (wasMenuIndex+1)*cut+wasMenuIndex+1 < llGetListLength(input)) {
        ++wasMenuIndex;
        jump slice;
    }
    if(direction == "<" && wasMenuIndex-1 >= 0) {
        --wasMenuIndex;
        jump slice;
    }
@slice;
    integer multiple = wasMenuIndex*cut;
    input = llList2List(input, multiple+wasMenuIndex, multiple+cut+wasMenuIndex);
    input = wasListMerge(input, actions, "");
    return input;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
integer wasListCountExclude(list input, list exclude) {
    if(llGetListLength(input) == 0) return 0;
    if(llListFindList(exclude, (list)llList2String(input, 0)) == -1) 
        return 1 + wasListCountExclude(llDeleteSubList(input, 0, 0), exclude);
    return wasListCountExclude(llDeleteSubList(input, 0, 0), exclude);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
list wasListMerge(list l, list m, string merge) {
    if(llGetListLength(l) == 0 && llGetListLength(m) == 0) return [];
    string a = llList2String(m, 0);
    if(a != merge) return [ a ] + 
        wasListMerge(l, llDeleteSubList(m, 0, 0), merge);
    return [ llList2String(l, 0) ] + 
        wasListMerge(
            llDeleteSubList(l, 0, 0), 
            llDeleteSubList(m, 0, 0), 
            merge
        );
}
 
// for notecard reading
integer line = 0;
// time to execute
list time = [];
// message to send
list exec = [];
// notecard schedule
list card = [];
// subject to send
list subj = [];
// inventory notecards
list notes = [];
// current notecard
string note = "";
key agent = NULL_KEY;
// minute store
integer minute = -1;
 
default {
    state_entry() {
        // allow notecards to be dropped
        llAllowInventoryDrop(FALSE);
        // get all the inventory notecards
        integer i = llGetInventoryNumber(INVENTORY_NOTECARD)-1;
        if(i == -1) {
            llSay(0, "No notecards found, idling...");
            return;
        }
        do {
            notes += llGetInventoryName(INVENTORY_NOTECARD, i);
        } while(--i>-1);
        note = llList2String(notes, 0);
        notes = llDeleteSubList(notes, 0, 0);
        line = 0;
        llSay(0, "Reading notecard: " + note);
        llGetNotecardLine(note, line);
    }
    dataserver(key id, string data) {
        if(data == EOF) {
            // if we have read all the notecards, 
            // start processing
            if(llGetListLength(notes) == 0) {
                // check if the crons are set-up properly or bail
                if(llGetListLength(time) == 0 || 
                    llGetListLength(exec) == 0 || 
                    llGetListLength(time) != llGetListLength(exec)) {
                    llSay(0, "No valid schedules found...");
                    return;
                }
                llSay(0, "All notecards have been read...");
                state cron;
            }
            // otherwise permute
            note = llList2String(notes, 0);
            notes = llDeleteSubList(notes, 0, 0);
            line = 0;
            llSay(0, "Reading notecard: " + note);
            llGetNotecardLine(note, line);
            return;
        }
        if(data == "") jump continue;
        integer i = llSubStringIndex(data, "#");
        if(i != -1) data = llDeleteSubString(data, i, -1);
        if(data == "") jump continue;
        list data = llParseString2List(data, [" "], []);
        // *  *  *  *  * message to send to the link-set
        // sanity check a little
        if(llGetListLength(data) < 6 ||
            (llList2String(data, 2) != "*" && llList2Integer(data, 2) == 0) ||
            (llList2String(data, 3) != "*" && llList2Integer(data, 3) == 0)) jump continue;
        list t = llList2List(data, 0, 4);
        // normalize 0x
        i = llGetListLength(t)-1;
        do {
            if(llList2String(t, i) == "*") jump wildcard;
            t = llListReplaceList(t, [ llList2Integer(t, i) ], i, i);
@wildcard;
        } while(--i>-1);
        list ms = llParseString2List(llDumpList2String(llList2List(data, 5, -1), " "), ["|"], []);
        // check if subject and message are present
        if(llGetListLength(ms) != 2) jump continue;
        time += llDumpList2String(t, " ");
        subj += llList2String(ms, 0);
        exec += llList2String(ms, 1);
        card += note;
@continue;
        llGetNotecardLine(note, ++line);
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
///////////////////////////////////////////////////////////////////////////
//         [WaS-K] Cron @ http://grimore.org/secondlife/cron             //
///////////////////////////////////////////////////////////////////////////
state cron {
    state_entry() {
        // cron runs every minute so we bind the event handler
        // to the second in order to be precisely on the minute
        llSetTimerEvent(1);
        llSay(0, "Scheduler activated...");
    }
    touch_start(integer num) {
        // not part of the same group, so bail
        if(llDetectedGroup(0) == FALSE) return;
        integer comChannel = (integer)("0x8" + llGetSubString(llGetKey(), 0, 6));
        llListen(comChannel, "", "", "");
        llDialog(llDetectedKey(0), "\n                Welcome to the Scheduler.\nCreated in 2014 by Wizardry and Steamworks\n           10 September 2014: Version: 1.0\n\n", ["⌚ Show", "⎀ Add", "⎙ Remove"], comChannel);
    }
    listen(integer channel, string name, key toucher, string message) {
        if(message == "⌚ Show") {
            llInstantMessage(toucher, "-----------------------------------------");
            integer i = llGetListLength(time)-1;
            do {
                llInstantMessage(toucher, llList2String(card, i) + " ◉ " + llList2String(time, i) + " ▶︎ " + "⌠" + llList2String(subj, i) + "⌡" + " " + llList2String(exec, i));
            } while(--i>-1);
            llInstantMessage(toucher, "-----------------------------------------");
            return;
        }
        if(message == "⎀ Add") {
            llSetTimerEvent(0);
            agent = toucher;
            state add;
        }
        if(message == "⎙ Remove") {
            llSetTimerEvent(0);
            agent = toucher;
            state remove;
        }
    }
    timer() {
        // build the current date
        list stamp = llParseString2List(
            wasUnixTimeToStamp(llGetUnixTime() - ((integer) llGetGMTclock() - (integer) llGetWallclock())),
            ["-",":","T"],[""]
        );
 
        // cron only once
        if(llList2Integer(stamp, 4) == minute) return;
 
        list ymd = llList2List(stamp, 0, 2);
        integer weekDay = llListFindList(
            // convert to cron syntax where Sunday counts as day 0 or 7
            [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ],
            [
                wasDayOfWeek(
                    llList2Integer(ymd, 0), 
                    llList2Integer(ymd, 1), 
                    llList2Integer(ymd, 2)
                )
            ]
        );
 
        // minute, hour, day, month, day of week
        list date  = wasListReverse(llList2List(stamp, 1, -2)) + weekDay;
        integer i = llGetListLength(date)-1;
        // normalize 0x
        do {
            date = llListReplaceList(date, [ llList2Integer(date, i) ], i, i);
        } while(--i>-1);
 
        // check if it is time
        list times = time;
        list execs = exec;
        list subjs = subj;
        do {
            list cron = llParseString2List(llList2String(times, 0), [" "], []);
            do {
                if(
                    // treat 0 or 7 weekday as Sunday
                    (
                        llGetListLength(cron) == 1 && 
                        llList2Integer(date, 0) == 0 && 
                        llList2Integer(cron, 0) != 0 && 
                        llList2Integer(cron, 0) != 7
                    ) ||
                    (
                        llGetListLength(cron) != 1 && 
                        llList2String(date, 0) != 
                        llList2String(cron, 0) && 
                        llList2String(cron, 0) != "*"
                    )
                ) jump continue;
                date = llDeleteSubList(date, 0, 0);
                cron = llDeleteSubList(cron, 0, 0);
            } while(llGetListLength(cron));
            llMessageLinked(LINK_SET, 0, llList2String(execs, 0), llList2String(subjs, 0));
@continue;
            times = llDeleteSubList(times, 0, 0);
            execs = llDeleteSubList(execs, 0, 0);
            subjs = llDeleteSubList(subjs, 0, 0);
        } while(llGetListLength(times));
 
        // cron only once
        minute = llList2Integer(stamp, 4);
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
///////////////////////////////////////////////////////////////////////////
//                          Remove Notecard                              //
///////////////////////////////////////////////////////////////////////////
state remove {
    state_entry() {
        // get all the inventory notecards
        integer i = llGetInventoryNumber(INVENTORY_NOTECARD)-1;
        do {
            notes += llGetSubString(llGetInventoryName(INVENTORY_NOTECARD, i), 0, 8);
        } while(--i>-1);
        integer channel = (integer)("0x8" + llGetSubString(llGetKey(), 0, 6));
        llListen(channel, "", "", "");
        llDialog(agent, "\n                Welcome to the Scheduler.\nCreated in 2014 by Wizardry and Steamworks\n           10 September 2014: Version: 1.0\n\n", wasDialogMenu(notes, ["⟵ Back", "⏏ Exit", "Next ⟶"], ""), channel);
        llSetTimerEvent(60);
    }
    listen(integer channel, string name, key id, string message) {
        if(message == "⟵ Back") {
            llSetTimerEvent(60);
            llDialog(id, "\n                Welcome to the Scheduler.\nCreated in 2014 by Wizardry and Steamworks\n           10 September 2014: Version: 1.0\n\n", wasDialogMenu(notes, ["⟵ Back", "⏏ Exit", "Next ⟶"], "<"), -10);
            return;
        }
        if(message == "Next ⟶") {
            llSetTimerEvent(60);
            llDialog(id, "\n                Welcome to the Scheduler.\nCreated in 2014 by Wizardry and Steamworks\n           10 September 2014: Version: 1.0\n\n", wasDialogMenu(notes, ["⟵ Back", "⏏ Exit", "Next ⟶"], ">"), -10);
            return;
        }
        if(message == "⏏ Exit") {
            llInstantMessage(id, "Resuming operations...");
            llResetScript();
        }
        integer i = llGetInventoryNumber(INVENTORY_NOTECARD)-1;
        do {
            string name = llGetInventoryName(INVENTORY_NOTECARD, i);
            if(llSubStringIndex(name, message) != -1) {
                llInstantMessage(id, "Deleting notecard: " + message);
                llRemoveInventory(name);
                integer j = llGetListLength(notes)-1;
                do {
                    note = llList2String(notes, j);
                    if(llSubStringIndex(name, note) != -1) {
                        notes = llDeleteSubList(notes, j, j);
                        jump menu;
                    }
                } while(--j>-1);
            }
        } while(--i>-1);
@menu;
        llSetTimerEvent(60);
        llDialog(id, "\n                Welcome to the Scheduler.\nCreated in 2014 by Wizardry and Steamworks\n           10 September 2014: Version: 1.0\n\n", wasDialogMenu(notes, ["⟵ Back", "⏏ Exit", "Next ⟶"], ""), channel);
    }
    timer() {
        llInstantMessage(agent, "Dialog expired, resuming operations...");
        llResetScript();
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
///////////////////////////////////////////////////////////////////////////
//                            Add Notecard                               //
///////////////////////////////////////////////////////////////////////////
state add {
    state_entry() {
        llAllowInventoryDrop(TRUE);
        llInstantMessage(agent, "You have 60 seconds to drag and drop a notecard onto the primitive. Touch the primitive to cancel.");
        llSetTimerEvent(60);
    }
    touch_start(integer num) {
        llInstantMessage(agent, "Aborted, resuming operations...");
        llResetScript();
    }
    timer() {
        llInstantMessage(agent, "Resuming operations...");
        llResetScript();
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}