/*
 * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.xml.internal.ws.policy.sourcemodel;

import com.sun.xml.internal.ws.policy.privateutil.LocalizationMessages;
import com.sun.xml.internal.ws.policy.privateutil.PolicyLogger;
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
import com.sun.xml.internal.ws.policy.sourcemodel.wspolicy.XmlToken;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;

/**
 * The general representation of a single node within a {@link com.sun.xml.internal.ws.policy.sourcemodel.PolicySourceModel} instance.
 * The model node is created via factory methods of the {@link com.sun.xml.internal.ws.policy.sourcemodel.PolicySourceModel} instance.
 * It may also hold {@link com.sun.xml.internal.ws.policy.sourcemodel.AssertionData} instance in case its type is {@code ModelNode.Type.ASSERTION}.
 *
 * @author Marek Potociar
 */
public final class ModelNode implements Iterable<ModelNode>, Cloneable {
    private static final PolicyLogger LOGGER = PolicyLogger.getLogger(ModelNode.class);

    /**
     * Policy source model node type enumeration
     */
    public static enum Type {
        POLICY(XmlToken.Policy),
        ALL(XmlToken.All),
        EXACTLY_ONE(XmlToken.ExactlyOne),
        POLICY_REFERENCE(XmlToken.PolicyReference),
        ASSERTION(XmlToken.UNKNOWN),
        ASSERTION_PARAMETER_NODE(XmlToken.UNKNOWN);

        private XmlToken token;

        Type(XmlToken token) {
            this.token = token;
        }

        public XmlToken getXmlToken() {
            return token;
        }

        /**
         * Method checks the PSM state machine if the creation of new child of given type is plausible for a node element
         * with type set to this type instance.
         *
         * @param childType The type.
         * @return True if the type is supported, false otherwise
         */
        private boolean isChildTypeSupported(final Type childType) {
            switch (this) {
                case POLICY:
                case ALL:
                case EXACTLY_ONE:
                    switch (childType) {
                        case ASSERTION_PARAMETER_NODE:
                            return false;
                        default:
                            return true;
                    }
                case POLICY_REFERENCE:
                    return false;
                case ASSERTION:
                    switch (childType) {
                        case POLICY:
                        case POLICY_REFERENCE:
                        case ASSERTION_PARAMETER_NODE:
                            return true;
                        default:
                            return false;
                    }
                case ASSERTION_PARAMETER_NODE:
                    switch (childType) {
                        case ASSERTION_PARAMETER_NODE:
                            return true;
                        default:
                            return false;
                    }
                default:
                    throw LOGGER.logSevereException(new IllegalStateException(
                            LocalizationMessages.WSP_0060_POLICY_ELEMENT_TYPE_UNKNOWN(this)));
            }
        }
    }

    // comon model node attributes
    private LinkedList<ModelNode> children;
    private Collection<ModelNode> unmodifiableViewOnContent;
    private final ModelNode.Type type;
    private ModelNode parentNode;
    private PolicySourceModel parentModel;

    // attributes used only in 'POLICY_REFERENCE' model node
    private PolicyReferenceData referenceData;
    private PolicySourceModel referencedModel;

    // attibutes used only in 'ASSERTION' or 'ASSERTION_PARAMETER_NODE' model node
    private AssertionData nodeData;

    /**
     * The factory method creates and initializes the POLICY model node and sets it's parent model reference to point to
     * the model supplied as an input parameter. This method is intended to be used ONLY from {@link PolicySourceModel} during
     * the initialization of its own internal structures.
     *
     * @param model policy source model to be used as a parent model of the newly created {@link ModelNode}. Must not be {@code null}
     * @return POLICY model node with the parent model reference initialized to the model supplied as an input parameter
     * @throws IllegalArgumentException if the {@code model} input parameter is {@code null}
     */
    static ModelNode createRootPolicyNode(final PolicySourceModel model) throws IllegalArgumentException {
        if (model == null) {
            throw LOGGER.logSevereException(new IllegalArgumentException(LocalizationMessages.WSP_0039_POLICY_SRC_MODEL_INPUT_PARAMETER_MUST_NOT_BE_NULL()));
        }
        return new ModelNode(ModelNode.Type.POLICY, model);
    }

    private ModelNode(Type type, PolicySourceModel parentModel) {
        this.type = type;
        this.parentModel = parentModel;
        this.children = new LinkedList<ModelNode>();
        this.unmodifiableViewOnContent = Collections.unmodifiableCollection(this.children);
    }

    private ModelNode(Type type, PolicySourceModel parentModel, AssertionData data) {
        this(type, parentModel);

        this.nodeData = data;
    }

    private ModelNode(PolicySourceModel parentModel, PolicyReferenceData data) {
        this(Type.POLICY_REFERENCE, parentModel);

        this.referenceData = data;
    }

    private void checkCreateChildOperationSupportForType(final Type type) throws UnsupportedOperationException {
        if (!this.type.isChildTypeSupported(type)) {
            throw LOGGER.logSevereException(new UnsupportedOperationException(LocalizationMessages.WSP_0073_CREATE_CHILD_NODE_OPERATION_NOT_SUPPORTED(type, this.type)));
        }
    }

    /**
     * Factory method that creates new policy source model node as specified by a factory method name and input parameters.
     * Each node is created with respect to its enclosing policy source model.
     *
     * @return A new Policy node.
     */
    public ModelNode createChildPolicyNode() {
        checkCreateChildOperationSupportForType(Type.POLICY);

        final ModelNode node = new ModelNode(ModelNode.Type.POLICY, parentModel);
        this.addChild(node);

        return node;
    }

    /**
     * Factory method that creates new policy source model node as specified by a factory method name and input parameters.
     * Each node is created with respect to its enclosing policy source model.
     *
     * @return A new All node.
     */
    public ModelNode createChildAllNode() {
        checkCreateChildOperationSupportForType(Type.ALL);

        final ModelNode node = new ModelNode(ModelNode.Type.ALL, parentModel);
        this.addChild(node);

        return node;
    }

    /**
     * Factory method that creates new policy source model node as specified by a factory method name and input parameters.
     * Each node is created with respect to its enclosing policy source model.
     *
     * @return A new ExactlyOne node.
     */
    public ModelNode createChildExactlyOneNode() {
        checkCreateChildOperationSupportForType(Type.EXACTLY_ONE);

        final ModelNode node = new ModelNode(ModelNode.Type.EXACTLY_ONE, parentModel);
        this.addChild(node);

        return node;
    }

    /**
     * Factory method that creates new policy source model node as specified by a factory method name and input parameters.
     * Each node is created with respect to its enclosing policy source model.
     *
     * @return A new policy assertion node.
     */
    public ModelNode createChildAssertionNode() {
        checkCreateChildOperationSupportForType(Type.ASSERTION);

        final ModelNode node = new ModelNode(ModelNode.Type.ASSERTION, parentModel);
        this.addChild(node);

        return node;
    }

    /**
     * Factory method that creates new policy source model node as specified by a factory method name and input parameters.
     * Each node is created with respect to its enclosing policy source model.
     *
     * @param nodeData The policy assertion data.
     * @return A new policy assertion node.
     */
    public ModelNode createChildAssertionNode(final AssertionData nodeData) {
        checkCreateChildOperationSupportForType(Type.ASSERTION);

        final ModelNode node = new ModelNode(Type.ASSERTION, parentModel, nodeData);
        this.addChild(node);

        return node;
    }

    /**
     * Factory method that creates new policy source model node as specified by a factory method name and input parameters.
     * Each node is created with respect to its enclosing policy source model.
     *
     * @return A new assertion parameter node.
     */
    public ModelNode createChildAssertionParameterNode() {
        checkCreateChildOperationSupportForType(Type.ASSERTION_PARAMETER_NODE);

        final ModelNode node = new ModelNode(ModelNode.Type.ASSERTION_PARAMETER_NODE, parentModel);
        this.addChild(node);

        return node;
    }

    /**
     * Factory method that creates new policy source model node as specified by a factory method name and input parameters.
     * Each node is created with respect to its enclosing policy source model.
     *
     * @param nodeData The assertion parameter data.
     * @return A new assertion parameter node.
     */
    ModelNode createChildAssertionParameterNode(final AssertionData nodeData) {
        checkCreateChildOperationSupportForType(Type.ASSERTION_PARAMETER_NODE);

        final ModelNode node = new ModelNode(Type.ASSERTION_PARAMETER_NODE, parentModel, nodeData);
        this.addChild(node);

        return node;
    }

    /**
     * Factory method that creates new policy source model node as specified by a factory method name and input parameters.
     * Each node is created with respect to its enclosing policy source model.
     *
     * @param referenceData The PolicyReference data.
     * @return A new PolicyReference node.
     */
    ModelNode createChildPolicyReferenceNode(final PolicyReferenceData referenceData) {
        checkCreateChildOperationSupportForType(Type.POLICY_REFERENCE);

        final ModelNode node = new ModelNode(parentModel, referenceData);
        this.parentModel.addNewPolicyReference(node);
        this.addChild(node);

        return node;
    }

    Collection<ModelNode> getChildren() {
        return unmodifiableViewOnContent;
    }

    /**
     * Sets the parent model reference on the node and its children. The method may be invoked only on the root node
     * of the policy source model (or - in general - on a model node that dose not reference a parent node). Otherwise an
     * exception is thrown.
     *
     * @param model new parent policy source model to be set.
     * @throws IllegalAccessException in case this node references a parent node (i.e. is not a root node of the model).
     */
    void setParentModel(final PolicySourceModel model) throws IllegalAccessException {
        if (parentNode != null) {
            throw LOGGER.logSevereException(new IllegalAccessException(LocalizationMessages.WSP_0049_PARENT_MODEL_CAN_NOT_BE_CHANGED()));
        }

        this.updateParentModelReference(model);
    }

    /**
     * The method updates the parentModel reference on current model node instance and all of it's children
     *
     * @param model new policy source model reference.
     */
    private void updateParentModelReference(final PolicySourceModel model) {
        this.parentModel = model;

        for (ModelNode child : children) {
            child.updateParentModelReference(model);
        }
    }

    /**
     * Returns the parent policy source model that contains this model node.
     *
     * @return the parent policy source model
     */
    public PolicySourceModel getParentModel() {
        return parentModel;
    }

    /**
     * Returns the type of this policy source model node.
     *
     * @return actual type of this policy source model node
     */
    public ModelNode.Type getType() {
        return type;
    }

    /**
     * Returns the parent referenced by this policy source model node.
     *
     * @return current parent of this policy source model node or {@code null} if the node does not have a parent currently.
     */
    public ModelNode getParentNode() {
        return parentNode;
    }

    /**
     * Returns the data for this policy source model node (if any). The model node data are expected to be not {@code null} only in
     * case the type of this node is ASSERTION or ASSERTION_PARAMETER_NODE.
     *
     * @return the data of this policy source model node or {@code null} if the node does not have any data associated to it
     * attached.
     */
    public AssertionData getNodeData() {
        return nodeData;
    }

    /**
     * Returns the policy reference data for this policy source model node. The policy reference data are expected to be not {@code null} only in
     * case the type of this node is POLICY_REFERENCE.
     *
     * @return the policy reference data for this policy source model node or {@code null} if the node does not have any policy reference data
     * attached.
     */
    PolicyReferenceData getPolicyReferenceData() {
        return referenceData;
    }

    /**
     * The method may be used to set or replace assertion data set for this node. If there are assertion data set already,
     * those are replaced by a new reference and eventualy returned from the method.
     * <p/>
     * This method is supported only in case this model node instance's type is {@code ASSERTION} or {@code ASSERTION_PARAMETER_NODE}.
     * If used from other node types, an exception is thrown.
     *
     * @param newData new assertion data to be set.
     * @return old and replaced assertion data if any or {@code null} otherwise.
     *
     * @throws UnsupportedOperationException in case this method is called on nodes of type other than {@code ASSERTION}
     * or {@code ASSERTION_PARAMETER_NODE}
     */
    public AssertionData setOrReplaceNodeData(final AssertionData newData) {
        if (!isDomainSpecific()) {
            throw LOGGER.logSevereException(new UnsupportedOperationException(LocalizationMessages.WSP_0051_OPERATION_NOT_SUPPORTED_FOR_THIS_BUT_ASSERTION_RELATED_NODE_TYPE(type)));
        }

        final AssertionData oldData = this.nodeData;
        this.nodeData = newData;

        return oldData;
    }

    /**
     * The method specifies whether the model node instance represents assertion related node, it means whether its type
     * is 'ASSERTION' or 'ASSERTION_PARAMETER_NODE'. This is, for example, the way to determine whether the node supports
     * setting a {@link AssertionData} object via {@link #setOrReplaceNodeData(AssertionData)} method or not.
     *
     * @return {@code true} or {@code false} according to whether the node instance represents assertion related node or not.
     */
    boolean isDomainSpecific() {
        return type == Type.ASSERTION || type == Type.ASSERTION_PARAMETER_NODE;
    }

    /**
     * Appends the specified child node to the end of the children list of this node and sets it's parent to reference
     * this node.
     *
     * @param child node to be appended to the children list of this node.
     * @return {@code true} (as per the general contract of the {@code Collection.add} method).
     *
     * @throws NullPointerException if the specified node is {@code null}.
     * @throws IllegalArgumentException if child has a parent node set already to point to some node
     */
    private boolean addChild(final ModelNode child) {
        children.add(child);
        child.parentNode = this;

        return true;
    }

    void setReferencedModel(final PolicySourceModel model) {
        if (this.type != Type.POLICY_REFERENCE) {
            throw LOGGER.logSevereException(new UnsupportedOperationException(LocalizationMessages.WSP_0050_OPERATION_NOT_SUPPORTED_FOR_THIS_BUT_POLICY_REFERENCE_NODE_TYPE(type)));
        }

        referencedModel = model;
    }

    PolicySourceModel getReferencedModel() {
        return referencedModel;
    }

    /**
     * Returns the number of child policy source model nodes. If this model node contains
     * more than {@code Integer.MAX_VALUE} children, returns {@code Integer.MAX_VALUE}.
     *
     * @return the number of children of this node.
     */
    public int childrenSize() {
        return children.size();
    }

    /**
     * Returns true if the node has at least one child node.
     *
     * @return true if the node has at least one child node, false otherwise.
     */
    public boolean hasChildren() {
        return !children.isEmpty();
    }

    /**
     * Iterates through all child nodes.
     *
     * @return An iterator for the child nodes
     */
    public Iterator<ModelNode> iterator() {
        return children.iterator();
    }

    /**
     * An {@code Object.equals(Object obj)} method override. Method ignores the parent source model. It means that two
     * model nodes may be the same even if they belong to different models.
     * <p/>
     * If parent model comparison is desired, it must be accomplished separately. To perform that, the reference equality
     * test is sufficient ({@code nodeA.getParentModel() == nodeB.getParentModel()}), since all model nodes are created
     * for specific model instances.
     */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }

        if (!(obj instanceof ModelNode)) {
            return false;
        }

        boolean result = true;
        final ModelNode that = (ModelNode) obj;

        result = result && this.type.equals(that.type);
        // result = result && ((this.parentNode == null) ? that.parentNode == null : this.parentNode.equals(that.parentNode));
        result = result && ((this.nodeData == null) ? that.nodeData == null : this.nodeData.equals(that.nodeData));
        result = result && ((this.children == null) ? that.children == null : this.children.equals(that.children));

        return result;
    }

    /**
     * An {@code Object.hashCode()} method override.
     */
    @Override
    public int hashCode() {
        int result = 17;

        result = 37 * result + this.type.hashCode();
        result = 37 * result + ((this.parentNode == null) ? 0 : this.parentNode.hashCode());
        result = 37 * result + ((this.nodeData == null) ? 0 : this.nodeData.hashCode());
        result = 37 * result + this.children.hashCode();

        return result;
    }

    /**
     * Returns a string representation of the object. In general, the <code>toString</code> method
     * returns a string that "textually represents" this object.
     *
     * @return  a string representation of the object.
     */
    @Override
    public String toString() {
        return toString(0, new StringBuffer()).toString();
    }

    /**
     * A helper method that appends indented string representation of this instance to the input string buffer.
     *
     * @param indentLevel indentation level to be used.
     * @param buffer buffer to be used for appending string representation of this instance
     * @return modified buffer containing new string representation of the instance
     */
    public StringBuffer toString(final int indentLevel, final StringBuffer buffer) {
        final String indent = PolicyUtils.Text.createIndent(indentLevel);
        final String innerIndent = PolicyUtils.Text.createIndent(indentLevel + 1);

        buffer.append(indent).append(type).append(" {").append(PolicyUtils.Text.NEW_LINE);
        if (type == Type.ASSERTION) {
            if (nodeData == null) {
                buffer.append(innerIndent).append("no assertion data set");
            } else {
                nodeData.toString(indentLevel + 1, buffer);
            }
            buffer.append(PolicyUtils.Text.NEW_LINE);
        } else if (type == Type.POLICY_REFERENCE) {
            if (referenceData == null) {
                buffer.append(innerIndent).append("no policy reference data set");
            } else {
                referenceData.toString(indentLevel + 1, buffer);
            }
            buffer.append(PolicyUtils.Text.NEW_LINE);
        } else if (type == Type.ASSERTION_PARAMETER_NODE) {
            if (nodeData == null) {
                buffer.append(innerIndent).append("no parameter data set");
            }
            else {
                nodeData.toString(indentLevel + 1, buffer);
            }
            buffer.append(PolicyUtils.Text.NEW_LINE);
        }

        if (children.size() > 0) {
            for (ModelNode child : children) {
                child.toString(indentLevel + 1, buffer).append(PolicyUtils.Text.NEW_LINE);
            }
        } else {
            buffer.append(innerIndent).append("no child nodes").append(PolicyUtils.Text.NEW_LINE);
        }

        buffer.append(indent).append('}');
        return buffer;
    }

    @Override
    protected ModelNode clone() throws CloneNotSupportedException {
        final ModelNode clone = (ModelNode) super.clone();

        if (this.nodeData != null) {
            clone.nodeData = this.nodeData.clone();
        }

        // no need to clone PolicyReferenceData, since those are immutable

        if (this.referencedModel != null) {
            clone.referencedModel = this.referencedModel.clone();
        }


        clone.children = new LinkedList<ModelNode>();
        clone.unmodifiableViewOnContent = Collections.unmodifiableCollection(clone.children);

        for (ModelNode thisChild : this.children) {
            clone.addChild(thisChild.clone());
        }

        return clone;
    }

    PolicyReferenceData getReferenceData() {
        return referenceData;
    }
}
