/*
 * Copyright (c) 2005, 2010, 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.txw2;

import com.sun.xml.internal.txw2.annotation.XmlAttribute;
import com.sun.xml.internal.txw2.annotation.XmlElement;
import com.sun.xml.internal.txw2.annotation.XmlNamespace;
import com.sun.xml.internal.txw2.annotation.XmlValue;
import com.sun.xml.internal.txw2.annotation.XmlCDATA;

import javax.xml.namespace.QName;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * Dynamically implements {@link TypedXmlWriter} interfaces.
 *
 * @author Kohsuke Kawaguchi
 */
final class ContainerElement implements InvocationHandler, TypedXmlWriter {

    final Document document;

    /**
     * Initially, point to the start tag token, but
     * once we know we are done with the start tag, we will reset it to null
     * so that the token sequence can be GC-ed.
     */
    StartTag startTag;
    final EndTag endTag = new EndTag();

    /**
     * Namespace URI of this element.
     */
    private final String nsUri;

    /**
     * When this element can accept more child content, this value
     * is non-null and holds the last child {@link Content}.
     *
     * If this element is committed, this parameter is null.
     */
    private Content tail;

    /**
     * Uncommitted {@link ContainerElement}s form a doubly-linked list,
     * so that the parent can close them recursively.
     */
    private ContainerElement prevOpen;
    private ContainerElement nextOpen;
    private final ContainerElement parent;
    private ContainerElement lastOpenChild;

    /**
     * Set to true if the start eleent is blocked.
     */
    private boolean blocked;

    public ContainerElement(Document document,ContainerElement parent,String nsUri, String localName) {
        this.parent = parent;
        this.document = document;
        this.nsUri = nsUri;
        this.startTag = new StartTag(this,nsUri,localName);
        tail = startTag;

        if(isRoot())
            document.setFirstContent(startTag);
    }

    private boolean isRoot() {
        return parent==null;
    }

    private boolean isCommitted() {
        return tail==null;
    }

    public Document getDocument() {
        return document;
    }

    boolean isBlocked() {
        return blocked && !isCommitted();
    }

    public void block() {
        blocked = true;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(method.getDeclaringClass()==TypedXmlWriter.class || method.getDeclaringClass()==Object.class) {
            // forward to myself
            try {
                return method.invoke(this,args);
            } catch (InvocationTargetException e) {
                throw e.getTargetException();
            }
        }

        XmlAttribute xa = method.getAnnotation(XmlAttribute.class);
        XmlValue xv = method.getAnnotation(XmlValue.class);
        XmlElement xe = method.getAnnotation(XmlElement.class);


        if(xa!=null) {
            if(xv!=null || xe!=null)
                throw new IllegalAnnotationException(method.toString());

            addAttribute(xa,method,args);
            return proxy; // allow method chaining
        }
        if(xv!=null) {
            if(xe!=null)
                throw new IllegalAnnotationException(method.toString());

            _pcdata(args);
            return proxy; // allow method chaining
        }

        return addElement(xe,method,args);
    }

    /**
     * Writes an attribute.
     */
    private void addAttribute(XmlAttribute xa, Method method, Object[] args) {
        assert xa!=null;

        checkStartTag();

        String localName = xa.value();
        if(xa.value().length()==0)
            localName = method.getName();

        _attribute(xa.ns(),localName,args);
    }

    private void checkStartTag() {
        if(startTag==null)
            throw new IllegalStateException("start tag has already been written");
    }

    /**
     * Writes a new element.
     */
    private Object addElement(XmlElement e, Method method, Object[] args) {
        Class<?> rt = method.getReturnType();

        // the last precedence: default name
        String nsUri = "##default";
        String localName = method.getName();

        if(e!=null) {
            // then the annotation on this method
            if(e.value().length()!=0)
                localName = e.value();
            nsUri = e.ns();
        }

        if(nsUri.equals("##default")) {
            // look for the annotation on the declaring class
            Class<?> c = method.getDeclaringClass();
            XmlElement ce = c.getAnnotation(XmlElement.class);
            if(ce!=null) {
                nsUri = ce.ns();
            }

            if(nsUri.equals("##default"))
                // then default to the XmlNamespace
                nsUri = getNamespace(c.getPackage());
        }



        if(rt==Void.TYPE) {
            // leaf element with just a value

            boolean isCDATA = method.getAnnotation(XmlCDATA.class)!=null;

            StartTag st = new StartTag(document,nsUri,localName);
            addChild(st);
            for( Object arg : args ) {
                Text text;
                if(isCDATA)     text = new Cdata(document,st,arg);
                else            text = new Pcdata(document,st,arg);
                addChild(text);
            }
            addChild(new EndTag());
            return null;
        }
        if(TypedXmlWriter.class.isAssignableFrom(rt)) {
            // sub writer
            return _element(nsUri,localName,(Class)rt);
        }

        throw new IllegalSignatureException("Illegal return type: "+rt);
    }

    /**
     * Decides the namespace URI of the given package.
     */
    private String getNamespace(Package pkg) {
        if(pkg==null)       return "";

        String nsUri;
        XmlNamespace ns = pkg.getAnnotation(XmlNamespace.class);
        if(ns!=null)
            nsUri = ns.value();
        else
            nsUri = "";
        return nsUri;
    }

    /**
     * Appends this child object to the tail.
     */
    private void addChild(Content child) {
        tail.setNext(document,child);
        tail = child;
    }

    public void commit() {
        commit(true);
    }

    public void commit(boolean includingAllPredecessors) {
        _commit(includingAllPredecessors);
        document.flush();
    }

    private void _commit(boolean includingAllPredecessors) {
        if(isCommitted())  return;

        addChild(endTag);
        if(isRoot())
            addChild(new EndDocument());
        tail = null;

        // _commit predecessors if so told
        if(includingAllPredecessors) {
            for( ContainerElement e=this; e!=null; e=e.parent ) {
                while(e.prevOpen!=null) {
                    e.prevOpen._commit(false);
                    // e.prevOpen should change as a result of committing it.
                }
            }
        }

        // _commit all children recursively
        while(lastOpenChild!=null)
            lastOpenChild._commit(false);

        // remove this node from the link
        if(parent!=null) {
            if(parent.lastOpenChild==this) {
                assert nextOpen==null : "this must be the last one";
                parent.lastOpenChild = prevOpen;
            } else {
                assert nextOpen.prevOpen==this;
                nextOpen.prevOpen = this.prevOpen;
            }
            if(prevOpen!=null) {
                assert prevOpen.nextOpen==this;
                prevOpen.nextOpen = this.nextOpen;
            }
        }

        this.nextOpen = null;
        this.prevOpen = null;
    }

    public void _attribute(String localName, Object value) {
        _attribute("",localName,value);
    }

    public void _attribute(String nsUri, String localName, Object value) {
        checkStartTag();
        startTag.addAttribute(nsUri,localName,value);
    }

    public void _attribute(QName attributeName, Object value) {
        _attribute(attributeName.getNamespaceURI(),attributeName.getLocalPart(),value);
    }

    public void _namespace(String uri) {
        _namespace(uri,false);
    }

    public void _namespace(String uri, String prefix) {
        if(prefix==null)
            throw new IllegalArgumentException();
        checkStartTag();
        startTag.addNamespaceDecl(uri,prefix,false);
    }

    public void _namespace(String uri, boolean requirePrefix) {
        checkStartTag();
        startTag.addNamespaceDecl(uri,null,requirePrefix);
    }

    public void _pcdata(Object value) {
        // we need to allow this method even when startTag has already been completed.
        // checkStartTag();
        addChild(new Pcdata(document,startTag,value));
    }

    public void _cdata(Object value) {
        addChild(new Cdata(document,startTag,value));
    }

    public void _comment(Object value) throws UnsupportedOperationException {
        addChild(new Comment(document,startTag,value));
    }

    public <T extends TypedXmlWriter> T _element(String localName, Class<T> contentModel) {
        return _element(nsUri,localName,contentModel);
    }

    public <T extends TypedXmlWriter> T _element(QName tagName, Class<T> contentModel) {
        return _element(tagName.getNamespaceURI(),tagName.getLocalPart(),contentModel);
    }

    public <T extends TypedXmlWriter> T _element(Class<T> contentModel) {
        return _element(TXW.getTagName(contentModel),contentModel);
    }

    public <T extends TypedXmlWriter> T _cast(Class<T> facadeType) {
        return facadeType.cast(Proxy.newProxyInstance(facadeType.getClassLoader(),new Class[]{facadeType},this));
    }

    public <T extends TypedXmlWriter> T _element(String nsUri, String localName, Class<T> contentModel) {
        ContainerElement child = new ContainerElement(document,this,nsUri,localName);
        addChild(child.startTag);
        tail = child.endTag;

        // update uncommitted link list
        if(lastOpenChild!=null) {
            assert lastOpenChild.parent==this;

            assert child.prevOpen==null;
            assert child.nextOpen==null;
            child.prevOpen = lastOpenChild;
            assert lastOpenChild.nextOpen==null;
            lastOpenChild.nextOpen = child;
        }

        this.lastOpenChild = child;

        return child._cast(contentModel);
    }
}
