/**
 * @file   llnotificationslistener.cpp
 * @author Brad Kittenbrink
 * @date   2009-07-08
 * @brief  Implementation for llnotificationslistener.
 * 
 * $LicenseInfo:firstyear=2009&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include "linden_common.h"
#include "llnotificationslistener.h"
#include "llnotifications.h"
#include "llnotificationtemplate.h"
#include "llsd.h"
#include "llui.h"
#include <boost/foreach.hpp>

LLNotificationsListener::LLNotificationsListener(LLNotifications & notifications) :
    LLEventAPI("LLNotifications",
               "LLNotifications listener to (e.g.) pop up a notification"),
    mNotifications(notifications)
{
    add("requestAdd",
        "Add a notification with specified [\"name\"], [\"substitutions\"] and [\"payload\"].\n"
        "If optional [\"reply\"] specified, arrange to send user response on that LLEventPump.",
        &LLNotificationsListener::requestAdd);
    add("listChannels",
        "Post to [\"reply\"] a map of info on existing channels",
        &LLNotificationsListener::listChannels,
        LLSD().with("reply", LLSD()));
    add("listChannelNotifications",
        "Post to [\"reply\"] an array of info on notifications in channel [\"channel\"]",
        &LLNotificationsListener::listChannelNotifications,
        LLSD().with("reply", LLSD()).with("channel", LLSD()));
    add("respond",
        "Respond to notification [\"uuid\"] with data in [\"response\"]",
        &LLNotificationsListener::respond,
        LLSD().with("uuid", LLSD()));
    add("cancel",
        "Cancel notification [\"uuid\"]",
        &LLNotificationsListener::cancel,
        LLSD().with("uuid", LLSD()));
    add("ignore",
        "Ignore future notification [\"name\"]\n"
        "(from <notification name= > in notifications.xml)\n"
        "according to boolean [\"ignore\"].\n"
        "If [\"name\"] is omitted or undefined, [un]ignore all future notifications.\n"
        "Note that ignored notifications are not forwarded unless intercepted before\n"
        "the \"Ignore\" channel.",
        &LLNotificationsListener::ignore);
    add("forward",
        "Forward to [\"pump\"] future notifications on channel [\"channel\"]\n"
        "according to boolean [\"forward\"]. When enabled, only types matching\n"
        "[\"types\"] are forwarded, as follows:\n"
        "omitted or undefined: forward all notifications\n"
        "string: forward only the specific named [sig]type\n"
        "array of string: forward any notification matching any named [sig]type.\n"
        "When boolean [\"respond\"] is true, we auto-respond to each forwarded\n"
        "notification.",
        &LLNotificationsListener::forward,
        LLSD().with("channel", LLSD()));
}

// This is here in the .cpp file so we don't need the definition of class
// Forwarder in the header file.
LLNotificationsListener::~LLNotificationsListener()
{
}

void LLNotificationsListener::requestAdd(const LLSD& event_data) const
{
	if(event_data.has("reply"))
	{
		LLSD payload(event_data["payload"]);
		// copy reqid, if provided, to link response with request
		payload["reqid"] = event_data["reqid"];
		mNotifications.add(event_data["name"], 
						   event_data["substitutions"], 
						   payload,
						   boost::bind(&LLNotificationsListener::NotificationResponder, 
									   this, 
									   event_data["reply"].asString(), 
									   _1, _2
									   )
						   );
	}
	else
	{
		mNotifications.add(event_data["name"], 
						   event_data["substitutions"], 
						   event_data["payload"]);
	}
}

void LLNotificationsListener::NotificationResponder(const std::string& reply_pump, 
										const LLSD& notification, 
										const LLSD& response) const
{
	LLSD response_event;
	response_event["notification"] = notification;
	response_event["response"] = response;
	// surface reqid at top level of response for request/response protocol
	response_event["reqid"] = notification["payload"]["reqid"];
	LLEventPumps::getInstance()->obtain(reply_pump).post(response_event);
}

void LLNotificationsListener::listChannels(const LLSD& params) const
{
    LLReqID reqID(params);
    LLSD response(reqID.makeResponse());
    for (auto& cm : LLNotificationChannel::instance_snapshot())
    {
        LLSD channelInfo, parents;
        for (const std::string& parent : cm.getParents())
        {
            parents.append(parent);
        }
        channelInfo["parents"] = parents;
        channelInfo["parent"] = parents.size()? parents[0] : "";
        response[cm.getName()] = channelInfo;
    }
    LLEventPumps::instance().obtain(params["reply"]).post(response);
}

void LLNotificationsListener::listChannelNotifications(const LLSD& params) const
{
    LLReqID reqID(params);
    LLSD response(reqID.makeResponse());
    LLNotificationChannelPtr channel(mNotifications.getChannel(params["channel"]));
    if (channel)
    {
        LLSD notifications(LLSD::emptyArray());
        for (LLNotificationChannel::Iterator ni(channel->begin()), nend(channel->end());
             ni != nend; ++ni)
        {
            notifications.append(asLLSD(*ni));
        }
        response["notifications"] = notifications;
    }
    LLEventPumps::instance().obtain(params["reply"]).post(response);
}

void LLNotificationsListener::respond(const LLSD& params) const
{
    LLNotificationPtr notification(mNotifications.find(params["uuid"]));
    if (notification)
    {
        notification->respond(params["response"]);
    }
}

void LLNotificationsListener::cancel(const LLSD& params) const
{
    LLNotificationPtr notification(mNotifications.find(params["uuid"]));
    if (notification)
    {
        mNotifications.cancel(notification);
    }
}

void LLNotificationsListener::ignore(const LLSD& params) const
{
    // Calling a method named "ignore", but omitting its "ignore" Boolean
    // argument, should by default cause something to be ignored. Explicitly
    // pass ["ignore"] = false to cancel ignore.
    bool ignore = true;
    if (params.has("ignore"))
    {
        ignore = params["ignore"].asBoolean();
    }
    // This method can be used to affect either a single notification name or
    // all future notifications. The two use substantially different mechanisms.
    if (params["name"].isDefined())
    {
        // ["name"] was passed: ignore just that notification
		LLNotificationTemplatePtr templatep = mNotifications.getTemplate(params["name"]);
		if (templatep)
		{
			templatep->mForm->setIgnored(ignore);
		}
    }
    else
    {
        // no ["name"]: ignore all future notifications
        mNotifications.setIgnoreAllNotifications(ignore);
    }
}

class LLNotificationsListener::Forwarder: public LLEventTrackable
{
    LOG_CLASS(LLNotificationsListener::Forwarder);
public:
    Forwarder(LLNotifications& llnotifications, const std::string& channel):
        mNotifications(llnotifications),
        mRespond(false)
    {
        // Connect to the specified channel on construction. Because
        // LLEventTrackable is a base, we should automatically disconnect when
        // destroyed.
        LLNotificationChannelPtr channelptr(llnotifications.getChannel(channel));
        if (channelptr)
        {
            // Insert our processing as a "passed filter" listener. This way
            // we get to run before all the "changed" listeners, and we get to
            // swipe it (hide it from the other listeners) if desired.
            channelptr->connectPassedFilter(boost::bind(&Forwarder::handle, this, _1));
        }
    }

    void setPumpName(const std::string& name) { mPumpName = name; }
    void setTypes(const LLSD& types) { mTypes = types; }
    void setRespond(bool respond) { mRespond = respond; }

private:
    bool handle(const LLSD& notification) const;
    bool matchType(const LLSD& filter, const std::string& type) const;

    LLNotifications& mNotifications;
    std::string mPumpName;
    LLSD mTypes;
    bool mRespond;
};

void LLNotificationsListener::forward(const LLSD& params)
{
    std::string channel(params["channel"]);
    // First decide whether we're supposed to start forwarding or stop it.
    // Default to true.
    bool forward = true;
    if (params.has("forward"))
    {
        forward = params["forward"].asBoolean();
    }
    if (! forward)
    {
        // This is a request to stop forwarding notifications on the specified
        // channel. The rest of the params don't matter.
        // Because mForwarders contains scoped_ptrs, erasing the map entry
        // DOES delete the heap Forwarder object. Because Forwarder derives
        // from LLEventTrackable, destroying it disconnects it from the
        // channel.
        mForwarders.erase(channel);
        return;
    }
    // From here on, we know we're being asked to start (or modify) forwarding
    // on the specified channel. Find or create an appropriate Forwarder.
    ForwarderMap::iterator
        entry(mForwarders.insert(ForwarderMap::value_type(channel, ForwarderMap::mapped_type())).first);
    if (! entry->second)
    {
        entry->second.reset(new Forwarder(mNotifications, channel));
    }
    // Now, whether this Forwarder is brand-new or not, update it with the new
    // request info.
    Forwarder& fwd(*entry->second);
    fwd.setPumpName(params["pump"]);
    fwd.setTypes(params["types"]);
    fwd.setRespond(params["respond"]);
}

bool LLNotificationsListener::Forwarder::handle(const LLSD& notification) const
{
    LL_INFOS("LLNotificationsListener") << "handle(" << notification << ")" << LL_ENDL;
    if (notification["sigtype"].asString() == "delete")
    {
        LL_INFOS("LLNotificationsListener") << "ignoring delete" << LL_ENDL;
        // let other listeners see the "delete" operation
        return false;
    }
    LLNotificationPtr note(mNotifications.find(notification["id"]));
    if (! note)
    {
        LL_INFOS("LLNotificationsListener") << notification["id"] << " not found" << LL_ENDL;
        return false;
    }
    if (! matchType(mTypes, note->getType()))
    {
        LL_INFOS("LLNotificationsListener") << "didn't match types " << mTypes << LL_ENDL;
        // We're not supposed to intercept this particular notification. Let
        // other listeners process it.
        return false;
    }
    LL_INFOS("LLNotificationsListener") << "sending via '" << mPumpName << "'" << LL_ENDL;
    // This is a notification we care about. Forward it through specified
    // LLEventPump.
    LLEventPumps::instance().obtain(mPumpName).post(asLLSD(note));
    // Are we also being asked to auto-respond?
    if (mRespond)
    {
        LL_INFOS("LLNotificationsListener") << "should respond" << LL_ENDL;
        note->respond(LLSD::emptyMap());
        // Did that succeed in removing the notification? Only cancel() if
        // it's still around -- otherwise we get an LL_ERRS crash!
        note = mNotifications.find(notification["id"]);
        if (note)
        {
            LL_INFOS("LLNotificationsListener") << "respond() didn't clear, canceling" << LL_ENDL;
            mNotifications.cancel(note);
        }
    }
    // If we've auto-responded to this notification, then it's going to be
    // deleted. Other listeners would get the change operation, try to look it
    // up and be baffled by lookup failure. So when we auto-respond, suppress
    // this notification: don't pass it to other listeners.
    return mRespond;
}

bool LLNotificationsListener::Forwarder::matchType(const LLSD& filter, const std::string& type) const
{
    // Decide whether this notification matches filter:
    // undefined: forward all notifications
    if (filter.isUndefined())
    {
        return true;
    }
    // array of string: forward any notification matching any named type
    if (filter.isArray())
    {
        for (LLSD::array_const_iterator ti(filter.beginArray()), tend(filter.endArray());
             ti != tend; ++ti)
        {
            if (ti->asString() == type)
            {
                return true;
            }
        }
        // Didn't match any entry in the array
        return false;
    }
    // string: forward only the specific named type
    return (filter.asString() == type);
}

LLSD LLNotificationsListener::asLLSD(LLNotificationPtr note)
{
    LLSD notificationInfo(note->asLLSD());
    // For some reason the following aren't included in LLNotification::asLLSD().
    notificationInfo["summary"] = note->summarize();
    notificationInfo["id"]      = note->id();
    notificationInfo["type"]    = note->getType();
    notificationInfo["message"] = note->getMessage();
    notificationInfo["label"]   = note->getLabel();
    return notificationInfo;
}