/* 
 * This file is part of the Echo Web Application Framework (hereinafter "Echo").
 * Copyright (C) 2002-2005 NextApp, Inc.
 *
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 */

package nextapp.echo2.webrender;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import nextapp.echo2.webrender.output.XmlDocument;

/**
 * The outgoing XML message which synchronizes the state of the client to that
 * of the server.
 */
public class ServerMessage extends XmlDocument {
    
    /**
     * Constant for the use with <code>setRootLayoutDirection()</code> 
     * indicating a left-to-right layout direction.
     */
    public static final int LEFT_TO_RIGHT = 0;

    /**
     * Constant for the use with <code>setRootLayoutDirection()</code> 
     * indicating a right-to-left layout direction.
     */
    public static final int RIGHT_TO_LEFT = 1;

    /**
     * A hash table associating <code>ItemizedDirectiveLookupKey</code>s to
     * corresponding <code>Element</code>s in the message.
     */
    private Map itemizedDirectivesMap = new HashMap();

    /**
     * Representation of the information required to look up a suitable Itemized
     * Directive <code>Element</code>. Instances are used as keys in the
     * <code>itemizedDirectivesMap</code>. This class provides a
     * <code>getHashCode()</code> implementation for efficient lookups.
     */
    private class ItemizedDirectiveLookupKey {

        String groupId;
        String processor;
        String directiveName;
        String[] keyAttributeNames;
        String[] keyAttributeValues;
        int hashCode;

        /**
         * Creates an <code>ItemizedDirectiveLookupKey</code> based on the
         * provided description.
         * 
         * @param groupId the identifier of the target message part group
         * @param processor the name of the client-side processor object which
         *        will process the message part containing the directive, e.g.,
         *        "EchoEventUpdate", or "EchoDomUpdate"
         * @param directiveName the name of the directive, e.g., "event-add" or
         *        "dom-remove".
         * @param keyAttributeNames
         * @param keyAttributeValues
         */
        private ItemizedDirectiveLookupKey(String groupId, String processor, String directiveName, String[] keyAttributeNames,
                String[] keyAttributeValues) {
            super();
            this.groupId = groupId;
            this.processor = processor;
            this.directiveName = directiveName;
            this.keyAttributeNames = keyAttributeNames;
            this.keyAttributeValues = keyAttributeValues;
            this.hashCode = groupId.hashCode() ^ processor.hashCode() ^ directiveName.hashCode();
            for (int i = 0; i < keyAttributeNames.length; ++i) {
                this.hashCode ^= keyAttributeNames[i].hashCode() ^ keyAttributeValues[i].hashCode();
            }
        }

        /**
         * @see java.lang.Object#equals(java.lang.Object)
         */
        public boolean equals(Object o) {
            if (!(o instanceof ItemizedDirectiveLookupKey)) {
                return false;
            }
            ItemizedDirectiveLookupKey that = (ItemizedDirectiveLookupKey) o;

            if (!this.groupId.equals(that.groupId)) {
                return false;
            }
            if (!this.processor.equals(that.processor)) {
                return false;
            }
            if (!this.directiveName.equals(that.directiveName)) {
                return false;
            }
            if (this.keyAttributeNames.length != that.keyAttributeNames.length) {
                return false;
            }
            for (int i = 0; i < keyAttributeValues.length; ++i) {
                if (!(this.keyAttributeValues[i].equals(that.keyAttributeValues[i]))) {
                    return false;
                }
            }
            for (int i = 0; i < keyAttributeNames.length; ++i) {
                if (!(this.keyAttributeNames[i].equals(that.keyAttributeNames[i]))) {
                    return false;
                }
            }

            return true;
        }

        /**
         * @see java.lang.Object#hashCode()
         */
        public int hashCode() {
            return hashCode;
        }
    }

    /**
     * Constant for the "init" message part group. Message parts in this group are
     * processed before the "preremove", "remove" "update", and "postupdate"
     * groups.
     */
    public static final String GROUP_ID_INIT = "init";

    /**
     * Constant for the "preremove" message part group. Message parts in this group
     * are processed after the "init" group. Message parts in this group are
     * processed before the "remove", "update" and "postupdate" groups.
     */
    public static final String GROUP_ID_PREREMOVE = "preremove";

    /**
     * Constant for the "remove" message part group. Message parts in this group
     * are processed after the "init" and "preremove" groups. Message parts in
     * this group are processed before the "update" and "postupdate" groups.
     * This group is used for removing elements from the DOM.
     */
    public static final String GROUP_ID_REMOVE = "remove";

    /**
     * Constant for the "update" message part group. Message parts in this group
     * are processed after the "init", "preremove" and "remove" groups.
     * Message parts in this group are processed before the "postupdate" group.
     * This group is used for adding elements to the DOM.
     */
    public static final String GROUP_ID_UPDATE = "update";

    /**
     * Constant for the "postupdate" message part group. Message parts in this
     * group are processed after the "init", "preremove", "remove" and "update"
     * groups.
     */
    public static final String GROUP_ID_POSTUPDATE = "postupdate";

    /** Set of added script libraries. */
    private Set addedLibraries;

    /**
     * DOM <code>libraries</code> Element to which <code>library</code>
     * elements are added to represent individual dynamically loaded JavaScript
     * libraries.
     */
    private Element librariesElement;

    /** Root DOM <code>server-message</code> element. */
    private Element serverMessageElement;

    /**
     * Creates a new <code>ServerMessage</code>.
     */
    public ServerMessage() {
        super("server-message", null, null, "http://www.nextapp.com/products/echo2/svrmsg/servermessage");
        Document document = getDocument();
        serverMessageElement = document.getDocumentElement();
        librariesElement = document.createElement("libraries");
        serverMessageElement.appendChild(librariesElement);

        // Add basic part groups.
        addPartGroup(GROUP_ID_INIT);
        addPartGroup(GROUP_ID_PREREMOVE);
        addPartGroup(GROUP_ID_REMOVE);
        addPartGroup(GROUP_ID_UPDATE);
        addPartGroup(GROUP_ID_POSTUPDATE);
    }

    /**
     * Adds a JavaScript library service to be dynamically loaded.
     * 
     * @param serviceId the id of the service to load (the service must return
     *        JavaScript code with content-type "text/javascript")
     */
    public void addLibrary(String serviceId) {
        if (addedLibraries == null) {
            addedLibraries = new HashSet();
        }
        if (addedLibraries.contains(serviceId)) {
            return;
        }
        Element libraryElement = getDocument().createElement("library");
        libraryElement.setAttribute("service-id", serviceId);
        librariesElement.appendChild(libraryElement);
        addedLibraries.add(serviceId);
    }

    /**
     * Adds a "group" to the document. Part groups enable certain groups of
     * operations, e.g., remove operations, to be performed before others, e.g.,
     * add operations.
     * 
     * @param groupId the identifier of the group
     * @return the created "message-part-group" element.
     */
    public Element addPartGroup(String groupId) {
        Element messagePartGroupElement = getDocument().createElement("message-part-group");
        messagePartGroupElement.setAttribute("id", groupId);
        serverMessageElement.appendChild(messagePartGroupElement);
        return messagePartGroupElement;
    }

    /**
     * Retrieves the "message-part-group" element pertaining to a specific group.
     * 
     * @param groupId the id of the group
     * @return the "message-part-group" element
     */
    public Element getPartGroup(String groupId) {
        NodeList groupList = serverMessageElement.getElementsByTagName("message-part-group");
        int length = groupList.getLength();
        for (int i = 0; i < length; ++i) {
            Element groupElement = (Element) groupList.item(i);
            if (groupId.equals(groupElement.getAttribute("id"))) {
                return groupElement;
            }
        }
        return null;
    }

    /**
     * Adds a "message-part" to the document that will be processed by the
     * specified client-side processor object.
     * 
     * @param groupId the id of the group to which the "message-part" element
     *        should be added
     * @param processor the name of the client-side processor object which will
     *        process the message part, e.g., "EchoEventUpdate", or
     *        "EchoDomUpdate"
     * @return the created "message-part" element
     */
    public Element addPart(String groupId, String processor) {
        Element messagePartGroupElement = getPartGroup(groupId);
        Element messagePartElement = getDocument().createElement("message-part");
        messagePartElement.setAttribute("processor", processor);
        messagePartGroupElement.appendChild(messagePartElement);
        return messagePartElement;
    }

    /**
     * Creates and appends a directive element beneath to a message part.
     * Attempts to append the directive to an existing message part if the last
     * message part in the specified group happens to have the same processor as
     * is specified by the <code>processor</code> argument. If this is not
     * possible, a new "message-part" element is created and the directive is
     * added to it.
     * 
     * @param groupId
     * @param processor the name of the client-side processor object which will
     *        process the message part, e.g., "EchoEventUpdate", or
     *        "EchoDomUpdate"
     * @param directiveName the name of the directive, e.g., "event-add" or
     *        "dom-remove".
     * @return the directive element
     */
    public Element appendPartDirective(String groupId, String processor, String directiveName) {
        Element messagePartElement = null;
        Element groupElement = getPartGroup(groupId);

        Element lastChild = (Element) groupElement.getLastChild();
        if (lastChild != null && processor.equals(lastChild.getAttribute("processor"))) {
            messagePartElement = lastChild;
        } else {
            messagePartElement = addPart(groupId, processor);
        }
        
        Element directiveElement = getDocument().createElement(directiveName);
        messagePartElement.appendChild(directiveElement);
        return directiveElement;
    }

    /**
     * Creates or retrieves a suitable "Itemized Directive" element. Itemized
     * Directives may be used to create more bandwidth-efficient ServerMessage
     * output in cases where a particular operation may need to be performed on
     * a significant number of targets. In the case that the directive must be
     * created, it will be added to the message. Repeated invocations of this
     * method with equivalent values of all parameters will result in the same
     * directive being returned each time.
     * 
     * This method should only be used for adding directives to the 
     * <code>GROUP_ID_PREREMOVE</code> and <code>GROUP_ID_POSTUPDATE</code>
     * groups as itemized directives will not be executed in-order, which
     * will cause problems if they are used for to manipulate the DOM.
     * 
     * @param groupId the identifier of the target message part group,
     *        either <code>GROUP_ID_PREREMOVE</code> or 
     *        <code>GROUP_ID_POSTUPDATE</code>
     * @param processor the name of the client-side processor object which will
     *        process the message part containing the directive, e.g.,
     *        "EchoEventUpdate", or "EchoDomUpdate"
     * @param directiveName the name of the directive, e.g., "event-add" or
     *        "dom-remove"
     * @param keyAttributeNames the names of the key attributes
     * @param keyAttributeValues the values of the key attributes
     * @return the created/retrieved directive element
     */
    public Element getItemizedDirective(String groupId, String processor, String directiveName, String[] keyAttributeNames,
            String[] keyAttributeValues) {
        ItemizedDirectiveLookupKey itemizedDirectiveLookupKey = new ItemizedDirectiveLookupKey(groupId, processor, directiveName,
                keyAttributeNames, keyAttributeValues);
        Element element = (Element) itemizedDirectivesMap.get(itemizedDirectiveLookupKey);
        if (element == null) {
            element = appendPartDirective(groupId, processor, directiveName);
            for (int i = 0; i < keyAttributeNames.length; ++i) {
                element.setAttribute(keyAttributeNames[i], keyAttributeValues[i]);
            }
            itemizedDirectivesMap.put(itemizedDirectiveLookupKey, element);
        }
        return element;
    }
    
    /**
     * Sets the interval between asynchronous requests to the server to check
     * for server-pushed updates.
     * 
     * @param newValue the new interval in milliseconds (a negative value will
     *        disable asynchronous requests)
     */
    public void setAsynchronousMonitorInterval(int newValue) {
        if (newValue < 0) {
            serverMessageElement.setAttribute("async-interval", "disable");
        } else {
            serverMessageElement.setAttribute("async-interval", Integer.toString(newValue));
        }
    }

    /**
     * Sets the element id of the root of the modal context. Only elements
     * within the modal context will be enabled. A <code>id</code> value of
     * null will disable the modal context, thus allowing ALL elements to be
     * enabled.
     * 
     * @param id the root element id of the modal context
     */
    public void setModalContextRootId(String id) {
        if (id == null) {
            serverMessageElement.setAttribute("modal-id", "");
        } else {
            serverMessageElement.setAttribute("modal-id", id);
        }
    }

    /**
     * Sets the root layout direction of the application, i.e., either
     * <code>LEFT_TO_RIGHT</code> or <code>RIGHT_TO_LEFT</code>.
     * 
     * @param layoutDirection the new layout direction
     */
    public void setRootLayoutDirection(int layoutDirection) {
        switch (layoutDirection) {
        case LEFT_TO_RIGHT:
            serverMessageElement.setAttribute("root-layout-direction", "ltr");
            break;
        case RIGHT_TO_LEFT:
            serverMessageElement.setAttribute("root-layout-direction", "rtl");
            break;
        default:
            throw new IllegalArgumentException("Illegal layout direction.");
        }
    }
}