/*

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */
package org.apache.batik.anim.dom;

import java.util.HashMap;
import java.util.HashSet;

import org.apache.batik.dom.AbstractNode;
import org.apache.batik.dom.events.AbstractEvent;
import org.apache.batik.dom.events.EventListenerList;
import org.apache.batik.dom.events.EventSupport;
import org.apache.batik.dom.events.NodeEventTarget;

import org.apache.batik.dom.xbl.NodeXBL;
import org.apache.batik.dom.xbl.ShadowTreeEvent;
import org.apache.batik.util.XMLConstants;

import org.w3c.dom.DOMException;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventException;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.MutationEvent;

/**
 * An EventSupport class that handles XBL-specific event processing.
 *
 * @author <a href="mailto:cam%40mcc%2eid%2eau">Cameron McCormack</a>
 * @version $Id: XBLEventSupport.java 1813521 2017-10-27 12:34:11Z ssteiner $
 */
public class XBLEventSupport extends EventSupport {

    /**
     * The unstoppable capturing listeners table.
     */
    protected HashMap<String, EventListenerList> capturingImplementationListeners;

    /**
     * The unstoppable bubbling listeners table.
     */
    protected HashMap<String, EventListenerList> bubblingImplementationListeners;

    /**
     * Map of event types to their aliases.
     */
    protected static HashMap<String, String> eventTypeAliases = new HashMap<String, String>();
    static {
        eventTypeAliases.put("SVGLoad",   "load");
        eventTypeAliases.put("SVGUnoad",  "unload");
        eventTypeAliases.put("SVGAbort",  "abort");
        eventTypeAliases.put("SVGError",  "error");
        eventTypeAliases.put("SVGResize", "resize");
        eventTypeAliases.put("SVGScroll", "scroll");
        eventTypeAliases.put("SVGZoom",   "zoom");
    }

    /**
     * Creates a new XBLEventSupport object.
     */
    public XBLEventSupport(AbstractNode n) {
        super(n);
    }

    /**
     * Registers an event listener for the given namespaced event type
     * in the specified group.
     */
    public void addEventListenerNS(String namespaceURI,
                                   String type,
                                   EventListener listener,
                                   boolean useCapture,
                                   Object group) {
        super.addEventListenerNS
            (namespaceURI, type, listener, useCapture, group);
        if (namespaceURI == null
                || namespaceURI.equals(XMLConstants.XML_EVENTS_NAMESPACE_URI)) {
            String alias = (String) eventTypeAliases.get(type);
            if (alias != null) {
                super.addEventListenerNS
                    (namespaceURI, alias, listener, useCapture, group);
            }
        }
    }

    /**
     * Deregisters an event listener.
     */
    public void removeEventListenerNS(String namespaceURI,
                                      String type,
                                      EventListener listener,
                                      boolean useCapture) {
        super.removeEventListenerNS(namespaceURI, type, listener, useCapture);
        if (namespaceURI == null
                || namespaceURI.equals(XMLConstants.XML_EVENTS_NAMESPACE_URI)) {
            String alias = (String) eventTypeAliases.get(type);
            if (alias != null) {
                super.removeEventListenerNS
                    (namespaceURI, alias, listener, useCapture);
            }
        }
    }

    /**
     * Registers an event listener that will not be stopped by the usual
     * XBL stopping.
     */
    public void addImplementationEventListenerNS(String namespaceURI,
                                                 String type,
                                                 EventListener listener,
                                                 boolean useCapture) {
        HashMap<String, EventListenerList> listeners;
        if (useCapture) {
            if (capturingImplementationListeners == null) {
                capturingImplementationListeners = new HashMap<String, EventListenerList>();
            }
            listeners = capturingImplementationListeners;
        } else {
            if (bubblingImplementationListeners == null) {
                bubblingImplementationListeners = new HashMap<String, EventListenerList>();
            }
            listeners = bubblingImplementationListeners;
        }
        EventListenerList list = listeners.get(type);
        if (list == null) {
            list = new EventListenerList();
            listeners.put(type, list);
        }
        list.addListener(namespaceURI, null, listener);
    }

    /**
     * Unregisters an implementation event listener.
     */
    public void removeImplementationEventListenerNS(String namespaceURI,
                                                    String type,
                                                    EventListener listener,
                                                    boolean useCapture) {
        HashMap<String, EventListenerList> listeners = useCapture
            ? capturingImplementationListeners : bubblingImplementationListeners;
        if (listeners == null) {
            return;
        }
        EventListenerList list = listeners.get(type);
        if (list == null) {
            return;
        }
        list.removeListener(namespaceURI, listener);
        if (list.size() == 0) {
            listeners.remove(type);
        }
    }

    /**
     * Moves all of the event listeners from this EventSupport object
     * to the given EventSupport object.
     * Used by {@link
     * org.apache.batik.dom.AbstractDocument#renameNode(Node,String,String)}.
     */
    public void moveEventListeners(EventSupport other) {
        super.moveEventListeners(other);
        XBLEventSupport es = (XBLEventSupport) other;
        es.capturingImplementationListeners = capturingImplementationListeners;
        es.bubblingImplementationListeners = bubblingImplementationListeners;
        capturingImplementationListeners = null;
        bubblingImplementationListeners = null;
    }

    /**
     * This method allows the dispatch of events into the
     * implementations event model. Events dispatched in this manner
     * will have the same capturing and bubbling behavior as events
     * dispatched directly by the implementation. The target of the
     * event is the <code> EventTarget</code> on which
     * <code>dispatchEvent</code> is called.
     *
     * @param target the target node
     * @param evt Specifies the event type, behavior, and contextual
     * information to be used in processing the event.
     *
     * @return The return value of <code>dispatchEvent</code>
     * indicates whether any of the listeners which handled the event
     * called <code>preventDefault</code>.  If
     * <code>preventDefault</code> was called the value is false, else
     * the value is true.
     *
     * @exception EventException
     *   UNSPECIFIED_EVENT_TYPE_ERR: Raised if the
     *   <code>Event</code>'s type was not specified by initializing
     *   the event before <code>dispatchEvent</code> was
     *   called. Specification of the <code>Event</code>'s type as
     *   <code>null</code> or an empty string will also trigger this
     *   exception.  
     */
    public boolean dispatchEvent(NodeEventTarget target, Event evt) 
            throws EventException {
//         System.err.println("\t[] dispatching " + e.getType() + " on " + ((Node) target).getNodeName());
        if (evt == null) {
            return false;
        }
        if (!(evt instanceof AbstractEvent)) {
            throw createEventException
                (DOMException.NOT_SUPPORTED_ERR,
                 "unsupported.event",
                 new Object[] {});
        }
        AbstractEvent e = (AbstractEvent) evt;
        String type = e.getType();
        if (type == null || type.length() == 0) {
            throw createEventException
                (EventException.UNSPECIFIED_EVENT_TYPE_ERR,
                 "unspecified.event",
                 new Object[] {});
        }
        // fix event status
        setTarget(e, target);
        stopPropagation(e, false);
        stopImmediatePropagation(e, false);
        preventDefault(e, false);
        // dump the tree hierarchy from top to the target
        NodeEventTarget[] ancestors = getAncestors(target);
        int bubbleLimit = e.getBubbleLimit();
        int minAncestor = 0;
        if (isSingleScopeEvent(e)) {
            // DOM Mutation events are dispatched only within the
            // one shadow scope
            AbstractNode targetNode = (AbstractNode) target;
            Node boundElement = targetNode.getXblBoundElement();
            if (boundElement != null) {
                minAncestor = ancestors.length;
                while (minAncestor > 0) {
                    AbstractNode ancestorNode =
                        (AbstractNode) ancestors[minAncestor - 1];
                    if (ancestorNode.getXblBoundElement() != boundElement) {
                        break;
                    }
                    minAncestor--;
                }
            }
        } else if (bubbleLimit != 0) {
            // Other events may have a bubble limit (such as UI events)
            minAncestor = ancestors.length - bubbleLimit + 1;
            if (minAncestor < 0) {
                minAncestor = 0;
            }
        }
//         System.err.println("\t== ancestors:");
//         for (int i = 0; i < ancestors.length; i++) {
//             if (i < minAncestor) {
//                 System.err.print("\t     ");
//             } else {
//                 System.err.print("\t   * ");
//             }
//             System.err.println(((Node) ancestors[i]).getNodeName());
//         }
        AbstractEvent[] es = getRetargettedEvents(target, ancestors, e);
        boolean preventDefault = false;
        // CAPTURING_PHASE : fire event listeners from top to EventTarget
        HashSet stoppedGroups = new HashSet();
        HashSet toBeStoppedGroups = new HashSet();
        for (int i = 0; i < minAncestor; i++) {
            NodeEventTarget node = ancestors[i];
//             System.err.println("\t--   CAPTURING " + e.getType() + "  " + ((Node) node).getNodeName());
            setCurrentTarget(es[i], node);
            setEventPhase(es[i], Event.CAPTURING_PHASE);
            fireImplementationEventListeners(node, es[i], true);
        }
        for (int i = minAncestor; i < ancestors.length; i++) {
            NodeEventTarget node = ancestors[i];
//             System.err.println("\t-- * CAPTURING " + e.getType() + "  " + ((Node) node).getNodeName());
            setCurrentTarget(es[i], node);
            setEventPhase(es[i], Event.CAPTURING_PHASE);
            fireImplementationEventListeners(node, es[i], true);
            fireEventListeners(node, es[i], true, stoppedGroups,
                               toBeStoppedGroups);
            fireHandlerGroupEventListeners(node, es[i], true, stoppedGroups,
                                           toBeStoppedGroups);
            preventDefault = preventDefault || es[i].getDefaultPrevented();
            stoppedGroups.addAll(toBeStoppedGroups);
            toBeStoppedGroups.clear();
        }
        // AT_TARGET : fire local event listeners
//         System.err.println("\t-- * AT_TARGET " + e.getType() + "  " + ((Node) target).getNodeName());
        setEventPhase(e, Event.AT_TARGET);
        setCurrentTarget(e, target);
        fireImplementationEventListeners(target, e, false);
        fireEventListeners(target, e, false, stoppedGroups,
                           toBeStoppedGroups);
        fireHandlerGroupEventListeners(node, e, false, stoppedGroups,
                                       toBeStoppedGroups);
        stoppedGroups.addAll(toBeStoppedGroups);
        toBeStoppedGroups.clear();
        preventDefault = preventDefault || e.getDefaultPrevented();
        // BUBBLING_PHASE : fire event listeners from target to top
        if (e.getBubbles()) {
            for (int i = ancestors.length - 1; i >= minAncestor; i--) {
                NodeEventTarget node = ancestors[i];
//                 System.err.println("\t-- * BUBBLING  " + e.getType() + "  " + ((Node) node).getNodeName());
                setCurrentTarget(es[i], node);
                setEventPhase(es[i], Event.BUBBLING_PHASE);
                fireImplementationEventListeners(node, es[i], false);
                fireEventListeners(node, es[i], false, stoppedGroups,
                                   toBeStoppedGroups);
                fireHandlerGroupEventListeners
                    (node, es[i], false, stoppedGroups, toBeStoppedGroups);
                preventDefault =
                    preventDefault || es[i].getDefaultPrevented();
                stoppedGroups.addAll(toBeStoppedGroups);
                toBeStoppedGroups.clear();
            }
            for (int i = minAncestor - 1; i >= 0; i--) {
                NodeEventTarget node = ancestors[i];
//                 System.err.println("\t--   BUBBLING  " + e.getType() + "  " + ((Node) node).getNodeName());
                setCurrentTarget(es[i], node);
                setEventPhase(es[i], Event.BUBBLING_PHASE);
                fireImplementationEventListeners(node, es[i], false);
                preventDefault =
                    preventDefault || es[i].getDefaultPrevented();
            }
        }
        if (!preventDefault) {
            runDefaultActions(e);
        }
        return preventDefault;
    }

    /**
     * Fires the event handlers registered on an XBL 'handlerGroup' element.
     */
    protected void fireHandlerGroupEventListeners(NodeEventTarget node, 
                                                  AbstractEvent e,
                                                  boolean useCapture,
                                                  HashSet stoppedGroups,
                                                  HashSet toBeStoppedGroups) {
        // get the XBL definitions in effect for the event target
        NodeList defs = ((NodeXBL) node).getXblDefinitions();
        for (int j = 0; j < defs.getLength(); j++) {
            // find the 'handlerGroup' element
            Node n = defs.item(j).getFirstChild();
            while (n != null &&
                    !(n instanceof XBLOMHandlerGroupElement)) {
                n = n.getNextSibling();
            }
            if (n == null) {
                continue;
            }
            node = (NodeEventTarget) n;
            String type = e.getType();
            EventSupport support = node.getEventSupport();
            // check if the event support has been instantiated
            if (support == null) {
                continue;
            }
            EventListenerList list = support.getEventListeners(type, useCapture);
            // check if the event listeners list is not empty
            if (list == null) {
                return;
            }
            // dump event listeners, we get the registered listeners NOW
            EventListenerList.Entry[] listeners = list.getEventListeners();
            fireEventListeners(node, e, listeners, stoppedGroups,
                               toBeStoppedGroups);
        }
    }

    /**
     * Returns whether the given event should be stopped once it crosses
     * a shadow scope boundary.
     */
    protected boolean isSingleScopeEvent(Event evt) {
        return evt instanceof MutationEvent
            || evt instanceof ShadowTreeEvent;
    }

    /**
     * Returns an array of Event objects to be used for each event target
     * in the event flow.  The Event objects are retargetted if an sXBL
     * shadow scope is crossed and the event is not a DOM mutation event.
     */
    protected AbstractEvent[] getRetargettedEvents(NodeEventTarget target,
                                                   NodeEventTarget[] ancestors,
                                                   AbstractEvent e) {
        boolean singleScope = isSingleScopeEvent(e);
        AbstractNode targetNode = (AbstractNode) target;
        AbstractEvent[] es = new AbstractEvent[ancestors.length];
        if (ancestors.length > 0) {
            int index = ancestors.length - 1;
            Node boundElement = targetNode.getXblBoundElement();
            AbstractNode ancestorNode = (AbstractNode) ancestors[index];
            if (!singleScope &&
                    ancestorNode.getXblBoundElement() != boundElement) {
                es[index] = retargetEvent(e, ancestors[index]);
            } else {
                es[index] = e;
            }
            while (--index >= 0) {
                ancestorNode = (AbstractNode) ancestors[index + 1];
                boundElement = ancestorNode.getXblBoundElement();
                AbstractNode nextAncestorNode = (AbstractNode) ancestors[index];
                Node nextBoundElement = nextAncestorNode.getXblBoundElement();
                if (!singleScope && nextBoundElement != boundElement) {
                    es[index] = retargetEvent(es[index + 1], ancestors[index]);
                } else {
                    es[index] = es[index + 1];
                }
            }
        }
        return es;
    }

    /**
     * Clones and retargets the given event.
     */
    protected AbstractEvent retargetEvent(AbstractEvent e,
                                          NodeEventTarget target) {
        AbstractEvent clonedEvent = e.cloneEvent();
        setTarget(clonedEvent, target);
        return clonedEvent;
    }
    
    /**
     * Returns the implementation listneers.
     */
    public EventListenerList getImplementationEventListeners
            (String type, boolean useCapture) {
        HashMap<String, EventListenerList> listeners = useCapture
            ? capturingImplementationListeners : bubblingImplementationListeners;
        return listeners != null ? listeners.get(type) : null;
    }

    /**
     * Fires the registered implementation listeners on the given event
     * target.
     */
    protected void fireImplementationEventListeners(NodeEventTarget node, 
                                                    AbstractEvent e,
                                                    boolean useCapture) {
        String type = e.getType();
        XBLEventSupport support = (XBLEventSupport) node.getEventSupport();
        // check if the event support has been instantiated
        if (support == null) {
            return;
        }
        EventListenerList list =
            support.getImplementationEventListeners(type, useCapture);
        // check if the event listeners list is not empty
        if (list == null) {
            return;
        }
        // dump event listeners, we get the registered listeners NOW
        EventListenerList.Entry[] listeners = list.getEventListeners();
        fireEventListeners(node, e, listeners, null, null);
    }
}
