/*
 *  Copyright (C) 2001 Peter Kelly (pmk@post.com)
 *  Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008 Apple Inc. All Rights Reserved.
 *
 *  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; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  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
 */

#include "config.h"
#include "JSEventListener.h"

#include "CString.h"
#include "Console.h"
#include "DOMWindow.h"
#include "Document.h"
#include "Event.h"
#include "Frame.h"
#include "FrameLoader.h"
#include "JSDOMWindow.h"
#include "JSEvent.h"
#include "JSEventTarget.h"
#include "JSEventTargetNode.h"
#include "ScriptController.h"
#include <runtime/FunctionConstructor.h>
#include <runtime/JSLock.h>
#include <wtf/RefCountedLeakCounter.h>

using namespace JSC;

namespace WebCore {

ASSERT_CLASS_FITS_IN_CELL(JSAbstractEventListener)

void JSAbstractEventListener::handleEvent(Event* event, bool isWindowEvent)
{
    JSLock lock(false);

    JSObject* listener = listenerObj();
    if (!listener)
        return;

    JSDOMGlobalObject* globalObject = this->globalObject();
    // Null check as clearGlobalObject() can clear this and we still get called back by
    // xmlhttprequest objects. See http://bugs.webkit.org/show_bug.cgi?id=13275
    // FIXME: Is this check still necessary? Requests are supposed to be stopped before clearGlobalObject() is called.
    if (!globalObject)
        return;

    ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext();
    if (!scriptExecutionContext)
        return;

    if (scriptExecutionContext->isDocument()) {
        JSDOMWindow* window = static_cast<JSDOMWindow*>(globalObject);
        Frame* frame = window->impl()->frame();
        if (!frame)
            return;
        // The window must still be active in its frame. See <https://bugs.webkit.org/show_bug.cgi?id=21921>.
        // FIXME: A better fix for this may be to change DOMWindow::frame() to not return a frame the detached window used to be in.
        if (frame->domWindow() != window->impl())
            return;
        // FIXME: Is this check needed for other contexts?
        ScriptController* script = frame->script();
        if (!script->isEnabled() || script->isPaused())
            return;
    }

    ExecState* exec = globalObject->globalExec();

    JSValuePtr handleEventFunction = listener->get(exec, Identifier(exec, "handleEvent"));
    CallData callData;
    CallType callType = handleEventFunction->getCallData(callData);
    if (callType == CallTypeNone) {
        handleEventFunction = noValue();
        callType = listener->getCallData(callData);
    }

    if (callType != CallTypeNone) {
        ref();

        ArgList args;
        args.append(toJS(exec, event));

        Event* savedEvent = globalObject->currentEvent();
        globalObject->setCurrentEvent(event);

        // If this event handler is the first JavaScript to execute, then the
        // dynamic global object should be set to the global object of the
        // window in which the event occurred.
        JSGlobalData* globalData = globalObject->globalData();
        DynamicGlobalObjectScope globalObjectScope(exec, globalData->dynamicGlobalObject ? globalData->dynamicGlobalObject : globalObject);

        JSValuePtr retval;
        if (handleEventFunction) {
            globalObject->startTimeoutCheck();
            retval = call(exec, handleEventFunction, callType, callData, listener, args);
        } else {
            JSValuePtr thisValue;
            if (isWindowEvent)
                thisValue = globalObject->toThisObject(exec);
            else
                thisValue = toJS(exec, event->currentTarget());
            globalObject->startTimeoutCheck();
            retval = call(exec, listener, callType, callData, thisValue, args);
        }
        globalObject->stopTimeoutCheck();

        globalObject->setCurrentEvent(savedEvent);

        if (exec->hadException())
            reportCurrentException(exec);
        else {
            if (!retval->isUndefinedOrNull() && event->storesResultAsString())
                event->storeResult(retval->toString(exec));
            if (m_isInline) {
                bool retvalbool;
                if (retval->getBoolean(retvalbool) && !retvalbool)
                    event->preventDefault();
            }
        }

        if (scriptExecutionContext->isDocument())
            Document::updateDocumentsRendering();
        deref();
    }
}

bool JSAbstractEventListener::isInline() const
{
    return m_isInline;
}

// -------------------------------------------------------------------------

JSUnprotectedEventListener::JSUnprotectedEventListener(JSObject* listener, JSDOMGlobalObject* globalObject, bool isInline)
    : JSAbstractEventListener(isInline)
    , m_listener(listener)
    , m_globalObject(globalObject)
{
    if (m_listener) {
        JSDOMWindow::UnprotectedListenersMap& listeners = isInline
            ? globalObject->jsUnprotectedInlineEventListeners() : globalObject->jsUnprotectedEventListeners();
        listeners.set(m_listener, this);
    }
}

JSUnprotectedEventListener::~JSUnprotectedEventListener()
{
    if (m_listener && m_globalObject) {
        JSDOMWindow::UnprotectedListenersMap& listeners = isInline()
            ? m_globalObject->jsUnprotectedInlineEventListeners() : m_globalObject->jsUnprotectedEventListeners();
        listeners.remove(m_listener);
    }
}

JSObject* JSUnprotectedEventListener::listenerObj() const
{
    return m_listener;
}

JSDOMGlobalObject* JSUnprotectedEventListener::globalObject() const
{
    return m_globalObject;
}

void JSUnprotectedEventListener::clearGlobalObject()
{
    m_globalObject = 0;
}

void JSUnprotectedEventListener::mark()
{
    if (m_listener && !m_listener->marked())
        m_listener->mark();
}

#ifndef NDEBUG
static WTF::RefCountedLeakCounter eventListenerCounter("EventListener");
#endif

// -------------------------------------------------------------------------

JSEventListener::JSEventListener(JSObject* listener, JSDOMGlobalObject* globalObject, bool isInline)
    : JSAbstractEventListener(isInline)
    , m_listener(listener)
    , m_globalObject(globalObject)
{
    if (m_listener) {
        JSDOMWindow::ListenersMap& listeners = isInline
            ? m_globalObject->jsInlineEventListeners() : m_globalObject->jsEventListeners();
        listeners.set(m_listener, this);
    }
#ifndef NDEBUG
    eventListenerCounter.increment();
#endif
}

JSEventListener::~JSEventListener()
{
    if (m_listener && m_globalObject) {
        JSDOMWindow::ListenersMap& listeners = isInline()
            ? m_globalObject->jsInlineEventListeners() : m_globalObject->jsEventListeners();
        listeners.remove(m_listener);
    }
#ifndef NDEBUG
    eventListenerCounter.decrement();
#endif
}

JSObject* JSEventListener::listenerObj() const
{
    return m_listener;
}

JSDOMGlobalObject* JSEventListener::globalObject() const
{
    return m_globalObject;
}

void JSEventListener::clearGlobalObject()
{
    m_globalObject = 0;
}

// -------------------------------------------------------------------------

JSLazyEventListener::JSLazyEventListener(LazyEventListenerType type, const String& functionName, const String& code, JSDOMGlobalObject* globalObject, Node* node, int lineNumber)
    : JSEventListener(0, globalObject, true)
    , m_functionName(functionName)
    , m_code(code)
    , m_parsed(false)
    , m_lineNumber(lineNumber)
    , m_originalNode(node)
    , m_type(type)
{
    // We don't retain the original node because we assume it
    // will stay alive as long as this handler object is around
    // and we need to avoid a reference cycle. If JS transfers
    // this handler to another node, parseCode will be called and
    // then originalNode is no longer needed.

    // A JSLazyEventListener can be created with a line number of zero when it is created with
    // a setAttribute call from JavaScript, so make the line number 1 in that case.
    if (m_lineNumber == 0)
        m_lineNumber = 1;
}

JSObject* JSLazyEventListener::listenerObj() const
{
    parseCode();
    return m_listener;
}

// Helper function
inline JSValuePtr eventParameterName(JSLazyEventListener::LazyEventListenerType type, ExecState* exec)
{
    switch (type) {
    case JSLazyEventListener::HTMLLazyEventListener:
        return jsNontrivialString(exec, "event");
#if ENABLE(SVG)
    case JSLazyEventListener::SVGLazyEventListener:
        return jsNontrivialString(exec, "evt");
#endif
    default:
        ASSERT_NOT_REACHED();
        return jsUndefined();
    }
}

void JSLazyEventListener::parseCode() const
{
    if (m_parsed)
        return;

    if (globalObject()->scriptExecutionContext()->isDocument()) {
        JSDOMWindow* window = static_cast<JSDOMWindow*>(globalObject());
        Frame* frame = window->impl()->frame();
        if (!frame)
            return;
        // FIXME: Is this check needed for non-Document contexts?
        ScriptController* script = frame->script();
        if (!script->isEnabled() || script->isPaused())
            return;
    }

    m_parsed = true;

    ExecState* exec = globalObject()->globalExec();

    ArgList args;
    UString sourceURL(globalObject()->scriptExecutionContext()->url().string());
    args.append(eventParameterName(m_type, exec));
    args.append(jsString(exec, m_code));

    // FIXME: Passing the document's URL to construct is not always correct, since this event listener might
    // have been added with setAttribute from a script, and we should pass String() in that case.
    m_listener = constructFunction(exec, args, Identifier(exec, m_functionName), sourceURL, m_lineNumber); // FIXME: is globalExec ok?

    JSFunction* listenerAsFunction = static_cast<JSFunction*>(m_listener.get());

    if (exec->hadException()) {
        exec->clearException();

        // failed to parse, so let's just make this listener a no-op
        m_listener = 0;
    } else if (m_originalNode) {
        // Add the event's home element to the scope
        // (and the document, and the form - see JSHTMLElement::eventHandlerScope)
        ScopeChain scope = listenerAsFunction->scope();

        JSValuePtr thisObj = toJS(exec, m_originalNode);
        if (thisObj->isObject()) {
            static_cast<JSEventTargetNode*>(asObject(thisObj))->pushEventHandlerScope(exec, scope);
            listenerAsFunction->setScope(scope);
        }
    }

    // no more need to keep the unparsed code around
    m_functionName = String();
    m_code = String();

    if (m_listener) {
        ASSERT(isInline());
        JSDOMWindow::ListenersMap& listeners = globalObject()->jsInlineEventListeners();
        listeners.set(m_listener, const_cast<JSLazyEventListener*>(this));
    }
}

} // namespace WebCore
