/*
 * Copyright (c) 2008, Aaron Digulla
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 * 
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in
 *       the documentation and/or other materials provided with the
 *       distribution.
 *     * Neither the name of Aaron Digulla nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 */
package de.pdark.decentxml;

import java.io.IOException;
import java.util.Collection;

import de.pdark.decentxml.XMLTokenizer.Type;
import de.pdark.decentxml.dtd.DocType;

/**
 * This class represents an XML document.
 * 
 * <p>If you add a {@code ProcessingInstruction} as the first node of the document,
 * it will be converted into an {@code XMLDeclaration}.
 * 
 * <p>Note: The automatic creation of {@code XMLDeclaration} is not perfect; if you
 * manipulate the list of nodes yourself (for example, via {@code getNodes().add()}),
 * then you're on your own.
 * 
 * @author DIGULAA
 *
 */
public class Document extends NodeWithChildren implements Parent
{
    private Element rootNode;
    private XMLDeclaration xmlDeclaration;
    private DocType docType;
    private Namespaces namespaces;
    
    public Document ()
    {
        // Do nothing
    }
    
    public Document (Node... nodes)
    {
        addNodes (nodes);
    }
    
    @Override
    public Document addNode (Node node)
    {
        super.addNode (node);
        return this;
    }
    
    public Document addNode (int index, Node node)
    {
        if (node instanceof Element)
        {
            Element e = (Element)node;
            if (rootNode != null)
            {
                Token startToken = e.getStartToken ();
                if (startToken == null)
                    throw new XMLParseException ("Only one root element allowed per document", rootNode);
                else
                    throw new XMLParseException ("Only one root element allowed per document", startToken);
            }
            
            rootNode = e;
        }
        else if (node instanceof DocType)
        {
            setDocType ((DocType)node);
        }
        else if (node == null)
            throw new NullPointerException ("node is null");
        
        if (index == 0 && xmlDeclaration != null)
            throw new XMLParseException ("It is not allowed to have content before the XML declaration", this, xmlDeclaration);
        
        switch (node.getType ()) //@COBEX
        {
        case COMMENT:
        case ELEMENT:
        case PROCESSING_INSTRUCTION:
        case CUSTOM_ELEMENT:
        case DOCTYPE:
            break;
        
        case TEXT: {
            Text t = (Text)node;
            if (t.isWhitespace ())
                break;
            }
            // Fall through
            
        default:
            Node sibling = null;
            if (nodeCount () > 0)
            {
                if (index == nodeCount ())
                    index --;
                sibling = getNode (index);
            }
            if (node instanceof BasicNode && ((BasicNode)node).getToken () != null)
            {
                throw new XMLParseException (node.getType ()+" node is not allowed here", ((BasicNode)node).getToken ());
            }
            throw new XMLParseException (node.getType ()+" node is not allowed here", this, sibling);
        }
        
        super.addNode (index, node);
        
        if (node.getType () == Type.PROCESSING_INSTRUCTION)
        {
            ProcessingInstruction pi = (ProcessingInstruction)node;
            
            if (XMLDeclaration.isXMLDeclaration (pi))
            {
                if (index == 0)
                    parseXMLDeclaration ();
                else
                {
                    String msg = "An XML declaration is only allowed as the first node in the document";
                    if (pi.getToken () != null)
                        throw new XMLParseException (msg, pi.getToken ());
                    
                    throw new XMLParseException (msg, this, pi);
                }
            }
        }
        
        return this;
    }
    
    @Override
    public Document addNodes (Collection<? extends Node> nodes)
    {
        super.addNodes (nodes);
        return this; 
    }
    
    @Override
    public Document addNodes (int index, Collection<? extends Node> nodes)
    {
        super.addNodes (index, nodes);
        return this;
    }
    
    @Override
    public Document addNodes (Node... nodes)
    {
        super.addNodes (nodes);
        return this; 
    }
    
    @Override
    public Document addNodes (int index, Node... nodes)
    {
        super.addNodes (index, nodes);
        return this; 
    }
    
    @Override
    public Node removeNode (int index)
    {
        Node n = getNode (index);
        if (n.equals (xmlDeclaration))
            clearXMLDeclaration ();
        else if (n.equals (rootNode))
            rootNode = null;
        
        return super.removeNode (index);
    }

    @Override
    public boolean removeNode (Node n)
    {
        if (n != null)
        {
            if (n.equals (xmlDeclaration))
                clearXMLDeclaration ();
            else if (n.equals (rootNode))
                rootNode = null;
        }

        return super.removeNode (n);
    }
    
    public Element getChild (String path)
    {
        return getChild (path, null);
    }
    
    public Element getChild (String path, Namespace ns)
    {
        if (getRootElement () == null)
            return null;

        if (path.startsWith ("/"))
            path = path.substring (1);
        
        int pos = path.indexOf ('/');
        if (pos == -1)
        {
            if (ns == null)
            {
                pos = path.indexOf (':');
            }
            
            if (path.equals (getRootElement ().getName ())
                && (ns == null || ns.equals (getRootElement ().getNamespace ()))
            )
                return getRootElement ();
            else
                return null;
        }
        
        String name = path.substring (0, pos);
        int pos2 = name.indexOf (':');
        Namespace ns2 = null;
        if (pos2 != -1)
        {
            String prefix = name.substring (0, pos2);
            name = name.substring (pos2 + 1);
            ns2 = getNamespaces ().getNamespace (prefix);
            if (ns2 == null)
                throw new XMLParseException ("Namespace prefix '"+prefix+"' is not defined");
        }
        if (!name.equals (getRootElement ().getName ())
            || (ns2 != null && !ns2.equals (getRootElement ().getNamespace ()))
        )
            return null;
        
        return getRootElement ().getChild (path.substring (pos+1), ns);
    }
    
    public Type getType ()
    {
        return Type.DOCUMENT;
    }
    
    public Element getRootElement ()
    {
        return rootNode;
    }
    
    public Document setRootNode (Element rootNode)
    {
        if (this.rootNode != null)
        {
            removeNode (this.rootNode);
        }
        
        this.rootNode = null;
        
        if (rootNode.getParent () != null)
        {
            rootNode.getParent ().removeNode (rootNode);
        }
        
        super.addNode (rootNode);
        
        return this;
    }
    
    public XMLDeclaration getXmlDeclaration ()
    {
        return xmlDeclaration;
    }
    
    public void setXmlDeclaration (XMLDeclaration xmlDeclaration)
    {
        this.xmlDeclaration = xmlDeclaration;
    }
    
    public String getVersion ()
    {
        return xmlDeclaration == null ? null : xmlDeclaration.getVersion ();
    }
    
    public Document setVersion (String version)
    {
        createXMLDeclaration ().setVersion (version == null ? "1.0" : version);
        return this;
    }
    
    protected XMLDeclaration createXMLDeclaration ()
    {
        if (xmlDeclaration == null)
        {
            addNode (0, new XMLDeclaration ("1.0"));
            addNode (1, new Text ("\n"));
        }
        return xmlDeclaration;
    }

    public String getEncoding ()
    {
        return xmlDeclaration == null ? null : xmlDeclaration.getEncoding ();
    }
    
    public Document setEncoding (String encoding)
    {
        createXMLDeclaration ().setEncoding (encoding);
        return this;
    }
    
    public boolean isStandalone ()
    {
        return xmlDeclaration == null ? false : xmlDeclaration.isStandalone ();
    }
    
    public void setStandalone (boolean standalone)
    {
        createXMLDeclaration ().setStandalone (standalone);
    }

    @Override
    public Document toXML (XMLWriter writer) throws IOException
    {
        super.toXML (writer);
        return this;
    }
    
    /** Parse a possible XML declaration and fill the internal fields with the data. */
    public void parseXMLDeclaration ()
    {
        if (getNodes ().isEmpty ())
            return;

        Node n = getNode (0);
        if (n.getType () != Type.PROCESSING_INSTRUCTION
            || n == xmlDeclaration)
            return;
        
        ProcessingInstruction pi = (ProcessingInstruction)n;
        if (xmlDeclaration != null)
        {
            xmlDeclaration.parseXMLDeclaration (pi.getValue ());
        }
        else
        {
            try
            {
                xmlDeclaration = XMLDeclaration.parseXMLDeclaration (pi);
            }
            catch (XMLParseException e)
            {
                XMLParseException ex = new XMLParseException (e.getMessageWithoutLocation (), e);
                if (pi.getToken () == null)
                    ex.setNode (this, pi);
                else
                    ex.setSource (pi.getToken ().getSource (), pi.getStartOffset () + e.getOffset ());
                throw ex;
            }
            getNodes ().set (0, xmlDeclaration);
        }
    }

    protected void clearXMLDeclaration ()
    {
        xmlDeclaration = null;
    }

    public DocType getDocType ()
    {
        return docType;
    }
    
    public void setDocType (DocType docType)
    {
        this.docType = docType;
    }
    
    public Namespaces getNamespaces ()
    {
        if (namespaces == null)
            namespaces = new Namespaces ();
        
        return namespaces;
    }
    
    public void setNamespaces (Namespaces namespaces)
    {
        this.namespaces = namespaces;
    }

    public Namespace getNamespace (String prefix)
    {
        if (prefix == null || prefix.length () == 0)
            return Namespace.NO_NAMESPACE;
        
        if ("xml".equals (prefix))
            return Namespace.XML_NAMESPACE;
        
        return getNamespaces ().getNamespace (prefix);
    }

    public Document createClone ()
    {
        return new Document ();
    }
    
    public Document copy (Node orig)
    {
        super.copy (orig);
        
        Document other = (Document)orig;
        
        this.xmlDeclaration = other.xmlDeclaration.copy ();
        this.docType = other.docType;
        if (other.namespaces != null)
            this.namespaces = other.namespaces.copy ();
        
        return this;
    }

    public Document copy ()
    {
        return (Document)super.copy ();
    }
    
    public TreeIterator iterator ()
    {
        return new TreeIterator (this);
    }
}
