/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.tools.xjc.reader.xmlschema;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;

import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.fmt.JTextFile;
import com.sun.istack.NotNull;
import com.sun.istack.Nullable;
import com.sun.tools.xjc.ErrorReceiver;
import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.Plugin;
import com.sun.tools.xjc.generator.bean.field.FieldRendererFactory;
import com.sun.tools.xjc.model.CClassInfoParent;
import com.sun.tools.xjc.model.Model;
import com.sun.tools.xjc.reader.ModelChecker;
import com.sun.tools.xjc.reader.Ring;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.BIDeclaration;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.BIDom;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.BIGlobalBinding;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.BISchemaBinding;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.BISerializable;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.BindInfo;
import com.sun.tools.xjc.util.CodeModelClassFactory;
import com.sun.tools.xjc.util.ErrorReceiverFilter;
import com.sun.xml.bind.api.impl.NameConverter;
import com.sun.xml.bind.v2.util.XmlFactory;
import com.sun.xml.xsom.XSAnnotation;
import com.sun.xml.xsom.XSAttributeUse;
import com.sun.xml.xsom.XSComponent;
import com.sun.xml.xsom.XSDeclaration;
import com.sun.xml.xsom.XSParticle;
import com.sun.xml.xsom.XSSchema;
import com.sun.xml.xsom.XSSchemaSet;
import com.sun.xml.xsom.XSSimpleType;
import com.sun.xml.xsom.XSTerm;
import com.sun.xml.xsom.XSType;
import com.sun.xml.xsom.XSWildcard;
import com.sun.xml.xsom.util.XSFinder;

import org.xml.sax.Locator;

/**
 * Root of the XML Schema binder.
 * 
 * <div><img src="doc-files/binding_chart.png" alt=""></div>
 * 
 * @author Kohsuke Kawaguchi
 */
public class BGMBuilder extends BindingComponent {

    /**
     * Entry point.
     */
    public static Model build( XSSchemaSet _schemas, JCodeModel codeModel,
            ErrorReceiver _errorReceiver, Options opts ) {
        // set up a ring
        final Ring old = Ring.begin();
        try {
            ErrorReceiverFilter ef = new ErrorReceiverFilter(_errorReceiver);

            Ring.add(XSSchemaSet.class,_schemas);
            Ring.add(codeModel);
            Model model = new Model(opts, codeModel, null/*set later*/, opts.classNameAllocator, _schemas);
            Ring.add(model);
            Ring.add(ErrorReceiver.class,ef);
            Ring.add(CodeModelClassFactory.class,new CodeModelClassFactory(ef));

            BGMBuilder builder = new BGMBuilder(opts.defaultPackage,opts.defaultPackage2,
                opts.isExtensionMode(),opts.getFieldRendererFactory(), opts.activePlugins);
            builder._build();

            if(ef.hadError())   return null;
            else                return model;
        } finally {
            Ring.end(old);
        }
    }


    /**
     * True if the compiler is running in the extension mode
     * (as opposed to the strict conformance mode.)
     */
    public final boolean inExtensionMode;

    /**
     * If this is non-null, this package name takes over
     * all the schema customizations.
     */
    public final String defaultPackage1;

    /**
     * If this is non-null, this package name will be
     * used when no customization is specified.
     */
    public final String defaultPackage2;

    private final BindGreen green = Ring.get(BindGreen.class);
    private final BindPurple purple = Ring.get(BindPurple.class);

    public final Model model = Ring.get(Model.class);

    public final FieldRendererFactory fieldRendererFactory;

    /**
     * Lazily computed {@link RefererFinder}.
     *
     * @see #getReferer
     */
    private RefererFinder refFinder;

    private List<Plugin> activePlugins;

    protected BGMBuilder(String defaultPackage1, String defaultPackage2,
            boolean _inExtensionMode, FieldRendererFactory fieldRendererFactory,
            List<Plugin> activePlugins) {
        this.inExtensionMode = _inExtensionMode;
        this.defaultPackage1 = defaultPackage1;
        this.defaultPackage2 = defaultPackage2;
        this.fieldRendererFactory = fieldRendererFactory;
        this.activePlugins = activePlugins;
        promoteGlobalBindings();
    }

    private void _build() {
        // do the binding
        buildContents();
        getClassSelector().executeTasks();

        // additional error check
        // Reports unused customizations to the user as errors.
        Ring.get(UnusedCustomizationChecker.class).run();

        Ring.get(ModelChecker.class).check();

        for( Plugin ma : activePlugins )
            ma.postProcessModel(model, Ring.get(ErrorReceiver.class));

    }


    /** List up all the global bindings. */
    private void promoteGlobalBindings() {
        // promote any global bindings in the schema
        XSSchemaSet schemas = Ring.get(XSSchemaSet.class);

        for( XSSchema s : schemas.getSchemas() ) {
            BindInfo bi = getBindInfo(s);

            // collect all global customizations
            model.getCustomizations().addAll(bi.toCustomizationList());

            BIGlobalBinding gb = bi.get(BIGlobalBinding.class);
            if(gb==null)
                continue;

            gb.markAsAcknowledged();

            if(globalBinding==null) {
                globalBinding = gb;
            } else {
                if (!globalBinding.isEqual(gb)) { // see Issue 687 - this may happen with syntactically imported documents
                    // acknowledge this customization and report an error
                    // otherwise the user will see "customization is attached to a wrong place" error,
                    // which is incorrect
                    getErrorReporter().error( gb.getLocation(),
                        Messages.ERR_MULTIPLE_GLOBAL_BINDINGS);
                    getErrorReporter().error( globalBinding.getLocation(),
                        Messages.ERR_MULTIPLE_GLOBAL_BINDINGS_OTHER);
                }
            }
        }

        if( globalBinding==null ) {
            // no global customization is present.
            // use the default one
            globalBinding = new BIGlobalBinding();
            BindInfo big = new BindInfo();
            big.addDecl(globalBinding);
            big.setOwner(this,null);
        }

        // code generation mode
        model.strategy = globalBinding.getCodeGenerationStrategy();
        model.rootClass = globalBinding.getSuperClass();
        model.rootInterface = globalBinding.getSuperInterface();

        particleBinder = globalBinding.isSimpleMode() ? new ExpressionParticleBinder() : new DefaultParticleBinder();

        // check XJC extensions and realize them
        BISerializable serial = globalBinding.getSerializable();
        if(serial!=null) {
            model.serializable = true;
            model.serialVersionUID = serial.uid;
        }

        // obtain the name conversion mode
        if (globalBinding.nameConverter!=null)
            model.setNameConverter(globalBinding.nameConverter);

        // attach global conversions to the appropriate simple types
        globalBinding.dispatchGlobalConversions(schemas);

        globalBinding.errorCheck();
    }

    /**
     * Global bindings.
     *
     * The empty global binding is set as the default, so that
     * there will be no need to test if the value is null.
     */
    private BIGlobalBinding globalBinding;

    /**
     * Gets the global bindings.
     */
    public @NotNull BIGlobalBinding getGlobalBinding() { return globalBinding; }

    
    private ParticleBinder particleBinder;

    /**
     * Gets the particle binder for this binding.
     */
    public @NotNull ParticleBinder getParticleBinder() { return particleBinder; }


    /**
     * Name converter that implements "{@code XML -> Java} name conversion"
     * as specified in the spec.
     *
     * This object abstracts the detail that we use different name
     * conversion depending on the customization.
     *
     * <p>
     * This object should be used to perform any name conversion
     * needs, instead of the JJavaName class in CodeModel.
     */
    public NameConverter getNameConverter() { return model.getNameConverter(); }

    /** Fill-in the contents of each classes. */
    private void buildContents() {
        ClassSelector cs = getClassSelector();
        SimpleTypeBuilder stb = Ring.get(SimpleTypeBuilder.class);

        for( XSSchema s : Ring.get(XSSchemaSet.class).getSchemas() ) {
            BISchemaBinding sb = getBindInfo(s).get(BISchemaBinding.class);

            if(sb!=null && !sb.map) {
                sb.markAsAcknowledged();
                continue;       // no mapping for this package
            }

            getClassSelector().pushClassScope( new CClassInfoParent.Package(
                getClassSelector().getPackage(s.getTargetNamespace())) );

            checkMultipleSchemaBindings(s);
            processPackageJavadoc(s);
            populate(s.getAttGroupDecls(),s);
            populate(s.getAttributeDecls(),s);
            populate(s.getElementDecls(),s);
            populate(s.getModelGroupDecls(),s);

            // fill in typeUses
            for (XSType t : s.getTypes().values()) {
                stb.refererStack.push(t);
                model.typeUses().put( getName(t), cs.bindToType(t,s) );
                stb.refererStack.pop();
            }

            getClassSelector().popClassScope();
        }
    }

    /** Reports an error if there are more than one jaxb:schemaBindings customization. */
    private void checkMultipleSchemaBindings( XSSchema schema ) {
        ArrayList<Locator> locations = new ArrayList<Locator>();

        BindInfo bi = getBindInfo(schema);
        for( BIDeclaration bid : bi ) {
            if( bid.getName()==BISchemaBinding.NAME )
                locations.add( bid.getLocation() );
        }
        if(locations.size()<=1)    return; // OK

        // error
        getErrorReporter().error( locations.get(0),
            Messages.ERR_MULTIPLE_SCHEMA_BINDINGS,
            schema.getTargetNamespace() );
        for( int i=1; i<locations.size(); i++ )
            getErrorReporter().error( (Locator)locations.get(i),
                Messages.ERR_MULTIPLE_SCHEMA_BINDINGS_LOCATION);
    }

    /**
     * Calls {@link ClassSelector} for each item in the iterator
     * to populate class items if there is any.
     */
    private void populate( Map<String,? extends XSComponent> col, XSSchema schema ) {
        ClassSelector cs = getClassSelector();
        for( XSComponent sc : col.values() )
            cs.bindToType(sc,schema);
    }

    /**
     * Generates <code>package.html</code> if the customization
     * says so.
     */
    private void processPackageJavadoc( XSSchema s ) {
        // look for the schema-wide customization
        BISchemaBinding cust = getBindInfo(s).get(BISchemaBinding.class);
        if(cust==null)      return; // not present

        cust.markAsAcknowledged();
        if( cust.getJavadoc()==null )   return;     // no javadoc customization

        // produce a HTML file
        JTextFile html = new JTextFile("package.html");
        html.setContents(cust.getJavadoc());
        getClassSelector().getPackage(s.getTargetNamespace()).addResourceFile(html);
    }






    /**
     * Gets or creates the BindInfo object associated to a schema component.
     *
     * @return
     *      Always return a non-null valid BindInfo object.
     *      Even if no declaration was specified, this method creates
     *      a new BindInfo so that new decls can be added.
     */
    public BindInfo getOrCreateBindInfo( XSComponent schemaComponent ) {

        BindInfo bi = _getBindInfoReadOnly(schemaComponent);
        if(bi!=null)    return bi;

        // XSOM is read-only, so we cannot add new annotations.
        // for components that didn't have annotations,
        // we maintain an external map.
        bi = new BindInfo();
        bi.setOwner(this,schemaComponent);
        externalBindInfos.put(schemaComponent,bi);
        return bi;
    }


    /**
     * Used as a constant instance to represent the empty {@link BindInfo}.
     */
    private final BindInfo emptyBindInfo = new BindInfo();

    /**
     * Gets the BindInfo object associated to a schema component.
     *
     * @return
     *      always return a valid {@link BindInfo} object. If none
     *      is specified for the given component, a dummy empty BindInfo
     *      will be returned.
     */
    public BindInfo getBindInfo( XSComponent schemaComponent ) {
        BindInfo bi = _getBindInfoReadOnly(schemaComponent);
        if(bi!=null)    return bi;
        else            return emptyBindInfo;
    }

    /**
     * Gets the BindInfo object associated to a schema component.
     *
     * @return
     *      null if no bind info is associated to this schema component.
     */
    private BindInfo _getBindInfoReadOnly( XSComponent schemaComponent ) {

        BindInfo bi = externalBindInfos.get(schemaComponent);
        if(bi!=null)    return bi;

        XSAnnotation annon = schemaComponent.getAnnotation();
        if(annon!=null) {
            bi = (BindInfo)annon.getAnnotation();
            if(bi!=null) {
                if(bi.getOwner()==null)
                    bi.setOwner(this,schemaComponent);
                return bi;
            }
        }

        return null;
    }

    /**
     * A map that stores binding declarations augmented by XJC.
     */
    private final Map<XSComponent,BindInfo> externalBindInfos = new HashMap<XSComponent,BindInfo>();

    /**
     * Gets the {@link BIDom} object that applies to the given particle.
     */
    protected final BIDom getLocalDomCustomization( XSParticle p ) {
        if (p == null) {
            return null;
        }
        BIDom dom = getBindInfo(p).get(BIDom.class);
        if(dom!=null)  return dom;

        // if not, the term might have one.
        dom = getBindInfo(p.getTerm()).get(BIDom.class);
        if(dom!=null)  return dom;

        XSTerm t = p.getTerm();
        // type could also have one, in case of the dom customization
        if(t.isElementDecl())
            return getBindInfo(t.asElementDecl().getType()).get(BIDom.class);
        // similarly the model group in a model group definition may have one.
        if(t.isModelGroupDecl())
            return getBindInfo(t.asModelGroupDecl().getModelGroup()).get(BIDom.class);

        return null;
    }

    /**
     * Returns true if the component should be processed by purple.
     */
    private final XSFinder toPurple = new XSFinder() {
        @Override
        public Boolean attributeUse(XSAttributeUse use) {
            // attribute use always maps to a property
            return true;
        }

        @Override
        public Boolean simpleType(XSSimpleType xsSimpleType) {
            // simple type always maps to a type, hence we should take purple
            return true;
        }

        @Override
        public Boolean wildcard(XSWildcard xsWildcard) {
            // attribute wildcards always maps to a property.
            // element wildcards should have been processed with particle binders
            return true;
        }
    };
    /**
     * If the component maps to a property, forwards to purple, otherwise to green.
     *
     * If the component is mapped to a type, this method needs to return true.
     * See the chart at the class javadoc.
     */
    public void ying( XSComponent sc, @Nullable XSComponent referer ) {
        if(sc.apply(toPurple)==true || getClassSelector().bindToType(sc,referer)!=null)
            sc.visit(purple);
        else
            sc.visit(green);
    }

    private Transformer identityTransformer;

    /**
     * Gets the shared instance of the identity transformer.
     */
    public Transformer getIdentityTransformer() {
        try {
            if(identityTransformer==null) {
                TransformerFactory tf = XmlFactory.createTransformerFactory(model.options.disableXmlSecurity);
                identityTransformer = tf.newTransformer();
            }
            return identityTransformer;
        } catch (TransformerConfigurationException e) {
            throw new Error(e); // impossible
        }
    }

    /**
     * Find all types that refer to the given complex type.
     */
    public Set<XSComponent> getReferer(XSType c) {
        if(refFinder==null) {
            refFinder = new RefererFinder();
            refFinder.schemaSet(Ring.get(XSSchemaSet.class));
        }
        return refFinder.getReferer(c);
    }

    /**
     * Returns the QName of the declaration.
     * @return null
     *      if the declaration is anonymous.
     */
    public static QName getName(XSDeclaration decl) {
        String local = decl.getName();
        if(local==null) return null;
        return new QName(decl.getTargetNamespace(),local);
    }

    /**
     * Derives a name from a schema component.
     *
     * This method handles prefix/suffix modification and
     * XML-to-Java name conversion.
     *
     * @param name
     *      The base name. This should be things like element names
     *      or type names.
     * @param comp
     *      The component from which the base name was taken.
     *      Used to determine how names are modified.
     */
    public String deriveName( String name, XSComponent comp ) {
        XSSchema owner = comp.getOwnerSchema();

        name = getNameConverter().toClassName(name);

        if( owner!=null ) {
            BISchemaBinding sb = getBindInfo(owner).get(BISchemaBinding.class);

            if(sb!=null)    name = sb.mangleClassName(name,comp);
        }

        return name;
    }

    public boolean isGenerateMixedExtensions() {
        if (globalBinding != null) {
            return globalBinding.isGenerateMixedExtensions();
        }
        return false;
    }

}
