/*
 * Copyright (c) 1997, 2011, 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.tools.internal.xjc.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlID;
import javax.xml.bind.annotation.XmlIDREF;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.namespace.QName;

import com.sun.codemodel.internal.JClass;
import com.sun.codemodel.internal.JCodeModel;
import com.sun.codemodel.internal.JPackage;
import com.sun.istack.internal.Nullable;
import com.sun.tools.internal.xjc.Language;
import com.sun.tools.internal.xjc.model.nav.NClass;
import com.sun.tools.internal.xjc.model.nav.NType;
import com.sun.tools.internal.xjc.outline.Aspect;
import com.sun.tools.internal.xjc.outline.Outline;
import com.sun.tools.internal.xjc.reader.Ring;
import com.sun.tools.internal.xjc.reader.xmlschema.BGMBuilder;
import com.sun.tools.internal.xjc.reader.xmlschema.bindinfo.BIFactoryMethod;
import com.sun.xml.internal.bind.v2.model.core.ClassInfo;
import com.sun.xml.internal.bind.v2.model.core.Element;
import com.sun.xml.internal.xsom.XSComponent;

import org.xml.sax.Locator;

/**
 * Mutable {@link ClassInfo} represenatation.
 *
 * <p>
 * Schema parsers build these objects.
 *
 * @author Kohsuke Kawaguchi
 */
public final class CClassInfo extends AbstractCElement implements ClassInfo<NType,NClass>, CClassInfoParent, CClass, NClass {

    @XmlIDREF
    private CClass baseClass;

    /**
     * List of all subclasses, together with {@link #nextSibling}.
     *
     * If this class has no sub-class, this field is null. Otherwise,
     * this field points to a sub-class of this class. From there you can enumerate
     * all the sub-classes by using {@link #nextSibling}.
     */
    private CClassInfo firstSubclass;

    /**
     * @see #firstSubclass
     */
    private CClassInfo nextSibling = null;

    /**
     * @see #getTypeName()
     */
    private final QName typeName;

    /**
     * Custom {@link #getSqueezedName() squeezed name}, if any.
     */
    private /*almost final*/ @Nullable String squeezedName;

    /**
     * If this class also gets {@link XmlRootElement}, the class name.
     */
    private final @Nullable QName elementName;

    private boolean isOrdered = true;

    private final List<CPropertyInfo> properties = new ArrayList<CPropertyInfo>();

    /**
     * TODO: revisit this design.
     * we should at least do a basic encapsulation to avoid careless
     * mistakes. Maybe we should even differ the javadoc generation
     * by queueing runners.
     */
    public String javadoc;

    @XmlIDREF
    private final CClassInfoParent parent;

    /**
     * short name.
     */
    public final String shortName;

    /**
     * Optional user-specified implementation override class.
     */
    private @Nullable String implClass;

    /**
     * The {@link Model} object to which this bean belongs.
     */
    public final Model model;

    /**
     * @see #hasAttributeWildcard()
     */
    private boolean hasAttributeWildcard;


    public CClassInfo(Model model,JPackage pkg, String shortName, Locator location, QName typeName, QName elementName, XSComponent source, CCustomizations customizations) {
        this(model,model.getPackage(pkg),shortName,location,typeName,elementName,source,customizations);
    }

    public CClassInfo(Model model,CClassInfoParent p, String shortName, Locator location, QName typeName, QName elementName, XSComponent source, CCustomizations customizations) {
        super(model,source,location,customizations);
        this.model = model;
        this.parent = p;
        this.shortName = model.allocator.assignClassName(parent,shortName);
        this.typeName = typeName;
        this.elementName = elementName;

        Language schemaLanguage = model.options.getSchemaLanguage();
        if ((schemaLanguage != null) &&
            (schemaLanguage.equals(Language.XMLSCHEMA) || schemaLanguage.equals(Language.WSDL))) {
            BIFactoryMethod factoryMethod = Ring.get(BGMBuilder.class).getBindInfo(source).get(BIFactoryMethod.class);
            if(factoryMethod!=null) {
                factoryMethod.markAsAcknowledged();
                this.squeezedName = factoryMethod.name;
            }
        }

        model.add(this);
    }

    public CClassInfo(Model model,JCodeModel cm, String fullName, Locator location, QName typeName, QName elementName, XSComponent source, CCustomizations customizations) {
        super(model,source,location,customizations);
        this.model = model;
        int idx = fullName.indexOf('.');
        if(idx<0) {
            this.parent = model.getPackage(cm.rootPackage());
            this.shortName = model.allocator.assignClassName(parent,fullName);
        } else {
            this.parent = model.getPackage(cm._package(fullName.substring(0,idx)));
            this.shortName = model.allocator.assignClassName(parent,fullName.substring(idx+1));
        }
        this.typeName = typeName;
        this.elementName = elementName;

        model.add(this);
    }

    public boolean hasAttributeWildcard() {
        return hasAttributeWildcard;
    }

    public void hasAttributeWildcard(boolean hasAttributeWildcard) {
        this.hasAttributeWildcard = hasAttributeWildcard;
    }

    public boolean hasSubClasses() {
        return firstSubclass!=null;
    }

    /**
     * Returns true if a new attribute wildcard property needs to be
     * declared on this class.
     */
    public boolean declaresAttributeWildcard() {
        return hasAttributeWildcard && !inheritsAttributeWildcard();
    }

    /**
     * Returns true if this class inherits a wildcard attribute property
     * from its ancestor classes.
     */
    public boolean inheritsAttributeWildcard() {
        if (getRefBaseClass() != null) {
            CClassRef cref = (CClassRef)baseClass;
            if (cref.getSchemaComponent().getForeignAttributes().size() > 0) {
                return true;
            }
        } else {
            for( CClassInfo c=getBaseClass(); c!=null; c=c.getBaseClass() ) {
                if(c.hasAttributeWildcard)
                    return true;
            }
        }
        return false;
    }


    public NClass getClazz() {
        return this;
    }

    public CClassInfo getScope() {
        return null;
    }

    @XmlID
    public String getName() {
        return fullName();
    }

    /**
     * Returns the "squeezed name" of this bean token.
     * <p>
     * The squeezed name of a bean is the concatenation of
     * the names of its outer classes and itself.
     * <p>
     * Thus if the bean is "org.acme.foo.Bean", then the squeezed name is "Bean",
     * if the bean is "org.acme.foo.Outer1.Outer2.Bean", then "Outer1Outer2Bean".
     * <p>
     * This is used by the code generator
     */
    @XmlElement
    public String getSqueezedName() {
        if (squeezedName != null)  return squeezedName;
        return calcSqueezedName.onBean(this);
    }

    private static final CClassInfoParent.Visitor<String> calcSqueezedName = new Visitor<String>() {
        public String onBean(CClassInfo bean) {
            return bean.parent.accept(this)+bean.shortName;
        }

        public String onElement(CElementInfo element) {
            return element.parent.accept(this)+element.shortName();
        }

        public String onPackage(JPackage pkg) {
            return "";
        }
    };

    /**
     * Returns a mutable list.
     */
    public List<CPropertyInfo> getProperties() {
        return properties;
    }

    public boolean hasValueProperty() {
        throw new UnsupportedOperationException();
    }

    /**
     * Gets a propery by name.
     */
    public CPropertyInfo getProperty(String name) {
        // TODO: does this method need to be fast?
        for( CPropertyInfo p : properties )
            if(p.getName(false).equals(name))
                return p;
        return null;
    }

    public boolean hasProperties() {
        return !getProperties().isEmpty();
    }

    public boolean isElement() {
        return elementName!=null;
    }

    /**
     * Guaranteed to return this.
     */
    @Deprecated
    public CNonElement getInfo() {
        return this;
    }

    public Element<NType,NClass> asElement() {
        if(isElement())
            return this;
        else
            return null;
    }

    public boolean isOrdered() {
        return isOrdered;
    }

    /**
     * @deprecated
     *      if you are calling this method directly, you must be doing something wrong.
     */
    public boolean isFinal() {
        return false;
    }

    public void setOrdered(boolean value) {
        isOrdered = value;
    }

    public QName getElementName() {
        return elementName;
    }

    public QName getTypeName() {
        return typeName;
    }

    public boolean isSimpleType() {
        throw new UnsupportedOperationException();
    }

    /**
     * Returns the FQCN of this bean.
     */
    public String fullName() {
        String r = parent.fullName();
        if(r.length()==0)   return shortName;
        else                return r+'.'+shortName;
    }

    public CClassInfoParent parent() {
        return parent;
    }

    public void setUserSpecifiedImplClass(String implClass) {
        assert this.implClass==null;
        assert implClass!=null;
        this.implClass = implClass;
    }

    public String getUserSpecifiedImplClass() {
        return implClass;
    }


    /**
     * Adds a new property.
     */
    public void addProperty(CPropertyInfo prop) {
        if(prop.ref().isEmpty())
            // this property isn't contributing anything
            // this happens when you try to map an empty sequence to a property
            return;
        prop.setParent(this);
        properties.add(prop);
    }

    /**
     * This method accepts both {@link CClassInfo} (which means the base class
     * is also generated), or {@link CClassRef} (which means the base class is
     * already generated and simply referenced.)
     *
     * The latter is treated somewhat special --- from the rest of the model
     * this external base class is invisible. This modeling might need more
     * thoughts to get right.
     */
    public void setBaseClass(CClass base) {
        assert baseClass==null;
        assert base!=null;
        baseClass = base;

        assert nextSibling==null;
        if (base instanceof CClassInfo) {
            CClassInfo realBase = (CClassInfo) base;
            this.nextSibling = realBase.firstSubclass;
            realBase.firstSubclass = this;
        }
    }

    /**
     * This inherited version returns null if this class extends from {@link CClassRef}.
     *
     * @see #getRefBaseClass()
     */
    public CClassInfo getBaseClass() {
        if (baseClass instanceof CClassInfo) {
            return (CClassInfo) baseClass;
        } else {
            return null;
        }
    }

    public CClassRef getRefBaseClass() {
        if (baseClass instanceof CClassRef) {
            return (CClassRef) baseClass;
        } else {
            return null;
        }
    }

    /**
     * Enumerates all the sub-classes of this class.
     */
    public Iterator<CClassInfo> listSubclasses() {
        return new Iterator<CClassInfo>() {
            CClassInfo cur = firstSubclass;
            public boolean hasNext() {
                return cur!=null;
            }

            public CClassInfo next() {
                CClassInfo r = cur;
                cur = cur.nextSibling;
                return r;
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    public CClassInfo getSubstitutionHead() {
        CClassInfo c=getBaseClass();
        while(c!=null && !c.isElement())
            c=c.getBaseClass();
        return c;
    }


    /**
     * Interfaces to be implemented.
     * Lazily constructed.
     */
    private Set<JClass> _implements = null;

    public void _implements(JClass c) {
        if(_implements==null)
            _implements = new HashSet<JClass>();
        _implements.add(c);
    }


    /** Constructor declarations. array of {@link Constructor}s. */
    private final List<Constructor> constructors = new ArrayList<Constructor>(1);

    /** Creates a new constructor declaration and adds it. */
    public void addConstructor( String... fieldNames ) {
        constructors.add(new Constructor(fieldNames));
    }

    /** list all constructor declarations. */
    public Collection<? extends Constructor> getConstructors() {
        return constructors;
    }

    public final <T> T accept(Visitor<T> visitor) {
        return visitor.onBean(this);
    }

    public JPackage getOwnerPackage() {
        return parent.getOwnerPackage();
    }

    public final NClass getType() {
        return this;
    }

    public final JClass toType(Outline o, Aspect aspect) {
        switch(aspect) {
        case IMPLEMENTATION:
            return o.getClazz(this).implRef;
        case EXPOSED:
            return o.getClazz(this).ref;
        default:
            throw new IllegalStateException();
        }
    }

    public boolean isBoxedType() {
        return false;
    }

    public String toString() {
        return fullName();
    }
}
