/*
 * Copyright (c) 2007 Henri Sivonen
 * Copyright (c) 2008-2017 Mozilla Foundation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a 
 * copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the 
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 */

package nu.validator.htmlparser.xom;

import nu.validator.htmlparser.common.DocumentMode;
import nu.validator.htmlparser.impl.CoalescingTreeBuilder;
import nu.validator.htmlparser.impl.HtmlAttributes;
import nu.xom.Attribute;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Node;
import nu.xom.Nodes;
import nu.xom.ParentNode;
import nu.xom.Text;
import nu.xom.XMLException;

import org.xml.sax.SAXException;

class XOMTreeBuilder extends CoalescingTreeBuilder<Element> {

    private final SimpleNodeFactory nodeFactory;

    private Document document;
    
    private int cachedTableIndex = -1;
    
    private Element cachedTable = null;

    protected XOMTreeBuilder(SimpleNodeFactory nodeFactory) {
        super();
        this.nodeFactory = nodeFactory;
    }

    @Override
    protected void addAttributesToElement(Element element, HtmlAttributes attributes)
            throws SAXException {
        try {
            for (int i = 0; i < attributes.getLength(); i++) {
                String localName = attributes.getLocalNameNoBoundsCheck(i);
                String uri = attributes.getURINoBoundsCheck(i);
                if (element.getAttribute(localName, uri) == null) {
                    element.addAttribute(nodeFactory.makeAttribute(
                            localName,
                            uri,
                            attributes.getValueNoBoundsCheck(i),
                            attributes.getTypeNoBoundsCheck(i) == "ID" ? Attribute.Type.ID
                                    : Attribute.Type.CDATA));
                }
            }
        } catch (XMLException e) {
            fatal(e);
        }
    }

    @Override protected void appendCharacters(Element parent, String text)
            throws SAXException {
        try {
            int childCount = parent.getChildCount();
            Node lastChild;
            if (childCount != 0
                    && ((lastChild = parent.getChild(childCount - 1)) instanceof Text)) {
                Text lastAsText = (Text) lastChild;
                lastAsText.setValue(lastAsText.getValue() + text);
                return;
            }
            parent.appendChild(nodeFactory.makeText(text));
        } catch (XMLException e) {
            fatal(e);
        }
    }

    @Override
    protected void appendChildrenToNewParent(Element oldParent,
            Element newParent) throws SAXException {
        try {
            Nodes children = oldParent.removeChildren();
            for (int i = 0; i < children.size(); i++) {
                newParent.appendChild(children.get(i));
            }
        } catch (XMLException e) {
            fatal(e);
        }
    }

    @Override
    protected void appendComment(Element parent, String comment) throws SAXException {
        try {
            parent.appendChild(nodeFactory.makeComment(comment));
        } catch (XMLException e) {
            fatal(e);
        }
    }

    @Override
    protected void appendCommentToDocument(String comment)
            throws SAXException {
        try {
            Element root = document.getRootElement();
            if ("http://www.xom.nu/fakeRoot".equals(root.getNamespaceURI())) {
                document.insertChild(nodeFactory.makeComment(comment), document.indexOf(root));
            } else {
                document.appendChild(nodeFactory.makeComment(comment));
            }
        } catch (XMLException e) {
            fatal(e);
        }
    }

    @Override
 protected Element createElement(String ns, String name,
            HtmlAttributes attributes, Element intendedParent) throws SAXException {
        try {
            Element rv = nodeFactory.makeElement(name, ns);
            for (int i = 0; i < attributes.getLength(); i++) {
                rv.addAttribute(nodeFactory.makeAttribute(
                        attributes.getLocalNameNoBoundsCheck(i),
                        attributes.getURINoBoundsCheck(i),
                        attributes.getValueNoBoundsCheck(i),
                        attributes.getTypeNoBoundsCheck(i) == "ID" ? Attribute.Type.ID
                                : Attribute.Type.CDATA));
            }
            return rv;
        } catch (XMLException e) {
            fatal(e);
            throw new RuntimeException("Unreachable");
        }
    }

    @Override
 protected Element createHtmlElementSetAsRoot(
            HtmlAttributes attributes) throws SAXException {
        try {
            Element rv = nodeFactory.makeElement("html",
                    "http://www.w3.org/1999/xhtml");
            for (int i = 0; i < attributes.getLength(); i++) {
                rv.addAttribute(nodeFactory.makeAttribute(
                        attributes.getLocalNameNoBoundsCheck(i),
                        attributes.getURINoBoundsCheck(i),
                        attributes.getValueNoBoundsCheck(i),
                        attributes.getTypeNoBoundsCheck(i) == "ID" ? Attribute.Type.ID
                                : Attribute.Type.CDATA));
            }
            document.setRootElement(rv);
            return rv;
        } catch (XMLException e) {
            fatal(e);
            throw new RuntimeException("Unreachable");
        }
    }

    @Override
    protected void detachFromParent(Element element) throws SAXException {
        try {
            element.detach();
        } catch (XMLException e) {
            fatal(e);
        }
    }

    @Override
    protected void appendElement(Element child,
            Element newParent) throws SAXException {
        try {
            child.detach();
            newParent.appendChild(child);
        } catch (XMLException e) {
            fatal(e);
        }
    }

    @Override
    protected boolean hasChildren(Element element) throws SAXException {
        try {
            return element.getChildCount() != 0;
        } catch (XMLException e) {
            fatal(e);
            throw new RuntimeException("Unreachable");
        }
    }

    /**
     * Returns the document.
     * 
     * @return the document
     */
    Document getDocument() {
        Document rv = document;
        document = null;
        return rv;
    }

    Nodes getDocumentFragment() {
        Element rootElt = document.getRootElement();
        Nodes rv = rootElt.removeChildren();
        document = null;
        return rv;
    }

    /**
     * @see nu.validator.htmlparser.impl.TreeBuilder#createElement(String,
     *      java.lang.String, org.xml.sax.Attributes, java.lang.Object)
     */
    @Override
    protected Element createElement(String ns, String name,
            HtmlAttributes attributes, Element form, Element intendedParent) throws SAXException {
        try {
            Element rv = nodeFactory.makeElement(name,
 ns, form);
            for (int i = 0; i < attributes.getLength(); i++) {
                rv.addAttribute(nodeFactory.makeAttribute(
                        attributes.getLocalName(i),
                        attributes.getURINoBoundsCheck(i),
                        attributes.getValueNoBoundsCheck(i),
                        attributes.getTypeNoBoundsCheck(i) == "ID" ? Attribute.Type.ID
                                : Attribute.Type.CDATA));
            }
            return rv;
        } catch (XMLException e) {
            fatal(e);
            throw new RuntimeException("Unreachable");
        }
    }

    /**
     * @see nu.validator.htmlparser.impl.TreeBuilder#start()
     */
    @Override
    protected void start(boolean fragment) throws SAXException {
        document = nodeFactory.makeDocument();
        cachedTableIndex = -1;
        cachedTable = null;
    }

    /**
     * @see nu.validator.htmlparser.impl.TreeBuilder#documentMode(nu.validator.htmlparser.common.DocumentMode,
     *      java.lang.String, java.lang.String, boolean)
     */
    @Override
    protected void documentMode(DocumentMode mode, String publicIdentifier,
            String systemIdentifier)
            throws SAXException {
        if (document instanceof Mode) {
            Mode modal = (Mode) document;
            modal.setMode(mode);
        }
    }

    @Override
    protected Element createAndInsertFosterParentedElement(String ns, String name,
            HtmlAttributes attributes, Element table, Element stackParent) throws SAXException {
        try {
            Node parent = table.getParent();
            Element child = createElement(ns, name, attributes, parent != null ? (Element) parent : stackParent);
            if (parent != null) { // always an element if not null
                ((ParentNode) parent).insertChild(child, indexOfTable(table, stackParent));
                cachedTableIndex++;
            } else {
                stackParent.appendChild(child);
            }
            return child;
        } catch (XMLException e) {
            fatal(e);
            throw new RuntimeException("Unreachable");
        }
    }

    @Override protected void insertFosterParentedCharacters(String text,
            Element table, Element stackParent) throws SAXException {
        try {
            Node parent = table.getParent();
            if (parent != null) { // always an element if not null
                Element parentAsElt = (Element) parent;
                int tableIndex = indexOfTable(table, parentAsElt);
                Node prevSibling;
                if (tableIndex != 0
                        && ((prevSibling = parentAsElt.getChild(tableIndex - 1)) instanceof Text)) {
                    Text prevAsText = (Text) prevSibling;
                    prevAsText.setValue(prevAsText.getValue() + text);
                    return;
                }
                parentAsElt.insertChild(nodeFactory.makeText(text), tableIndex);
                cachedTableIndex++;
                return;
            }
            int childCount = stackParent.getChildCount();
            Node lastChild;
            if (childCount != 0
                    && ((lastChild = stackParent.getChild(childCount - 1)) instanceof Text)) {
                Text lastAsText = (Text) lastChild;
                lastAsText.setValue(lastAsText.getValue() + text);
                return;
            }
            stackParent.appendChild(nodeFactory.makeText(text));
        } catch (XMLException e) {
            fatal(e);
        }
    }

    @Override protected void insertFosterParentedChild(Element child,
            Element table, Element stackParent) throws SAXException {
        try {
            Node parent = table.getParent();
            if (parent != null) { // always an element if not null
                ((ParentNode)parent).insertChild(child, indexOfTable(table, stackParent));
                cachedTableIndex++;
            } else {
                stackParent.appendChild(child);
            }
        } catch (XMLException e) {
            fatal(e);
        }
    }
    
    private int indexOfTable(Element table, Element stackParent) {
        if (table == cachedTable) {
            return cachedTableIndex;
        } else {
            cachedTable = table;
            return (cachedTableIndex = stackParent.indexOf(table));
        }
    }

    /**
     * @see nu.validator.htmlparser.impl.TreeBuilder#end()
     */
    @Override protected void end() throws SAXException {
        cachedTableIndex = -1;
        cachedTable = null;
    }
}
