/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

package com.sun.enterprise.tools.admingui.tree;

import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.io.Serializable;
import java.io.IOException;
import java.lang.reflect.Constructor;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.management.ObjectName;
import javax.management.MBeanServer;

import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.iplanet.jato.RequestContext;
import com.iplanet.jato.RequestManager;
import com.iplanet.jato.ViewBeanManager;
import com.iplanet.jato.view.ViewBean;
import com.iplanet.jato.view.TreeViewStateData;
import com.iplanet.jato.util.NonSyncStringBuffer;

import com.sun.enterprise.tools.admingui.util.Util;
import com.sun.enterprise.tools.admingui.util.MBeanUtil;
import com.sun.enterprise.tools.guiframework.exception.FrameworkException;
import com.sun.enterprise.tools.guiframework.view.DescriptorViewBeanBase;

public class IndexTreeNode extends Object implements Serializable {
    
    // Need to have a set of CONSTANT node IDs for tree highlighting.
    // This maps names to IDs, assuming the names are constant.
    private static HashMap idsMap = new HashMap();
    private static int nodeCount = 3; // 1&2 reserved for links outside tree(domains).
    private static String computeID(IndexTreeNode parent, IndexTreeNode child, String name) {
	String key = child.getNamePath();
	String id = (String)idsMap.get(key);
	if (id == null) {
	    nodeCount++;
	    id = (String)(""+nodeCount);
	    idsMap.put(key, id);
	}
	return id;
    }
    
    private String qualifyType(String type) {
        if (type.equalsIgnoreCase(IndexTreeModel.CONTAINER)) {
            return IndexTreeModel.CONTAINER;
        } else if (type.equalsIgnoreCase(IndexTreeModel.LEAF)) {
            return IndexTreeModel.LEAF;
        } else if (type.equalsIgnoreCase(IndexTreeModel.ROOT)) {
            return IndexTreeModel.ROOT;
        }
        throw new RuntimeException("Unknown node type specified. Error likely in treeXML.");
    }

    private String qualifyName(String name) {
        if (name != null && name.length() > 0) {
            return name;
        }
        throw new RuntimeException("Node name must be non-null and non-blank. "
            + "Error likely in treeXML.");
    }
    
    public IndexTreeNode(IndexTreeNode parent, String type, String name, IndexTreeModel model) {
	super();
	this.type = qualifyType(type);
	this.name = qualifyName(name);
	this.id = model.getNextNodeID();
	this.model = model;
        this.link = "underConstruction"; // default page to show if nothing is specified.
        setParent(parent);
        
	setAttribute(IndexTreeModel.FIELD_NAME, name);
	setAttribute(IndexTreeModel.FIELD_ID, getPath());
        
	
	// when creating, needsrefresh is true;
	// on first expansion, needs refresh is turned false.
	// on refresh, needs refresh is true for all container nodes
	setRefresh(true);
    }
    
    public void setParent(IndexTreeNode parent) {
	if (parent == null) {
	    parent = model.getRoot();
	}
	this.parent = parent;
	
	if (parent != null) {
	    parent.addChild(this);
            
            if (parent.getType().equals(IndexTreeModel.LEAF))
                parent.type = IndexTreeModel.CONTAINER;
        }
	this.hid=computeID(parent, this, name); // highlight ID
    }
        
    public void removeChild(String childName) {
	Iterator it = children.iterator();
	while (it.hasNext()) {
	    IndexTreeNode childNode = (IndexTreeNode)it.next();
	    if (childNode.getName().equals(childName)) {
		IndexTreeNode n = childNode.getParent();
		if (n != null) {
		    n.children.remove(childNode);
		    break;
		}
	    }
	}
    }
    
    public IndexTreeNode getChild(String childName) {
	Iterator it = children.iterator();
	while (it.hasNext()) {
	    IndexTreeNode childNode = (IndexTreeNode)it.next();
	    if (childNode.getName().equals(childName)) {
                return childNode;
	    }
	}
        return null;
    }
    
    public IndexTreeNode getChildByUniqueID(String uniqueID) {
	Iterator it = children.iterator();
	while (it.hasNext()) {
	    IndexTreeNode childNode = (IndexTreeNode)it.next();
            if (childNode.getUniqueID() != null) {
                if (childNode.getUniqueID().equals(uniqueID)) {
                    return childNode;
                }
            }
	}
        return null;
    }
    
    public void addChild(IndexTreeNode child) {
        String isCluster = null;
        //Known Issues: Tree is stored in session so if there is a change add/remove a cluster/instance
        //the config node is not updated and displayed properly.  This is not a dynamic node b/c there are no mbeans or children
        //so it does not get updated.  Since we are moving to the JSF tree it does not make sense to
        //have a fix.
        //We have to make this generic for other nodes as well. We will fix this issue later.
        if(child.getName().equalsIgnoreCase("Group Management Service")){
            ObjectName[]  targets = null;
            try {
                targets = (ObjectName[])MBeanUtil.invoke("com.sun.appserv:type=config,name="+child.getParent().getName()+",category=config", "listReferencees", null, null);
                if (targets != null) {
                    for (int j = 0; j < targets.length; j++) {
                        
                        String objectType = targets[j].getKeyProperty("type");
                        isCluster = (objectType.equalsIgnoreCase("cluster"))?"true":"false";
                    }
                }
                if(isCluster.equals("true"))
                    children.add(child);
            }catch (Exception  ex) {
                
                return; //default-config..
                
            }
        } else {
            children.add(child);
        }
    }

    public void setDynamicChild(Element node) {
        dynamicChild = node;
    }
    
    protected void loadParams(Element node, HashMap params) { 
        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
            String nodeName = child.getNodeName();
            if (nodeName.equalsIgnoreCase("parameter")) {
                String name = ((Element)child).getAttribute("name");
                String value = ((Element)child).getAttribute("value");
                if (name != null && value != null) {
                    value = replaceTokens(value);
                    params.put(name, value);
                }
            }
        }
    }
    
    protected IndexTreeNode createNode(Element element, String name, Object objectName) {
        if (getChild(name) != null) {
            return null;
        }

        IndexTreeNode treeNode = createNode(element, this, 
            IndexTreeModelImpl.LEAF, name, model);

	
        if (MBeanUtil.isValidMBean(objectName.toString())) {
            treeNode.setAttribute(OBJECT_NAME, objectName.toString());
        }

        boolean hasChildren = TreeReader.process(element, model, treeNode);
        if (hasChildren) {
            treeNode.type = IndexTreeModel.CONTAINER;
            treeNode.getChildren();
        }
        return treeNode;
    }
    
    public static IndexTreeNode createNode(Element element, IndexTreeNode parent,
        String nodeType, String name, IndexTreeModel model) {
            
        IndexTreeNode treeNode = null;
        String classType = element.getAttribute("classType");
        if (classType == null || classType.length() == 0) {
            treeNode = new IndexTreeNode(parent, nodeType, name, model);
        } else {
            String clazz = TreeReader.getClass(classType);
            if (clazz == null)
                throw new RuntimeException("NodeClassType not specified for: "+classType);
            try {
                Class treeNodeClass = Class.forName(clazz);
                Constructor constructor = treeNodeClass.getConstructor(
                    new Class[] {IndexTreeNode.class, String.class, 
                                 String.class, IndexTreeModel.class});

                treeNode = (IndexTreeNode)constructor.newInstance(
                    new Object[] {parent, nodeType, name, model});
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }
       
        treeNode.setIcon(element.getAttribute("icon"));
        treeNode.setIconExpanded(element.getAttribute("expandIcon"));
        treeNode.setLink(element.getAttribute("link"));
        
        String id = element.getAttribute("id");
        if (id != null && id.length() > 0)
            treeNode.setUniqueID(id);

        if (nodeType.equalsIgnoreCase(model.ROOT))
            model.setRoot(treeNode);
        processAttributes(element, treeNode);
        return treeNode;
    }
    
    private static void processAttributes(Element node, IndexTreeNode treeNode) {
        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
            String nodeName = child.getNodeName();
            if (nodeName.equalsIgnoreCase("attribute")) {
                String name = ((Element)child).getAttribute("name");
                String value = ((Element)child).getAttribute("value");
                if (value == null || value.length() == 0) {
                    ArrayList values = new ArrayList();
                    for (Node n = child.getFirstChild(); n != null; n = n.getNextSibling()) {
                        if (n.getNodeName().equalsIgnoreCase("values")) {
                            String v = ((Element)n).getAttribute("value");
                            if (v != null && v.length() > 0) {
                                v = treeNode.replaceTokens(v);
                                values.add(v);
                            }
                        }
                    }
                    if (values.size() > 0)
                        treeNode.setAttribute(name, values);
                }
                else if (name != null && name.length() > 0 && 
                         value != null && value.length() > 0) {
                    value = treeNode.replaceTokens(value);
                    treeNode.setAttribute(name, value);
                }
            }
        }
    }

   /*private static String getConfigName(String type, String objName) {
        String configName = null;
        try {
            String objectName = "com.sun.appserv:type="+type+",name="+objName+",category=config";
            configName = (String) MBeanUtil.getAttribute(objectName, "config_ref");
        } catch (FrameworkException ex) {
	    if (Util.isLoggableINFO()) {
		Util.logINFO("CAN'T GET CONFIG NAME....... "+ex.getMessage(), ex);
	    }
        }
        return configName;
    }*/
    
    protected String replaceTokens(String str) {
        if (str == null) return str;
        return replaceVariableWithAttribute(str, "$", "(", ")", "");
    }
    // this was copied from the framework.... should call a framework method instead.
    public String replaceVariableWithAttribute(String string, String startToken, 
            String typeDelim, String endToken, String valueForNull) {
	int startIndex = string.lastIndexOf(startToken);
	int delimIndex;
	int endIndex;
	int startTokenLen = startToken.length();
	int delimLen = typeDelim.length();
	int endTokenLen = endToken.length();
	boolean expressionIsWholeString = false;

	for (; startIndex != -1; startIndex = string.lastIndexOf(startToken)) {
	    // Handle special case where string starts with startToken and ends
	    // with endToken (and no replacements inbetween).  This is special
	    // because we don't want to convert the attribute to a string, we
	    // want to return it (this allows Object types).
	    if ((startIndex == 0) && (string.endsWith(endToken))) {
		// This is the special case...
		expressionIsWholeString = true;
	    }

	    // Find first typeDelim
	    delimIndex = string.indexOf(typeDelim, startIndex+startTokenLen);
	    if (delimIndex == -1) {
		continue;
	    }

	    // Next find first end token
	    endIndex = string.indexOf(endToken, delimIndex+delimLen);
	    if (endIndex == -1) {
		continue;
	    }
	    // Pull off the type...
	    //type = string.substring(startIndex+startTokenLen, delimIndex);
            if (startIndex+startTokenLen != delimIndex)
                throw new RuntimeException("types not support in tree expressions.");

	    // Pull off the variable...
	    String variable = string.substring(delimIndex+delimLen, endIndex);

	    // Get the value...
	    Object value = getAttribute(variable);
	    if (value == null) {
		variable = valueForNull;
	    } else if ((value instanceof String) == false) {
                variable = value.toString();
            } else {
                variable = (String) value;
            }
	    if (expressionIsWholeString) {
		return variable;
	    }
	    // Make new string
	    string = string.substring(0, startIndex) +	// Before replacement
	    	     variable +                         // Replacement
		     string.substring(endIndex+endTokenLen); // After
	}
	return string;
    }
    
    protected void removeDeletedNodes(HashSet newNames) {
        // if any of the existing nodes are not in the newNames set, they are deleted.
        if (newNames.size() != this.children.size()) {
            // if the list sizes aren't the same, then check for deleted nodes
            boolean doAgain = true;
            while (doAgain) {
                doAgain = false;
                for (int i=0; i<this.children.size(); i++) {
                    if (newNames.contains(((IndexTreeNode)this.children.get(i)).name) == false) {
                        this.children.remove(i);
                        doAgain = true;
                        break;
                    }
                }
            }
        }
        if (this.children.size() == 0) {
            closeNode(RequestManager.getRequestContext());
        }
    }

    /* returns false if the node corresponding to objectName should not be added to tree.
       if there is an exception trying to validate, still return true */
     protected boolean isChildValid(ObjectName objectName) {
        try {
            String category = objectName.getKeyProperty("category");
            if (category.equalsIgnoreCase("monitor"))
                return true;
            String objectType = objectName.getKeyProperty("type");
            if (objectType.equals("j2ee-application") || 
                objectType.equals("web-module") || 
                objectType.equals("ejb-module") ||
                objectType.equals("connector-module")) {     
                String type = (String)MBeanUtil.getAttribute(objectName, "object_type");
                if (!type.equalsIgnoreCase("user")) {
                    return false;
                }
            }
        } catch (Exception ex) { 
             ex.printStackTrace(); // deliberately ignoring exception
        }
        return true;
    }
    
    protected void ensureChildren() { 
        // nothing to do in the default case, override in derived classes.
    }

    public List getChildren() {
	ensureChildren();
	return children;
    }

    public String toString() {
	return "[name=" + getNamePath() + " path=" + getPath() + " hid=" + getHighlightID() + "]";
    }

    public String getNamePath() {
	if (parent!=null)
	    return parent.getNamePath()+"."+name;
	else
	    return ""+name;
    }

    public String getPath() {
	if (parent!=null)
	    return parent.getPath()+"."+id;
	else
	    return ""+id;
    }
    
    public IndexTreeNode getNextSibling() {
	IndexTreeNode result = null;
	if (parent!=null) {
	    int index=parent.getChildren().indexOf(this);
	    index++;
	    if (index<parent.getChildren().size())
		result=(IndexTreeNode)parent.getChildren().get(index);
	}
	return result;
    }
    
    public void setIsExpanded(boolean expanded) {
        // if the node was OPENED, now is CLOSED, then set the flag to refresh children again.
        if (isExpanded == true && expanded == false) {
            setRefresh(true);
        }
        isExpanded = expanded;
    }        

    public IndexTreeNode getParent() {
	return parent;
    }
    
    public String getHighlightID() {
	return hid;
    }
    
    public String getHighlightIDPath() {
	String path = hid;
	IndexTreeNode p = parent;
	while (p != null) {
	    path += "."+p.getHighlightID();
	    p = p.parent;
	}
	return path;
    }
    
    public String getName() {
	return Util.getMessage(name);
    }
    
    public String getType() {
	return type;
    }
    
    public Object getAttribute(String name) {
        return this.getAttribute(name, true);
    }
    
    public Object getAttribute(String name, boolean lookUpHiearchy) {
 	Object obj = getAttributes().get(name);
	if (lookUpHiearchy == true && obj == null && parent != null)
	    return parent.getAttribute(name);
	return obj;
    }
    
    public void setAttribute(String name, Object value) {
	attributes.put(name,value);
    }
    
    public void setRefresh(boolean refresh) {
	setAttribute(IndexTreeModel.FIELD_REFRESH, new Boolean(refresh));
    }
    
    public boolean getRefresh() {
	return ((Boolean)getAttributes().get(IndexTreeModel.FIELD_REFRESH)).booleanValue();
    }
    
    public void setUniqueID(String uniqueID) {
	setAttribute(IndexTreeModel.FIELD_UNIQUEID, uniqueID);
    }
    
    public String getUniqueID() {
	return (String)getAttributes().get(IndexTreeModel.FIELD_UNIQUEID);
    }
    
    public String getDisplayName() {
        // display name is for special case where we want to show a different
        // name than the node name.
        String displayName = (String)getAttributes().get(IndexTreeModel.FIELD_DISPLAYNAME);
	return (displayName == null)?(getName()):(displayName);
    }
    
    public String getLink() {
	return link;
    }
    
    public void setLink(String link) {
	this.link = link;
    }
    
    public void removeAllChildren() {
	children.clear();
    }
    
    protected Map getAttributes() {
	return attributes;
    }
    
    public String getIconName(boolean expanded) {
	if (expanded && iconExpanded != null && iconExpanded.equals("")==false)
	    return iconExpanded;
	if (icon != null)
	    return icon;
        // return some sort of a default.
        return "object.gif";
    }
    
    public void setIcon(String icon) {
	this.icon = icon;
    }
    
    public void setIconExpanded(String iconExpanded) {
	this.iconExpanded = iconExpanded;
    }
    
    public void setName(String name) {
	this.name = name;
    }
    
    public int getID() {
	return id;
    }
    
    public IndexTreeModel getModel() {
        return model;
    }

    public boolean closeNode(RequestContext rc) {
        boolean closed = false;
        TreeViewStateData data = 
            (TreeViewStateData)rc.getRequest().getSession().getAttribute(
                model.getStateDataName());
        if (data == null)
            return closed;
        
        if (data.isNodeExpanded(getPath())) {
            closed = true;
            data.setNodeExpanded(getPath(), false);
        }
        return closed;
    }
    
    public boolean openNode(RequestContext rc) {
        boolean opened = false;
        TreeViewStateData data = 
            (TreeViewStateData)rc.getRequest().getSession().getAttribute(
                model.getStateDataName());
        if (data == null)
            return opened;

        IndexTreeNode node = this;
        while (node != null && node.getParent() != null) {
            if (data.isNodeExpanded(node.getPath()) == false) {
                opened = true;
                data.setNodeExpanded(node.getPath(), true);
            }
            node = node.getParent();
        }
        return opened;
    }        
    
    private void setRequestAttribute(HttpServletRequest req, String attrName) {
        String attr = (String) this.getAttribute(attrName);
        if (attr == null)
            req.removeAttribute(attrName);
        else
            req.setAttribute(attrName, attr);
    }
    
    private void setRequestAttributes(HttpServletRequest req) {
        req.setAttribute(EDIT_KEY_VALUE, name);
        req.setAttribute(OBJECT_NAME, this.getAttribute(OBJECT_NAME, false));
        req.getSession().setAttribute(EDIT_KEY_VALUE, name);
        
        // this is used for the links-page and referenced directly by the JSP
        req.setAttribute("numberOfChildNodes", new Integer(this.getChildren().size()));
        setRequestAttribute(req, INSTANCE_NAME);
        setRequestAttribute(req, CONFIG_NAME);
        setRequestAttribute(req, CLUSTER_NAME);
        setRequestAttribute(req, NODEAGENT_NAME);
        setRequestAttribute(req, APPLICATION_TYPE);
    }
    
    public static IndexTreeNode validateNode(RequestContext rc, IndexTreeNode node) {
        // Validate the object name of the MBean of the given node. If good, 
        // return the given node. If not, return a parent with a valid 
        // object name. 
        String objectName = (String)node.getAttribute(IndexTreeNode.OBJECT_NAME, false);
        if (objectName == null) // not holding on to any MBean.
            return node;
        boolean valid = MBeanUtil.isValidMBean(objectName);
        if (valid)
            return node;
        IndexTreeNode parent = node.getParent();
        parent.setRefresh(true);  // make the tree view refresh.
        rc.getRequest().setAttribute("refreshTree", new Boolean("true"));
        // recursive call.
        return validateNode(rc, parent);
    }
    
    /**
     *
     */
    public void handleSelection(RequestContext rc) 
            throws ClassNotFoundException, ServletException, IOException {
	if (Util.isLoggableFINER()) {
	    Util.logFINER("ENTERING IndexTreeNode.handleSelection('"+name+"')");
	}
        
        IndexTreeNode selectedNode = validateNode(rc, this);
        model.setCurrentNode(selectedNode);  // <-- maybe not needed?
        Util.setSelectedNode(selectedNode);
        selectedNode.setRequestAttributes(rc.getRequest());
        
        String linkPage = selectedNode.getLink();

        if (linkPage != null) {
	    if (linkPage.toLowerCase().endsWith(".html") ||
		    linkPage.toLowerCase().endsWith(".jsp")) {
		rc.getServletContext().getRequestDispatcher(
		    Util.getLocalizedURL(rc, linkPage)).
			forward( rc.getRequest(), rc.getResponse());
	    } else {
		ViewBeanManager mgr = rc.getViewBeanManager();
 		ViewBean targetView = (ViewBean)mgr.getViewBean(linkPage);
		if ((targetView instanceof DescriptorViewBeanBase)) {
		    DescriptorViewBeanBase viewBean = ((DescriptorViewBeanBase)targetView);
		    viewBean.setAttributes(getAttributes());  // what does this do?
		}
		targetView.forwardTo(rc);
	    }
	} else {
            throw new FrameworkException(
		"The link in the Tree node must not be null!");
        }
	if (Util.isLoggableFINER()) {
	    Util.logFINER("LEAVING IndexTreeNode.handleSelection('"+name+"')");
	}
    }

    public static final String INSTANCE_NAME = "instanceName";
    public static final String CONFIG_NAME = "configName";
    public static final String CLUSTER_NAME = "clusterName";
    public static final String NODEAGENT_NAME = "nodeAgentName";
    public static final String EDIT_KEY_VALUE = "editKeyValue";
    public static final String OBJECT_NAME = "objectName";
    public static final String APPLICATION_TYPE = "ApplicationType";
        
    private int id = 0;
    private IndexTreeNode parent;
    private String name;
    private String type;
    private String hid;
    private String icon = null;
    private String iconExpanded = null;
    private String link = "underConstruction";
    protected Element dynamicChild = null;
    
    protected List children=new ArrayList();
    private Map attributes=new HashMap();
    protected IndexTreeModel model;
    protected boolean isExpanded = false;
}
