// Onefang's NPC menu version 1.0.
// Requires NPC tool, and onefang's utilities scripts.

// Copyright (C) 2013 David Seikel (onefang rejected).
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies of the Software and its Copyright notices. In addition publicly
// documented acknowledgment must be given that this software has been used if no
// source code of this software is made available publicly. This includes
// acknowledgments in either Copyright notices, Manuals, Publicity and Marketing
// documents or any documentation provided with any product containing this
// software. This License does not apply to any software that links to the
// libraries provided by this software (statically or dynamically), but only to
// the software provided.
//
// Please see the COPYING-PLAIN for a plain-english explanation of this notice
// and it's intent.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

list chatters = [];   // npc key, relay channel.
integer CHAT_KEY     = 0;
integer CHAT_CHANNEL = 1;
integer CHAT_STRIDE  = 2;

list  sensorRequests = [];  // name to search for, type of search, range, command, user key, npc key, title for menu, agents flag, every flag
integer sensorInFlight = FALSE;

// NPC commands.
string NPC_ANIMATE  = "animate";    // NPC name, animation name
string NPC_ATTENTION= "attention";  // NPC name.
string NPC_CHANGE   = "change";     // NPC name, NPC (notecard) name.
string NPC_CLONE    = "clone";      // Avatar's name.
string NPC_COME     = "come";       // NPC name.
string NPC_COMPLETE = "complete";   // NPC name.
string NPC_CREATE   = "create";     // NPC (notecard) name, (optional) position vector.
string NPC_DELETE   = "delete";     // NPC name.
string NPC_DISMISSED= "dismissed";  // NPC name.
string NPC_FOLLOW   = "follow";     // NPC name, (optional) distance (float or vector).
string NPC_FLY      = "fly";        // NPC name, position vector or object/agent name/key.
string NPC_GO       = "go";         // NPC name, position vector or object/agent name/key.
string NPC_LAND     = "land";       // NPC name, position vector or object/agent name/key.
string NPC_LINK     = "link";       // Link number, number, string, (optional) key
string NPC_LISTEN   = "listen";     // new command channel
string NPC_LOCATE   = "locate";     // NPC name
string NPC_NUKE     = "nuke";       // No arguments.
string NPC_ROTATE   = "rotate";     // NPC name, Z rotation in degrees.
string NPC_SAY      = "say";        // NPC name, thing to say, or relay channel.
string NPC_SCRIPT   = "script";     // Script notecard name.
string NPC_SHOUT    = "shout";      // NPC name, thing to shout, or relay channel.
string NPC_SIT      = "sit";        // NPC name, object to sit on.
string NPC_SLEEP    = "sleep";      // Seconds.
string NPC_STALK    = "stalk";      // NPC name, avatar name, (optional) distance (float or vector).
string NPC_STAND    = "stand";      // NPC name.
string NPC_STOP     = "stop";       // NPC name.
string NPC_STOPANIM = "stopanim";   // NPC name, animation name
string NPC_TOUCH    = "touch";      // NPC name, object to touch.
string NPC_WHISPER  = "whisper";    // NPC name, thing to whisper, or relay channel.

string NPC_MAIN     = "Main";   // TODO - does not really matter what this is I think, so use it as the registered menu button.
string NPC_PICK     = "Pick";
string NPC_NPC      = "NPC";
string NPC_RELAY    = "Relay";

string NPC_NPC_EXT = ".avatar";
string NPC_SCRIPT_EXT = ".npc";
string NPC_BACKUP_CARD = "Restore";
string NPC_RECORD_CARD = "Recorded";

integer NPC_RECORD      = -100;
integer NPC_RECORD_STOP = -101;
integer NPC_NEW_NPC     = -102;
integer NPC_ADD_CHATTER = -103;
integer NPC_DEL_CHATTER = -104;


// Stuff for onefangs common utilities.
key NPCscriptKey;
key     scriptKey;

string  UTILITIES_SCRIPT_NAME       = "onefang's utilities";
string  LIST_SEP                    = "$!#";           // Used to seperate lists when sending them as strings.
integer UTILITIES_RESET             = -1;
integer UTILITIES_RESET_DONE        = -2;
integer UTILITIES_READ              = -3;
integer UTILITIES_READ_DONE         = -4;
integer UTILITIES_SUBSTITUTE        = -5;
integer UTILITIES_SUBSTITUTE_DONE   = -6;
integer UTILITIES_NEXT_WORD         = -7;
integer UTILITIES_NEXT_WORD_DONE    = -8;
integer UTILITIES_PAYMENT_MADE      = -9;
integer UTILITIES_PAYMENT_MADE_DONE = -10;
integer UTILITIES_MENU              = -11;
integer UTILITIES_MENU_DONE         = -12;
integer UTILITIES_WARP              = -13;
integer UTILITIES_WARP_DONE         = -14;
integer UTILITIES_AVATAR_KEY        = -15;
integer UTILITIES_AVATAR_KEY_DONE   = -16;
integer UTILITIES_CHAT              = -17;
integer UTILITIES_CHAT_DONE         = -18;
integer UTILITIES_CHAT_FAKE         = -19;
integer UTILITIES_CHAT_FAKE_DONE    = -20;


showMenu(key id, string type, string title, list buttons, string extra)
{
    llMessageLinked(LINK_SET, UTILITIES_MENU, llDumpList2String([id, type, title] + buttons, LIST_SEP), (key) scriptKey + "|" + extra);
}

NPCmenu(key id, key npc)
{
    string name = llKey2Name(npc);

    if ((0 == osIsUUID(npc)) || (npc == NULL_KEY) || ("" == name))
    {
        showMenu(id, (string) INVENTORY_NONE, "Main NPC menu.", 
            [
            "clone avatar..", "create NPC..", "run script..",
            "nearby NPCs..", "local NPCs..", "NPCs in sim..",
            "backup NPCs", "nuke NPCs", "restore NPCs",
            "start recording", "stop recording"
            ], NPC_MAIN);
    }
    else
    {
        integer found = llListFindList(chatters, [npc]);
        string chat = "";

        if (0 <= found)
            chat = "\nUse /" + llList2String(chatters, found + CHAT_CHANNEL) + " to relay chat to " + llKey2Name(npc);
        showMenu(id, (string) INVENTORY_NONE, "Play with " + llKey2Name(npc) + " :" + chat,
            [
            "change..", "chat relay..", "come here",
            "go to..", "fly to..", "land at..",
            "follow me", "stalk them..", "stop moving",
            "sit..", NPC_STAND,
            NPC_LOCATE, "take controls", "touch..",
            "start animation..", "stop animation..",
            NPC_DELETE
            ], NPC_NPC + "|" + npc);
    }
}

startSensor(key user, key npc, string name, integer type, float range, 
    string command, string title, integer agents, integer everything)
{
    if ((AGENT == type) && (range > 9999.9999))   // Assumes we are only looking for NPCs.
    {
        integer i;
        list menu = [];
        list avatars = osGetAvatarList();   // Strided list, UUID, position, name.
        integer length = llGetListLength(avatars);

        for (i = 0; i < length; i++)
        {
            key this = (key) llList2String(avatars, i * 3);

            if (osIsNpc(this))
                menu += [llKey2Name(this) + "|" + (string) this];
        }
        if (llGetListLength(menu) > 0)
            showMenu(user, (string) INVENTORY_NONE, "Choose NPC :", menu, NPC_PICK);
    }
    else
    {
        sensorRequests += [name, type, range, command, user, npc, title, agents, everything];
        nextSensor();
    }
}

nextSensor()
{
    if (sensorInFlight)
        return;
    if (0 < llGetListLength(sensorRequests))
    {
        string  name    = llList2String(sensorRequests, 0);
        integer type    = llList2Integer(sensorRequests, 1);
        float   range   = llList2Float(sensorRequests, 2);
        string  command = llList2String(sensorRequests, 3);
        key     user    = llList2String(sensorRequests, 4);
        key     npc     = llList2String(sensorRequests, 5);
        string  title   = llList2String(sensorRequests, 6);
        integer agents  = llList2Integer(sensorRequests, 7);
        integer every   = llList2Integer(sensorRequests, 8);

        sensorInFlight = TRUE;
        llSensor(name, "", type, range, TWO_PI);
    }
}

sendCommand(key user, string command)
{
    // Work around the other script getting reset later, with a fresh key
    NPCscriptKey = llGetInventoryKey("NPC tool");
    llMessageLinked(LINK_SET, UTILITIES_CHAT_FAKE, llDumpList2String([0, llKey2Name(user), user, command], LIST_SEP), NPCscriptKey);
}

init()
{
    scriptKey = llGetInventoryKey(llGetScriptName());
    // Register our interest in touch menus.
    llMessageLinked(LINK_SET, UTILITIES_MENU, llDumpList2String([NULL_KEY, INVENTORY_NONE, "NPC tool|" + llGetScriptName()], LIST_SEP), (key) scriptKey);
}


default
{
    state_entry()
    {
        init();
    }

    on_rez(integer param)
    {
        init();
    }

    attach(key attached)
    {
        init();
    }

    link_message(integer sender_num, integer num, string value, key id)
    {
        list keys = llParseStringKeepNulls((string) id, ["|"], []);
        string extra = llList2String(keys, 1);

        id = (key) llList2String(keys, 0);
        // Work around the other script getting reset later, with a fresh key
        NPCscriptKey = llGetInventoryKey("NPC tool");
//llSay(0, "id = " + (string) id + " extra = " + extra + " VALUE " + value);
        if (UTILITIES_RESET_DONE == num)
            init();
        else if ((NPC_NEW_NPC == num) && (NPCscriptKey == id))
        {
            list args = llParseString2List(value, ["|"], []);

            NPCmenu(llList2String(args, 0), llList2String(args, 1));
        }
        else if ((NPC_ADD_CHATTER == num) && (NPCscriptKey == id))
        {
            list args = llParseString2List(value, ["|"], []);
            key npc = llList2String(args, 0);
            integer channel = llList2Integer(args, 1);
            integer found = llListFindList(chatters, [npc]);

            if (0 <= found)
                chatters = llDeleteSubList(chatters, found, found + CHAT_STRIDE - 1);
            chatters += [npc, channel];
        }
        else if ((NPC_DEL_CHATTER == num) && (NPCscriptKey == id))
        {
            integer found = llListFindList(chatters, [value]);

            if (0 <= found)
                chatters = llDeleteSubList(chatters, found, found + CHAT_STRIDE - 1);
        }
        else if ((UTILITIES_CHAT_DONE == num) && (NPCscriptKey == id))
        {
            // incoming channel | incoming name | incoming key | incoming message | prefix | command | list of arguments | rest of message
            list    result        = llParseStringKeepNulls(value, [LIST_SEP], []);
            //integer inchannel    = llList2Integer(result, 0);
            //string  inName          = llList2String (result, 1);
            key     user        = llList2Key    (result, 2);
            //string  inMessage    = llList2String (result, 3);
            //string  prefix          = llList2String (result, 4);
            string  command        = llList2String (result, 5);
            list    arguments    = llList2List   (result, 6, -1);   // Includes "rest of message" as the last one.

            if (NPC_NUKE == command)
            {
                chatters = [];
                sensorRequests = [];
                sensorInFlight = FALSE;
            }
        }
        else if ((UTILITIES_MENU_DONE == num) && (scriptKey == id))    // Big menu button pushed
        {
            list    input = llParseStringKeepNulls(value, [LIST_SEP], []);
            key     user = (key) llList2String(input, 0);
            string  selection =  llList2String(input, 1);
            list    parts = llParseString2List(selection, ["|"], []);
            key     uuid = (key) llList2String(parts, 1);
            key     npc = (key) llList2String(keys, 2);
            list    details = llGetObjectDetails(uuid, [OBJECT_POS]);
            string  menu = extra;
//llSay(0, extra + " MENU " + value + "  KEYS " + llDumpList2String(keys, " "));

            // See if it was our top level menu requested via touch registration.
            if ("" == selection)
            {
                NPCmenu(user, NULL_KEY);
                return;
            }

            // Figure out what the user picked.
            if ((NPC_MAIN == extra) || (NPC_NPC == extra))
                menu = selection;
            // Make sure main menu items don't return to an NPC menu, by setting npc to null.
            if (NPC_MAIN == extra)
                npc = NULL_KEY;

            // Check if the NPC still exists.
            if (NPC_NPC == extra)
            {
                list npcDetails = llGetObjectDetails(npc, [OBJECT_POS]);

                if (llGetListLength(npcDetails) == 0)
                {
                    // Bail out if the NPC went AWOL.
                    npc = NULL_KEY;
                    selection = "Exit";
                }
            }

            if ("Exit" == selection)
            {
                if (NPC_NPC  == extra)  npc = NULL_KEY;
                if (NPC_MAIN == extra)  return;
            }
            // Commands.
            else if (NPC_ANIMATE       == menu)  sendCommand(user, NPC_ANIMATE  + " " + npc + " " + selection);
            else if (NPC_CHANGE        == menu)  sendCommand(user, NPC_CHANGE   + " " + npc + " " + selection);
            else if (NPC_CLONE         == menu)  sendCommand(user, NPC_CLONE    + " " + uuid);
            else if (NPC_FLY           == menu)  sendCommand(user, NPC_FLY      + " " + npc + " " + llList2String(details, 0));
            else if (NPC_GO            == menu)  sendCommand(user, NPC_GO       + " " + npc + " " + llList2String(details, 0));
            else if (NPC_LAND          == menu)  sendCommand(user, NPC_LAND     + " " + npc + " " + llList2String(details, 0));
            else if (NPC_LOCATE        == menu)  sendCommand(user, NPC_LOCATE   + " " + npc);
            else if (NPC_RELAY         == menu)  sendCommand(user, NPC_SAY      + " " + npc + " " + selection);
            else if (NPC_SCRIPT        == menu)  sendCommand(user, NPC_SCRIPT   + " " + selection);
            else if (NPC_SIT           == menu)  sendCommand(user, NPC_SIT      + " " + npc + " " + uuid);
            else if (NPC_STALK         == menu)  sendCommand(user, NPC_STALK    + " " + npc + " " + uuid);
            else if (NPC_STAND         == menu)  sendCommand(user, NPC_STAND    + " " + npc);
            else if (NPC_STOPANIM      == menu)  sendCommand(user, NPC_STOPANIM + " " + npc + " " + selection);
            else if (NPC_TOUCH         == menu)  sendCommand(user, NPC_TOUCH    + " " + npc + " " + uuid);
            else if ("come here"       == menu)  sendCommand(user, NPC_COME     + " " + npc);
            else if ("follow me"       == menu)  sendCommand(user, NPC_FOLLOW   + " " + npc);
            else if ("nuke NPCs"       == menu)  sendCommand(user, NPC_NUKE);
            else if ("restore NPCs"    == menu)  sendCommand(user, NPC_SCRIPT   + " " + NPC_BACKUP_CARD + NPC_SCRIPT_EXT);
            else if ("stop moving"     == menu)  sendCommand(user, NPC_STOP     + " " + npc);

            // Menus.
            else if ("change.."          == menu)  showMenu(user, ((string) INVENTORY_NOTECARD) + "|.+\\" + NPC_NPC_EXT,
                                                        "Choose an NPC to change to :",   [], NPC_CHANGE + "|" + npc);
            else if ("chat relay.."      == menu)  showMenu(user,  (string) INVENTORY_NONE,
                                                        "Choose a channel to relay chat from :", ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"], NPC_RELAY + "|" + npc);
            else if ("create NPC.."      == menu)  showMenu(user, ((string) INVENTORY_NOTECARD) + "|.+\\" + NPC_NPC_EXT,
                                                        "Choose an NPC to create :",      [], NPC_CREATE);
            else if ("run script.."      == menu)  showMenu(user, ((string) INVENTORY_NOTECARD) + "|.+\\" + NPC_SCRIPT_EXT,
                                                        "Choose a script to run :",       [], NPC_SCRIPT);
            else if ("start animation.." == menu)  showMenu(user,  (string) INVENTORY_ANIMATION,
                                                        "Choose an animation to start :", [], NPC_ANIMATE  + "|" + npc);
            else if ("stop animation.."  == menu)  showMenu(user,  (string) INVENTORY_ANIMATION,
                                                        "Choose an animation to stop :",  [], NPC_STOPANIM + "|" + npc);

            // Sensor menus.
            else if ("clone avatar.."  == menu)  startSensor(user, "",  "", AGENT,              256.0, NPC_CLONE,  "Choose person to clone :",  TRUE,  TRUE);
            else if ("fly to.."        == menu)  startSensor(user, npc, "", ACTIVE | PASSIVE,  1024.0, NPC_FLY,    "Choose thing to fly to :",  FALSE, TRUE);
            else if ("go to.."         == menu)  startSensor(user, npc, "", ACTIVE | PASSIVE,  1024.0, NPC_GO,     "Choose thing to go to :",   FALSE, TRUE);
            else if ("land at.."       == menu)  startSensor(user, npc, "", ACTIVE | PASSIVE,  1024.0, NPC_LAND,   "Choose thing to land at :", FALSE, TRUE);
            else if ("local NPCs.."    == menu)  startSensor(user, "",  "", AGENT,              256.0, NPC_PICK,   "Choose NPC :",              FALSE, FALSE);
            else if ("nearby NPCs.."   == menu)  startSensor(user, "",  "", AGENT,               20.0, NPC_PICK,   "Choose NPC :",              FALSE, FALSE);
            else if ("NPCs in sim.."   == menu)  startSensor(user, "",  "", AGENT,            16384.0, NPC_PICK,   "Choose NPC :",              FALSE, FALSE);
            else if ("sit.."           == menu)  startSensor(user, npc, "", ACTIVE | SCRIPTED, 1024.0, NPC_SIT,    "Choose thing to sit on :",  FALSE, TRUE);
            else if ("stalk them.."    == menu)  startSensor(user, npc, "", AGENT,             1024.0, NPC_STALK,  "Choose person to stalk :",  TRUE,  TRUE);
            else if ("touch.."         == menu)  startSensor(user, npc, "", ACTIVE | SCRIPTED, 1024.0, NPC_TOUCH,  "Choose thing to touch :",   FALSE, TRUE);

            // Misc.
            else if (NPC_PICK          == menu)  npc = uuid;
            else if ("start recording" == menu)  llMessageLinked(LINK_SET, NPC_RECORD,      "", NPCscriptKey);
            else if ("stop recording"  == menu)  llMessageLinked(LINK_SET, NPC_RECORD_STOP, "", NPCscriptKey);
            else if (NPC_CREATE   == menu)
            {
                sendCommand(user, NPC_CREATE   + " " + selection);
                // Avoid the NPCmenu() below.  An odd one out, coz the NPC wont exist yet, but we want their menu when they do exist.
                return;
            }
            else if (NPC_DELETE    == menu)
            {
                sendCommand(user, NPC_DELETE + " " + npc);
                // An odd one out, the NPC wont exist, so return to the main menu.
                npc = NULL_KEY;
            }
            else if ("backup NPCs"     == menu)
            {
                list avatars = osGetAvatarList();   // Strided list, UUID, position, name.
                list delete = [];
                list backup = [];
                integer length = llGetListLength(avatars);
                integer i;

                llRemoveInventory(NPC_BACKUP_CARD + NPC_SCRIPT_EXT);
                for (i = 0; i < length; i++)
                {
                    key this = (key) llList2String(avatars, i * 3);

                    if (osIsNpc(this))
                    {
                        string aName = llKey2Name(this);

                        osAgentSaveAppearance(this, aName + NPC_NPC_EXT);
                        delete += [NPC_DELETE + " " + aName];
                        backup += [NPC_CREATE + " " + aName + " " + llList2String(avatars, (i * 3) + 1)];
                    }
                }
                osMakeNotecard(NPC_BACKUP_CARD + NPC_SCRIPT_EXT,
                      ["script " + NPC_BACKUP_CARD + ".before" + NPC_SCRIPT_EXT]
                    + delete + backup
                    + ["script " + NPC_BACKUP_CARD + ".after" + NPC_SCRIPT_EXT]);
            }

            // If the menu name ends in "..", then it's expected that we are waiting on another menu, so don't show one now.
            if (0 == osRegexIsMatch(menu, ".+\\.\\.$"))
                NPCmenu(user, npc);
        }   // End of menu block.
    }

    no_sensor()
    {
        sensorInFlight = FALSE;
        if (llGetListLength(sensorRequests))
        {
            sensorRequests = llDeleteSubList(sensorRequests, 0, 8);
            nextSensor();
        }
    }

    sensor(integer numberDetected)
    {
        sensorInFlight = FALSE;
        if (llGetListLength(sensorRequests))
        {
            string  name    = llList2String(sensorRequests, 0);
            integer type    = llList2Integer(sensorRequests, 1);
            float   range   = llList2Float(sensorRequests, 2);
            string  command = llList2String(sensorRequests, 3);
            key     user    = llList2String(sensorRequests, 4);
            key     npc     = llList2String(sensorRequests, 5);
            string  title   = llList2String(sensorRequests, 6);
            integer agents  = llList2Integer(sensorRequests, 7);
            integer every   = llList2Integer(sensorRequests, 8);

            sensorRequests = llDeleteSubList(sensorRequests, 0, 8);
            if ("" == title)
                sendCommand(user, command + " " + npc + " " + llDetectedKey(0));
            else
            {
                integer i;
                list menu = [];

                if (agents)
                    menu += ["you|" + (string) user];
                for (i = 0; i < numberDetected; i++)
                {
                    key this = llDetectedKey(i);

                    if (!(agents && (this == user)))
                        if (every || osIsNpc(this))
                            menu += [llDetectedName(i) + "|" + (string) this];
                }
                if (llGetListLength(menu) > 0)
                    showMenu(user, (string) INVENTORY_NONE, title, menu, command + "|" + npc);
            }
            nextSensor();
        }
    }

}