/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed 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.harmony.xml.dom;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.w3c.dom.DOMException;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Provides a straightforward implementation of the corresponding W3C DOM
 * interface. The class is used internally only, thus only notable members that
 * are not in the original interface are documented (the W3C docs are quite
 * extensive).
 *
 * <p>Some of the fields may have package visibility, so other classes belonging
 * to the DOM implementation can easily access them while maintaining the DOM
 * tree structure.
 *
 * <p>This class represents a Node that has a parent Node as well as
 * (potentially) a number of children.
 *
 * <p>Some code was adapted from Apache Xerces.
 */
public abstract class InnerNodeImpl extends LeafNodeImpl {

    // Maintained by LeafNodeImpl and ElementImpl.
    List<LeafNodeImpl> children = new ArrayList<LeafNodeImpl>();

    protected InnerNodeImpl(DocumentImpl document) {
        super(document);
    }

    public Node appendChild(Node newChild) throws DOMException {
        return insertChildAt(newChild, children.size());
    }

    public NodeList getChildNodes() {
        NodeListImpl list = new NodeListImpl();

        for (NodeImpl node : children) {
            list.add(node);
        }

        return list;
    }

    public Node getFirstChild() {
        return (!children.isEmpty() ? children.get(0) : null);
    }

    public Node getLastChild() {
        return (!children.isEmpty() ? children.get(children.size() - 1) : null);
    }

    public Node getNextSibling() {
        if (parent == null || index + 1 >= parent.children.size()) {
            return null;
        }

        return parent.children.get(index + 1);
    }

    public boolean hasChildNodes() {
        return children.size() != 0;
    }

    public Node insertBefore(Node newChild, Node refChild) throws DOMException {
        LeafNodeImpl refChildImpl = (LeafNodeImpl) refChild;

        if (refChildImpl == null) {
            return appendChild(newChild);
        }

        if (refChildImpl.document != document) {
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, null);
        }

        if (refChildImpl.parent != this) {
            throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
        }

        return insertChildAt(newChild, refChildImpl.index);
    }

    /**
     * Inserts {@code newChild} at {@code index}. If it is already child of
     * another node, it is removed from there.
     */
    Node insertChildAt(Node newChild, int index) throws DOMException {
        if (newChild instanceof DocumentFragment) {
            NodeList toAdd = newChild.getChildNodes();
            for (int i = 0; i < toAdd.getLength(); i++) {
                insertChildAt(toAdd.item(i), index + i);
            }
            return newChild;
        }

        LeafNodeImpl toInsert = (LeafNodeImpl) newChild;
        if (toInsert.document != null && document != null && toInsert.document != document) {
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, null);
        }
        if (toInsert.isParentOf(this)) {
            throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
        }

        if (toInsert.parent != null) {
            int oldIndex = toInsert.index;
            toInsert.parent.children.remove(oldIndex);
            toInsert.parent.refreshIndices(oldIndex);
        }

        children.add(index, toInsert);
        toInsert.parent = this;
        refreshIndices(index);

        return newChild;
    }

    public boolean isParentOf(Node node) {
        LeafNodeImpl nodeImpl = (LeafNodeImpl) node;

        while (nodeImpl != null) {
            if (nodeImpl == this) {
                return true;
            }

            nodeImpl = nodeImpl.parent;
        }

        return false;
    }

    /**
     * Normalize the text nodes within this subtree. Although named similarly,
     * this method is unrelated to Document.normalize.
     */
    @Override
    public final void normalize() {
        Node next;
        for (Node node = getFirstChild(); node != null; node = next) {
            next = node.getNextSibling();
            node.normalize();

            if (node.getNodeType() == Node.TEXT_NODE) {
                ((TextImpl) node).minimize();
            }
        }
    }

    private void refreshIndices(int fromIndex) {
        for (int i = fromIndex; i < children.size(); i++) {
            children.get(i).index = i;
        }
    }

    public Node removeChild(Node oldChild) throws DOMException {
        LeafNodeImpl oldChildImpl = (LeafNodeImpl) oldChild;

        if (oldChildImpl.document != document) {
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, null);
        }
        if (oldChildImpl.parent != this) {
            throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
        }

        int index = oldChildImpl.index;
        children.remove(index);
        oldChildImpl.parent = null;
        refreshIndices(index);

        return oldChild;
    }

    /**
     * Removes {@code oldChild} and adds {@code newChild} in its place. This
     * is not atomic.
     */
    public Node replaceChild(Node newChild, Node oldChild) throws DOMException {
        int index = ((LeafNodeImpl) oldChild).index;
        removeChild(oldChild);
        insertChildAt(newChild, index);
        return oldChild;
    }

    public String getTextContent() throws DOMException {
        Node child = getFirstChild();
        if (child == null) {
            return "";
        }

        Node next = child.getNextSibling();
        if (next == null) {
            return hasTextContent(child) ? child.getTextContent() : "";
        }

        StringBuilder buf = new StringBuilder();
        getTextContent(buf);
        return buf.toString();
    }

    void getTextContent(StringBuilder buf) throws DOMException {
        Node child = getFirstChild();
        while (child != null) {
            if (hasTextContent(child)) {
                ((NodeImpl) child).getTextContent(buf);
            }
            child = child.getNextSibling();
        }
    }

    final boolean hasTextContent(Node child) {
        // TODO: skip text nodes with ignorable whitespace?
        return child.getNodeType() != Node.COMMENT_NODE
                && child.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE;
    }

    void getElementsByTagName(NodeListImpl out, String name) {
        for (NodeImpl node : children) {
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                ElementImpl element = (ElementImpl) node;
                if (matchesNameOrWildcard(name, element.getNodeName())) {
                    out.add(element);
                }
                element.getElementsByTagName(out, name);
            }
        }
    }

    void getElementsByTagNameNS(NodeListImpl out, String namespaceURI, String localName) {
        for (NodeImpl node : children) {
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                ElementImpl element = (ElementImpl) node;
                if (matchesNameOrWildcard(namespaceURI, element.getNamespaceURI())
                        && matchesNameOrWildcard(localName, element.getLocalName())) {
                    out.add(element);
                }
                element.getElementsByTagNameNS(out, namespaceURI, localName);
            }
        }
    }

    /**
     * Returns true if {@code pattern} equals either "*" or {@code s}. Pattern
     * may be {@code null}.
     */
    private static boolean matchesNameOrWildcard(String pattern, String s) {
        return "*".equals(pattern) || Objects.equals(pattern, s);
    }
}
