///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2012 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                            CONFIGURATION                              //
///////////////////////////////////////////////////////////////////////////
 
// The UUID / Key of the scripted agent.
string CORRADE = "18a56ee7-4330-48f9-835a-f7839a32cdbf";
 
// The name of a configured group.
string GROUP = "My Group";
 
// The password for the configured group.
string PASSWORD = "mypassword";
 
// How many seconds per letter are allowed before dropping a hint.
integer TIME_PER_LETTER = 5;
 
// The prefix used to trigger trivia commands.
string COMMAND_PREFIX = "@";
 
// A string to be used as a notification tag.
string NOTIFY_TAG = "TjP9Nd1boQ";
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
list wasDualQuicksort(list a, list b) {
    if(llGetListLength(a) <= 1) return a+b;
 
    integer pivot_a = llList2Integer(a, 0);
    a = llDeleteSubList(a, 0, 0);
    string pivot_b = llList2String(b, 0);
    b = llDeleteSubList(b, 0, 0);
 
    list less = [];
    list less_b = [];
    list more = [];
    list more_b = [];
 
    do {
        if(llList2Integer(a, 0) > pivot_a) {
            less += llList2List(a, 0, 0);
            less_b += llList2List(b, 0, 0);
            jump continue;
        }
        more += llList2List(a, 0, 0);
        more_b += llList2List(b, 0, 0);
@continue;
        a = llDeleteSubList(a, 0, 0);
        b = llDeleteSubList(b, 0, 0);
    } while(llGetListLength(a));
    return wasDualQuicksort(less, less_b) + [ pivot_a ] + [ pivot_b ] + wasDualQuicksort(more, more_b);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseStringKeepNulls(data, ["&", "="], []);
    integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]);
    if(i != -1) return llList2String(a, 2*i+1);
    return "";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueEncode(list data) {
    integer i = llGetListLength(data);
    if (i % 2 != 0 || i == 0) return "";
    --i;
    do {
        data = llListInsertList(
            llDeleteSubList(
                data, 
                i-1, 
                i
            ),
            [ llList2String(data, i-1) + "=" + llList2String(data, i) ], 
            i-1
        );
        i -= 2;
    } while(i > 0);
    return llDumpList2String(data, "&");
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// unescapes a string in conformance with RFC1738
string wasFormDecode(string i) {
    return llUnescapeURL(
        llDumpList2String(
            llParseString2List(
                llDumpList2String(
                    llParseString2List(
                        i, 
                        ["+"], 
                        []
                    ), 
                    " "
                ), 
                ["%0D%0A"], 
                []
            ), 
            "\n"
        )
    );
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// escapes a string in conformance with RFC1738
string wasFormEncode(string i) {
    string o = "";
    do {
        string c = llGetSubString(i, 0, 0);
        i = llDeleteSubString(i, 0, 0);
        if(c == "") jump continue;
        if(c == " ") {
            o += "+";
            jump continue;
        }
        if(c == "\n") {
            o += "%0D" + llEscapeURL(c);
            jump continue;
        }
        o += llEscapeURL(c);
@continue;
    } while(i != "");
    return o;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasListToCSV(list l) {
    list v = [];
    do {
        string a = llDumpList2String(
            llParseStringKeepNulls(
                llList2String(
                    l, 
                    0
                ), 
                ["\""], 
                []
            ),
            "\"\""
        );
        if(llParseStringKeepNulls(
            a, 
            [" ", ",", "\n", "\""], []
            ) != 
            (list) a
        ) a = "\"" + a + "\"";
        v += a;
        l = llDeleteSubList(l, 0, 0);
    } while(l != []);
    return llDumpList2String(v, ",");
}
 
list cards = [];
string card = "";
integer cardIterator = 0;
integer lineIterator = 0;
list categories = [];
string category = "";
string select = "";
string question = "";
string clue = "";
list store = [];
list scores = [];
list names = [];
string name = "";
integer seconds = 0;
string URL = "";
integer running = FALSE;
key dataRequest = NULL_KEY;
 
default {
    state_entry() {
        // DEBUG
        llOwnerSay("Please wait, reading brains...");
 
        integer i = llGetInventoryNumber(INVENTORY_NOTECARD)-1;
        do {
            list brainCard = llParseString2List(
                llGetInventoryName(
                    INVENTORY_NOTECARD, 
                    i
                ), 
                ["_"], 
                [""]
            );
            if(llList2String(brainCard, 0) == "Trivia" && 
                llList2String(brainCard, 1) == "Brain") {
                cards += llGetInventoryName(INVENTORY_NOTECARD, i);
            }
        } while(--i > -1);
 
        if(llGetListLength(cards) == 0) {
            // DEBUG
            llOwnerSay("Failed to find any trivia brains!");
            return;
        }
 
        // DEBUG
        llOwnerSay("Categorizing brains...");
        cardIterator = llGetListLength(cards)-1;
        card = llList2String(cards, cardIterator);
 
        // DEBUG
        llOwnerSay("Reading brain: " + card);
 
        lineIterator = 0;
        llGetNotecardLine(card, lineIterator);
    }
    dataserver(key id, string data) {
        if(data == EOF) {
            // If all cards have been read, jump to the next state.
            if(cardIterator-- == 0) {
                // DEBUG
                llOwnerSay("All brain cards have been read.");
                state url;
            }
 
            // Select the next card.
            card = llList2String(cards, cardIterator);
 
            // DEBUG
            llOwnerSay("Reading brain: " + card);
 
            lineIterator = 0;
            llGetNotecardLine(card, lineIterator);
            return;
        }
 
        category = llList2String(
                llParseString2List(
                    llList2String(
                        llParseString2List(
                            data, 
                            ["//"], 
                            [""]), 
                        0
                    ), 
                    ["/"], 
                    [""]
                ), 
            0
        );
 
        // Broken line in notecard brain.
        if(category == "") {
            // DEBUG
            llOwnerSay("Skipping brain line without category in " + 
                card + 
                " on line " + 
                (string)lineIterator
            );
            llGetNotecardLine(card, ++lineIterator);
            return;
        }
 
        // If category has not been added yet then add it.          
        if(llListFindList(categories, (list)category) == -1) {
            categories += category;
        }
 
        llGetNotecardLine(card, ++lineIterator);
    }
    changed(integer change) {
        if(!(change & (CHANGED_INVENTORY | CHANGED_OWNER))) {
            return;
        }
        llResetScript();
    }
    on_rez(integer param) {
        llResetScript();
    }
}
 
state url {
    state_entry() {
        // DEBUG
        llOwnerSay("Requesting LSL URL...");
        llReleaseURL(URL);
        llRequestURL();
    }
    http_request(key id, string method, string body) {
        if(method != URL_REQUEST_GRANTED) {
            // DEBUG
            llOwnerSay("LSL URL reqest failed...");
            llResetScript();
        }
 
        URL = body;
        state rebind;
    }
}
 
state rebind {
    state_entry() {
        // DEBUG
        llOwnerSay("Removing group notification...");
        llMessageLinked(LINK_THIS, 0,
            wasKeyValueEncode(
                [
                    "command", "notify",
                    "group", wasFormEncode(GROUP),
                    "password", wasFormEncode(PASSWORD),
                    "action", "remove",
                    "tag", wasFormEncode(NOTIFY_TAG),
                    // afterburn
                    "aftAction", "remove",
                    "callback", wasFormEncode(URL)
                ]
            ),
            CORRADE
        );
        llSetTimerEvent(60);
    }
    http_request(key id, string method, string body) {
        llHTTPResponse(id, 200, "OK");
 
        if(wasKeyValueGet("command", body) != "notify") {
            return;
        }
 
        if(wasKeyValueGet("success", body) != "True") {
            // DEBUG
            llOwnerSay("Cannot rebind group notifications: " + 
                wasKeyValueGet(
                    "error", 
                    body
                )
            );
 
            state url;
        }
 
        if(wasKeyValueGet("success", body) == "True" && 
            wasKeyValueGet("aftAction", body) == "remove") {
            llMessageLinked(LINK_THIS, 0,
                wasKeyValueEncode(
                    [
                        "command", "notify",
                        "group", wasFormEncode(GROUP),
                        "password", wasFormEncode(PASSWORD),
                        "action", "set",
                        "tag", wasFormEncode(NOTIFY_TAG),
                        "type", "group",
                        // afterburn
                        "aftAction", "set",
                        "URL", wasFormEncode(URL),
                        "callback", wasFormEncode(URL)
                    ]
                ),
                CORRADE
            );
 
            return;
        }
 
        // DEBUG
        llOwnerSay("Notification bound successfully!");
        state group_messages;
    }
    timer() {
        // DEBUG
        llOwnerSay("Timeout binding to group notification.");
        state url;
    }
    changed(integer change) {
        if(!(change & (CHANGED_INVENTORY | CHANGED_OWNER))) {
            return;
        }
 
        llResetScript();
    }
    on_rez(integer param) {
        llResetScript();
    }
    state_exit() {
        llSetTimerEvent(0);
    }
}
 
state group_messages {
    state_entry() {
        // If no category has been selected, then stop and wait for commands.
        if(running == FALSE) {
            // DEBUG
            llOwnerSay("Waiting for command...");
            return;
        }
 
        // A category and question has been selected so send the message to the group.
        llMessageLinked(LINK_THIS, 0,
            wasKeyValueEncode(
                [
                    "command", "tell",
                    "group", wasFormEncode(GROUP),
                    "password", wasFormEncode(PASSWORD),
                    "entity", "group",
                    "message", wasFormEncode("[" + category + "]: " + question)
                ]
            ),
            CORRADE
        );
 
        seconds = llStringLength(clue);
        llSetTimerEvent(TIME_PER_LETTER);
    }
    http_request(key id, string method, string body) {
        llHTTPResponse(id, 200, "OK");
 
        // Skip messages that are not for the configured group.
        if(wasFormDecode(wasKeyValueGet("group", body)) != GROUP) {
            return;
        }
 
        // Ignore self messages.
        if(wasKeyValueGet("agent", body) == CORRADE) {
            return;
        }
 
        // Skip messages that are not trivia commands.
        string message = wasFormDecode(wasKeyValueGet("message", body));
        if(llGetSubString(message, 0, 0) != COMMAND_PREFIX) {
            jump interpret_answer;
        }
 
        list data = llParseString2List(
            message,
            [" "], 
            []
        );
 
        // Ignore non-trivia commands.
        if(llList2String(data, 0) != "@trivia") {
            return;
        }
 
        if(llList2String(data, 1) == "start") {
            // If no category has been selected then set the category to any.
            if(llListFindList(categories, (list)llList2String(data, 2)) == -1) {
                select = "any";
            }
 
            running = TRUE;
            state select_question;
        }
 
        if(llList2String(data, 1) == "help") {
            llMessageLinked(LINK_THIS, 0,
                wasKeyValueEncode(
                    [
                        "command", "tell",
                        "group", wasFormEncode(GROUP),
                        "password", wasFormEncode(PASSWORD),
                        "entity", "group",
                        "message", wasFormEncode("syntax: @trivia [start <category>|stop|categories|help]")
                    ]
                ),
                CORRADE
            );
 
            return;
        }
 
        if(llList2String(data, 1) == "categories") {
            llMessageLinked(LINK_THIS, 0,
                wasKeyValueEncode(
                    [
                        "command", "tell",
                        "group", wasFormEncode(GROUP),
                        "password", wasFormEncode(PASSWORD),
                        "entity", "group",
                        "message", wasFormEncode(llDumpList2String(categories, ", "))
                    ]
                ),
                CORRADE
            );
 
            return;
        }
 
        if(llList2String(data, 1) == "stop") {
            // DEBUG
            llOwnerSay("Stopping trivia...");
 
            llSetTimerEvent(0);
            running = FALSE;
 
            llMessageLinked(LINK_THIS, 0,
                wasKeyValueEncode(
                    [
                        "command", "tell",
                        "group", wasFormEncode(GROUP),
                        "password", wasFormEncode(PASSWORD),
                        "entity", "group",
                        "message", wasFormEncode("Trivia stopped.")
                    ]
                ),
                CORRADE
            );
 
            return;
        }
 
        return;
 
@interpret_answer;
 
        if(running == FALSE) {
            return;
        }
 
        integer i = llGetListLength(store) - 1;
        string answer = "";
        do {
            answer = llList2String(store, i);
 
            if(llStringLength(answer) == llStringLength(message) && 
                !(llToUpper(answer) != llToUpper(message))) {
                    jump correct_answer;
            }
        } while(--i > -1);
 
        return;
 
@correct_answer;
 
        llSetTimerEvent(0);
 
        // Get the name of the avatar sending the message.
        name = wasFormDecode(
            wasKeyValueGet(
                "firstname", 
                body
            )
        ) + 
        " " + 
        wasFormDecode(
            wasKeyValueGet(
                "lastname", 
                body
            )
        );
 
        llMessageLinked(LINK_THIS, 0,
            wasKeyValueEncode(
                [
                    "command", "tell",
                    "group", wasFormEncode(GROUP),
                    "password", wasFormEncode(PASSWORD),
                    "entity", "group",
                    "message", wasFormEncode(name + " answered the question correctly: " + answer + " is a correct answer!")
                ]
            ),
            CORRADE
        );
 
        i = llListFindList(names, (list)name);
        if(i == -1) {
            names = llListInsertList(names, (list)name, 0);
            scores = llListInsertList(scores, (list)1, 0);
            jump compute_score;
        }
 
        integer score = llList2Integer(scores, i) + 1;
        scores = llListReplaceList(scores, (list)score, i, i);
 
@compute_score;
 
        llMessageLinked(LINK_THIS, 0,
            wasKeyValueEncode(
                [
                    "command", "tell",
                    "group", wasFormEncode(GROUP),
                    "password", wasFormEncode(PASSWORD),
                    "entity", "group",
                    "message", wasFormEncode("Score: " + 
                        llDumpList2String(
                            wasDualQuicksort(
                                scores,
                                names
                            ),
                            " -> "
                        )
                    )
                ]
            ),
            CORRADE
        );
 
        state select_question;
    }
    timer() {
        if(running == FALSE) {
            return;
        }
 
        integer i = (integer)llFrand(llStringLength(clue));
        do {
            if(--i < 0) {
                i = llStringLength(clue);
            }
        } while(llGetSubString(clue, i, i) != "-");
 
        //
        // f = a + i + b
        //       
        //      / 0..i - 1 iff. i - 1 >= 0
        // a = {
        //      \ emp iff. i - 1 < 0
        //
        //
        //      / 0..i + 1 iff. i + 1 < len(b)
        // b = {
        //      \ emp iff. i + 1 >= len(b)
        //
        //
        string answer = llList2String(store, 0);
        clue = llList2String(
                [ 
                    llGetSubString(clue, 0, i - 1), 
                    ""
                ], 
                i - 1 < 0
            ) +
            llGetSubString(answer, i, i) +
            llList2String(
                [
                    llGetSubString(clue, i + 1, -1), 
                    "" 
                ], 
                i + 1 >= llStringLength(clue)
        );
 
        llMessageLinked(LINK_THIS, 0,
            wasKeyValueEncode(
                [
                    "command", "tell",
                    "group", wasFormEncode(GROUP),
                    "password", wasFormEncode(PASSWORD),
                    "entity", "group",
                    "message", wasFormEncode("Clue: " + "[" + clue +  "]")
                ]
            ),
            CORRADE
        );
 
        // The question has not been answered yet so return.
        if(--seconds != 0) {
            return;
        }
 
        llMessageLinked(LINK_THIS, 0,
            wasKeyValueEncode(
                [
                    "command", "tell",
                    "group", wasFormEncode(GROUP),
                    "password", wasFormEncode(PASSWORD),
                    "entity", "group",
                    "message", wasFormEncode("A correct answer would have been: " + answer + ".")
                ]
            ),
            CORRADE
        );
 
        state select_question;
    }
    changed(integer change) {
        if(!(change & (CHANGED_INVENTORY | CHANGED_OWNER))) {
            return; 
        }
 
        llResetScript();
    }
    on_rez(integer param) {
        llResetScript();
    }
    state_exit() {
        llSetTimerEvent(0);
    }
}
 
state select_question {
    state_entry() {
        // DEBUG
        llOwnerSay("Selecting a line...");
 
        if(cardIterator < 0) {
            cardIterator = (integer)llFrand(llGetListLength(cards));
        }
 
        card = llList2String(cards, cardIterator);
        dataRequest = llGetNumberOfNotecardLines(card);
    }
    dataserver(key id, string data) {
        if(dataRequest == id) {
            lineIterator = (integer)llFrand((integer)data);
 
            // DEBUG
            llOwnerSay("Selecting a question...");
 
            card = llList2String(cards, cardIterator);
            llGetNotecardLine(card, lineIterator);
 
            return;
        }
 
        if(data == EOF) {
            --cardIterator;
 
            if(cardIterator < 0) {
                cardIterator = (integer)llFrand(llGetListLength(cards));
            }
 
            card = llList2String(cards, cardIterator);
            dataRequest = llGetNumberOfNotecardLines(card);
 
            return;
        }
 
        // Skip empty notecard lines.
        if(data == "") {
            llGetNotecardLine(card, ++lineIterator);
 
            return;
        }
 
        list q = llParseString2List(
            data, 
            ["//"], 
            [""]
        );
 
        list c = llParseString2List(
            llList2String(q, 0), 
            ["/"], 
            [""]
        );
 
        if(select != "any" && 
            select != llList2String(c, 0)) {
            // DEBUG
            llOwnerSay("Question for category not found, continuing...");
 
            llGetNotecardLine(card, ++lineIterator);
 
            return;
        }
 
        category = llList2String(c, 0);
        question = llList2String(q, 1);
 
        // Build a clue string.
        c = llDeleteSubList(c, 0, 0);
        string head = llList2String(c, 0);
        integer i = llStringLength(head) - 1;
        clue = "";
        do {
            clue += "-";
        } while(--i > -1);
 
        // DEBUG
        llOwnerSay("Generated clue: " + clue);
 
        store = (list)head;
 
        do {
            store += llList2String(c, 0);
            c = llDeleteSubList(c, 0, 0);
        } while(llGetListLength(c) != 0);
 
        ++lineIterator;
 
        // DEBUG
        llOwnerSay("Question selected...");
 
        state group_messages;
    }
    changed(integer change) {
        if(!(change & (CHANGED_INVENTORY | CHANGED_OWNER))) {
            return;
        }
 
        llResetScript();
    }
    on_rez(integer param) {
        llResetScript();
    }
}