/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * 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.1 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.
 */

package org.teiid.query.mapping.xml;

import java.io.PrintStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.teiid.core.TeiidRuntimeException;
import org.teiid.query.QueryPlugin;
import org.teiid.query.sql.symbol.ElementSymbol;


/**
 * There will be a mapping node
 * for every entity (element or attribute) in a target XML document.
 * @see MappingNodeConstants
 */
public abstract class MappingNode implements Cloneable, Serializable {

	private static final long serialVersionUID = 6761829541871178451L;

	/** The parent of this node, null if root. */
    private MappingNode parent;

    /** Child nodes, usually just 1 or 2, but occasionally more */
    private List<MappingNode> children = new LinkedList<MappingNode>();

    /** node properties, as defined in NodeConstants.Properties. */
    private Map<MappingNodeConstants.Properties, Object> nodeProperties;

    /** default constructor */
    public MappingNode(){
    }

    /**
     * Get the parent of this node.
     */
    public MappingNode getParent() {
        return this.parent;
    }
    
    public static MappingNode findNode(MappingNode root, String partialName) {
    	return findNode(root, partialName, 0);
    }
    
    public static MappingNode findNode(MappingNode root, String partialName, int start) {
        String canonicalName = root.getName();
        
        if (canonicalName != null) {
            int offset = partialName.charAt(start)=='@'?1:0;
            
            if (partialName.startsWith(canonicalName, start + offset)) { 
                if (partialName.length() - start > canonicalName.length() + 1 && partialName.charAt(start + canonicalName.length()) == '.') {
                    start += canonicalName.length() + 1;
                } else if (partialName.length() - start == canonicalName.length() + offset) {
                    return root;
                } else {
                	return null;
                }
            } else {
            	return null;
            }
        }
        
        for (Iterator<MappingNode> i = root.getChildren().iterator(); i.hasNext();) {
            MappingNode child = i.next();
            MappingNode found = findNode(child, partialName, start);
            if (found != null) {
                return found;
            }
        }
        
        return null;
    }

    /**
     * Set the parent of this node.  This method is restricted, as
     * it should be called only when {@link #addChild adding a child node}
     */
    void setParent( MappingNode parent ) {
        this.parent = parent;
    }

    /**
     * Get the children contained by this node, or an empty List
     * @return children; if no children, return empty List (never null)
     */
    public List<MappingNode> getChildren(){
        return this.children;
    }

    /**
     * Get all children of this node of a specified target node type.  The value
     * of node type should be one of {@link #ATTRIBUTE} or {@link #ELEMENT}.
     */
    public List<MappingNode> getChildren( String type ) {
        List<MappingNode> subset = new ArrayList<MappingNode>();
        Iterator<MappingNode> iter = children.iterator();
        while ( iter.hasNext() ) {
            MappingNode node = iter.next();
            if ( node.getProperty(MappingNodeConstants.Properties.NODE_TYPE).equals(type) ) {
                subset.add( node );
            }
        }
        return subset;
    }
    
    public List<MappingNode> getNodeChildren() {
        List<MappingNode> subset = new ArrayList<MappingNode>();
        Iterator<MappingNode> iter = children.iterator();
        while ( iter.hasNext() ) {
            MappingNode node = iter.next();
            if ( !node.getProperty(MappingNodeConstants.Properties.NODE_TYPE).equals(MappingNodeConstants.ATTRIBUTE) ) {
                subset.add( node );
            }
        }
        return subset;
    }    

    /**
     * Add a child mapping node to this one.
     * @param MappingNode to add as a child of this node
     * @return the added node, with parent set to this node
     */
    public MappingNode addChild( MappingNode node ) {
        this.children.add( node );
        node.setParent(this);
        return node;
    }

    /**
     * Retrieve one of the Object values, keyed off the
     * Integer property keys defined in
     * {@link MappingNodeConstants.Properties}
     * @param propertyID Integer property key
     * @return Object value
     */
    public Object getProperty(MappingNodeConstants.Properties propertyID) {
        Object value = null;
        if(nodeProperties != null) {
            value = nodeProperties.get(propertyID);
        }
        if (value == null){
            value = MappingNodeConstants.Defaults.DEFAULT_VALUES.get(propertyID);
        }
        return value;
    }

    /**
     * Sets one of the Object values, keyed off the
     * Integer property keys defined in
     * {@link MappingNodeConstants.Properties}
     * @param propertyID Integer property key
     * @param value Object property value
     */
    void setProperty(MappingNodeConstants.Properties propertyID, Object value) {
        if (value != null){
            // Get the default for the property ...
            final Object defaultValue = MappingNodeConstants.Defaults.DEFAULT_VALUES.get(propertyID);
            final Map<MappingNodeConstants.Properties, Object> props = getNodeProperties();      // props is never null
            if ( !value.equals(defaultValue) ) {        // we know value is not null
                // Set the value only if different than the default; note that the 'getProperty'
                // method is returning the default if there isn't a value
                props.put(propertyID, value);
            } else {
                // The value is equal to the default, so because we didn't set it
                // we have to make sure to remove any existing value ...
                props.remove(propertyID);
            }
        }
    }

    void removeProperty(MappingNodeConstants.Properties propertyID) {
        getNodeProperties().remove(propertyID);
    }
    
    /**
     * Returns the actual local properties object, instantiates if necessary.
     * The Map returned is the basis for equality for a MappingNode.
     * <b>Note:</b> Use {@link #getProperties} unless absolutely necessary.
     * @return the actual properties (not including defaults) stored at this
     * object.
     * @see #getProperties
     */
    public Map<MappingNodeConstants.Properties, Object> getNodeProperties(){
        if(nodeProperties == null) {
            nodeProperties = new LinkedHashMap<MappingNodeConstants.Properties, Object>();
        }
        return nodeProperties;
    }


    /**
     * <p>Gets the fully qualified name of this node, this is obtained by concatenating
     * the nodes name to the parents qualified name with a delimiter in between them.</p>
     * @return The fully qualified name of this node.
     */
    public String getFullyQualifiedName() {
    	String myName = getPathName();
    	String parentName = (getParent() == null) ? "" : getParent().getFullyQualifiedName(); //$NON-NLS-1$

    	if(myName == null || myName.equals("")) { //$NON-NLS-1$
    		return parentName;
    	} else if(parentName == null || parentName.equals("")) { //$NON-NLS-1$
            return myName;
        } else {
    		return parentName + MappingNodeConstants.PATH_DELIM + myName;
    	}
    }
    
    public String getName() {
        return (String) this.getProperty(MappingNodeConstants.Properties.NAME);
    }
    
    public String getPathName() {
        return getName();
    }

    // =========================================================================
    // OVERRIDE Object METHODS
    // =========================================================================

    /**
     * Compare the symbol based ONLY on properties (including name), NOT on
     * parent node and children nodes (or lack thereof).
     * @param obj Other object
     * @return True if other obj is a MappingNode (or subclass) and properties
     * are equal
     */
    public boolean equals(Object obj) {
        if(obj == this) {
            return true;
        }

        if(obj != null && obj instanceof MappingNode) {
            return ((MappingNode)obj).getNodeProperties().equals(getNodeProperties());
        }
        return false;
    }

    /**
     * Return a hash code for this symbol.
     * @return Hash code
     */
    public int hashCode() {
        return this.getNodeProperties().hashCode();
    }

    /**
     * Returns a string representation of an instance of this class.
     */
    public String toString() {
        Object name = getProperty(MappingNodeConstants.Properties.NAME);
        Object criteria = getProperty(MappingNodeConstants.Properties.CRITERIA);
        Object prefix = getProperty(MappingNodeConstants.Properties.NAMESPACE_PREFIX);
        Object defaultValue = getProperty(MappingNodeConstants.Properties.DEFAULT_VALUE);
        Object fixedValue = getProperty(MappingNodeConstants.Properties.FIXED_VALUE);
        Object namespaces = getProperty(MappingNodeConstants.Properties.NAMESPACE_DECLARATIONS);

        return "[" + getProperty(MappingNodeConstants.Properties.NODE_TYPE) + "]" //$NON-NLS-1$ //$NON-NLS-2$
            + " name='" + ((prefix != null) ? prefix + ":" : "") + ((name != null) ? name : "")  + "'" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
            //+ " path='" + ((name != null) ? getSchemaPath() : "undefined") + "'"
            +  ((defaultValue != null) ? " default='" + defaultValue + "'" : "") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            +  ((fixedValue != null) ? " fixed='" + fixedValue + "'" : "") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            + " minOccurs=" + getProperty(MappingNodeConstants.Properties.CARDINALITY_MIN_BOUND) //$NON-NLS-1$
            + " maxOccurs=" + getProperty(MappingNodeConstants.Properties.CARDINALITY_MAX_BOUND) //$NON-NLS-1$
            + ((criteria != null) ? (" constraint=\"" + criteria + "\"") : "") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            + ((namespaces != null) ? (" namespaces=\"" + namespaces + "\"") : ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    }


    /**
     * Prints the whole tree of MappingNodes to the provided PrintStream.
     */
    public static void printMappingNodeTree(MappingNode root, PrintStream output){
        output.print(toStringNodeTree(root));
    }

    public static String toStringNodeTree(MappingNode root){
        StringBuffer str = new StringBuffer();
        buildTreeString(root, str, 0);
        return str.toString();
    }

    // Define a single tab
    private static final String TAB = "  "; //$NON-NLS-1$

    private static void buildTreeString(MappingNode node, StringBuffer str, int tabLevel){
        setTab(str, tabLevel++);
        str.append(node.toString());
        str.append(node.getNodeProperties());
        str.append("\n"); //$NON-NLS-1$

        Iterator<MappingNode> i = node.getChildren().iterator();
        while (i.hasNext()){
            buildTreeString(i.next(), str, tabLevel);
        }
    }

    private static void setTab(StringBuffer str, int tabStop) {
        for(int i=0; i<tabStop; i++) {
            str.append(TAB);
        }
    }

    /**
     * Starting at a point in a mapping document and traversing either upward toward the root or
     * downward (depth or breadth first), this method finds and returns
     * the first MappingNode instance that has the indicated property key and value.  A <code>null</code>
     * can be passed in for the value parameter, indicating the first node should be returned which has
     * <i>any</i> non-null value for the property key.
     * @param propertyKey any of the properties defined in {@link MappingNodeConstants.Properties}
     * @param value an Object value that is checked for.  <code>null</code> can be passed in, indicating
     * the first node with any non-null value for that property should be returned
     * @param node a MappingNode in a mapping document - this is the beginning point of the search
     * @param searchDirection indicates whether to search upward or downward (depth or breadth first) from the
     * node parameter; use either {@link MappingNodeConstants#SEARCH_UP},
     * {@link MappingNodeConstants#SEARCH_DOWN} or {@link MappingNodeConstants#SEARCH_DOWN_BREADTH_FIRST}
     * @return MappingNode first node found that has the indicated property and value, or null if none found
     */
    static MappingNode findFirstNodeWithProperty(MappingNodeConstants.Properties propertyKey, Object value, MappingNode node, int searchDirection) {
        return findFirstNodeWithPropertyValue(propertyKey, value, false, node, searchDirection);
    }
    
    /**
     * Starting at a point in a mapping document and traversing either upward toward the root or
     * downward (depth or breadth first), this method finds and returns
     * the first MappingNode instance that has the indicated property key and value.  A <code>null</code>
     * can be passed in for the value parameter, indicating the first node should be returned which has
     * <i>any</i> non-null value for the property key.
     * @param propertyKey any of the properties defined in {@link MappingNodeConstants.Properties}
     * for which the value is of type String
     * @param value an Object value that is checked for.  <code>null</code> can be passed in, indicating
     * the first node with any non-null value for that property should be returned
     * @param node a MappingNode in a mapping document - this is the beginning point of the search
     * @param searchDirection indicates whether to search upward or downward (depth or breadth first) from the
     * node parameter; use either {@link MappingNodeConstants#SEARCH_UP},
     * {@link MappingNodeConstants#SEARCH_DOWN} or {@link MappingNodeConstants#SEARCH_DOWN_BREADTH_FIRST}
     * @return MappingNode first node found that has the indicated property and value, or null if none found
     */
    static MappingNode findFirstNodeWithPropertyString(MappingNodeConstants.Properties propertyKey, String value, MappingNode node, int searchDirection) {
        return findFirstNodeWithPropertyValue(propertyKey, value, true, node, searchDirection);
    }

    private static MappingNode findFirstNodeWithPropertyValue(MappingNodeConstants.Properties propertyKey, Object value, boolean isStringValue, MappingNode node, int searchDirection) {
        
        if (node == null || propertyKey == null){
            return null;
        }
        if (searchDirection == MappingNodeConstants.SEARCH_UP){
            return traverseUpForFirstNodeWithPropertyString(propertyKey, value, isStringValue, node);
        } else if (searchDirection == MappingNodeConstants.SEARCH_DOWN){
            return traverseDownForFirstNodeWithPropertyString(propertyKey, value, isStringValue, node, false);
        } else if (searchDirection == MappingNodeConstants.SEARCH_DOWN_BREADTH_FIRST){
            // Check root node first
            if (checkThisNodeForPropertyValue(propertyKey, value, isStringValue, node)){
                return node;
            }
            return traverseDownForFirstNodeWithPropertyString(propertyKey, value, isStringValue, node, true);
        } else {
            throw new IllegalArgumentException(QueryPlugin.Util.getString("ERR.015.002.0009", searchDirection )); //$NON-NLS-1$
        }
    }

    private static MappingNode traverseDownForFirstNodeWithPropertyString(MappingNodeConstants.Properties propertyKey, Object value, boolean isStringValue, MappingNode node, boolean breadthFirst) {
        if (breadthFirst) {
            Iterator<MappingNode> children = node.getChildren().iterator();
            while (children.hasNext()){
                MappingNode child = children.next();
                if (checkThisNodeForPropertyValue(propertyKey, value, isStringValue, child)){
                    return child;
                }
            }
        } else {
            if (checkThisNodeForPropertyValue(propertyKey, value, isStringValue, node)){
                return node;
            }
        }
        
        Iterator<MappingNode> children = node.getChildren().iterator();
        while (children.hasNext()){
            MappingNode child = children.next();
            
            //recursive call to this method
            MappingNode result = traverseDownForFirstNodeWithPropertyString(propertyKey, value, isStringValue, child, breadthFirst);
            if (result != null){
                return result;
            }
        }

        return null;
    }
    
    private static boolean checkThisNodeForPropertyValue(MappingNodeConstants.Properties propertyKey, Object value, boolean isStringValue, MappingNode node) {
        Object thisValue = node.getProperty(propertyKey);
        if (thisValue != null){
            if (value == null){
                return true;
            }
            if (isStringValue &&  ((String)thisValue).equalsIgnoreCase((String)value)) {
                return true;
            }
            if (thisValue.equals(value)) {
                return true;
            }
        }
        return false;
    }

    private static MappingNode traverseUpForFirstNodeWithPropertyString(MappingNodeConstants.Properties propertyKey, Object value, boolean isStringValue, MappingNode node) {
        while (node != null){
            if (checkThisNodeForPropertyValue(propertyKey, value, isStringValue, node)){
                return node;
            }
            node = node.getParent();
        }
        return null;
    }
    
    public MappingNode setExclude(boolean exclude) {
        setProperty(MappingNodeConstants.Properties.IS_EXCLUDED, Boolean.valueOf(exclude));
        return this;
    }

    public boolean isExcluded() {
        Boolean exclude = (Boolean)getProperty(MappingNodeConstants.Properties.IS_EXCLUDED);
        if (exclude != null) {
            return exclude.booleanValue();
        }
        return false;
    }

    public abstract void acceptVisitor(MappingVisitor visitor);
    
    /**
     * Get the source node for this Mapping Node; note that only mapping elements and 
     * mapping attributes have the source nodes; that too ones with specified with
     * NameInSource specified attribute.
     * 
     * If not find nearest one looking up the tree. 
     * @return
     */
    public MappingSourceNode getSourceNode() {
        if (getParent() != null) {
            return getParent().getSourceNode();
        }
        return null;
    }

    public String getNameInSource() {
        return null;        
    }    
    
    public MappingNode clone() {
    	try {
			MappingNode clone = (MappingNode) super.clone();
			clone.children = new ArrayList<MappingNode>(children);
			for (int i = 0; i < clone.children.size(); i++) {
				MappingNode childClone = clone.children.get(i).clone();
				childClone.setParent(clone);
				clone.children.set(i, childClone);
			}
			if (this.nodeProperties != null) {
				this.nodeProperties = new HashMap<MappingNodeConstants.Properties, Object>(this.nodeProperties);
			}
			return clone;
		} catch (CloneNotSupportedException e) {
			 throw new TeiidRuntimeException(QueryPlugin.Event.TEIID30463, e);
		}
    }
    
    public ElementSymbol getElementSymbol() {
    	return null;
    }
}
