/*
 * Copyright (c) 1998, 2024 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.jaxb.compiler;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.XMLConstants;
import javax.xml.bind.SchemaOutputResolver;
import javax.xml.namespace.QName;

import org.eclipse.persistence.core.sessions.CoreProject;
import org.eclipse.persistence.internal.oxm.Constants;
import org.eclipse.persistence.internal.oxm.mappings.Descriptor;
import org.eclipse.persistence.internal.oxm.schema.SchemaModelProject;
import org.eclipse.persistence.internal.oxm.schema.model.Schema;
import org.eclipse.persistence.internal.oxm.schema.model.SchemaCompareByNamespace;
import org.eclipse.persistence.jaxb.MOXySystemProperties;
import org.eclipse.persistence.jaxb.TypeMappingInfo;
import org.eclipse.persistence.jaxb.javamodel.Helper;
import org.eclipse.persistence.jaxb.javamodel.JavaClass;
import org.eclipse.persistence.jaxb.javamodel.JavaModelInput;
import org.eclipse.persistence.jaxb.javamodel.reflection.JavaModelInputImpl;
import org.eclipse.persistence.jaxb.xmlmodel.XmlBindings;
import org.eclipse.persistence.logging.AbstractSessionLog;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.oxm.NamespaceResolver;
import org.eclipse.persistence.oxm.XMLContext;
import org.eclipse.persistence.oxm.XMLMarshaller;
import org.eclipse.persistence.sessions.Project;

/**
 * INTERNAL:
 *  <p><b>Purpose:</b>The purpose of this class is to act as an entry point into the
 *  TopLink JAXB 2.0 Generation framework
 *  <p><b>Responsibilities:</b><ul>
 *  <li>Run initial processing on a list of classes to create TypeInfo meta data</li>
 *  <li>Provide API to generate Schema Files</li>
 *  <li>Provide API to generate a TopLink Project</li>
 *  <li>Act as an integration point with WebServices</li>
 *  </ul>
 *  <p> This class acts as an entry point into JAXB 2.0 Generation. A Generator is created with a
 *  specific set of JAXB 2.0 Annotated classes and then performs actions on those, such as
 *  generating schema files, or generating TopLink Projects. Additional information is returned
 *  from the schema generation methods as a means of integration with WebServices.
 *
 *  @author  mmacivor
 *  @since   Oracle TopLink 11.1.1.0.0
 *  @see AnnotationsProcessor
 *  @see MappingsGenerator
 *  @see SchemaGenerator
 */
public class Generator {

    private AnnotationsProcessor annotationsProcessor;
    private SchemaGenerator schemaGenerator;
    private MappingsGenerator mappingsGenerator;
    private Helper helper;
    private Map<Type, TypeMappingInfo> typeToTypeMappingInfo;

    /**
     * This constructor creates a Helper using the JavaModelInput
     * instance's JavaModel. Annotations are processed here as well.
     *
     * @param jModelInput
     */
    public Generator(JavaModelInput jModelInput) {
        helper = new Helper(jModelInput.getJavaModel());
        configureFacetsGeneration(jModelInput);
        annotationsProcessor = new AnnotationsProcessor(helper);
        schemaGenerator = new SchemaGenerator(helper);
        mappingsGenerator = new MappingsGenerator(helper);
        annotationsProcessor.processClassesAndProperties(jModelInput.getJavaClasses(), null);
    }

    /**
     * This constructor will process and apply the given XmlBindings as appropriate.  Classes
     * declared in the bindings will be amalgamated with any classes in the JavaModelInput.
     *
     * If xmlBindings is null or empty, AnnotationsProcessor will be used to process
     * annotations as per usual.
     *
     * @param jModelInput
     * @param xmlBindings map of XmlBindings keyed on package name
     * @param cLoader
     */
    public Generator(JavaModelInput jModelInput, Map<String, XmlBindings> xmlBindings, ClassLoader cLoader, String defaultTargetNamespace, boolean enableXmlAccessorFactory) {
        helper = new Helper(jModelInput.getJavaModel());
        configureFacetsGeneration(jModelInput);
        annotationsProcessor = new AnnotationsProcessor(helper);
        annotationsProcessor.setXmlAccessorFactorySupport(enableXmlAccessorFactory);
        annotationsProcessor.setDefaultTargetNamespace(defaultTargetNamespace);
        schemaGenerator = new SchemaGenerator(helper);
        mappingsGenerator = new MappingsGenerator(helper);
        if (xmlBindings != null && !xmlBindings.isEmpty()) {
            new XMLProcessor(xmlBindings).processXML(annotationsProcessor, jModelInput, null, null);
        } else {
            annotationsProcessor.processClassesAndProperties(jModelInput.getJavaClasses(), null);
        }
    }

    /**
     * This constructor creates a Helper using the JavaModelInput
     * instance's JavaModel and a map of javaclasses that were generated from Type objects.
     * Annotations are processed here as well.
     *
     * @param jModelInput
     */
    public Generator(JavaModelInput jModelInput, TypeMappingInfo[] typeMappingInfos, JavaClass[] javaClasses, Map<Type, TypeMappingInfo> typeToTypeMappingInfo, String defaultTargetNamespace) {
        helper = new Helper(jModelInput.getJavaModel());
        configureFacetsGeneration(jModelInput);
        annotationsProcessor = new AnnotationsProcessor(helper);
        annotationsProcessor.setDefaultTargetNamespace(defaultTargetNamespace);
        schemaGenerator = new SchemaGenerator(helper);
        mappingsGenerator = new MappingsGenerator(helper);
        this.typeToTypeMappingInfo = typeToTypeMappingInfo;
        annotationsProcessor.processClassesAndProperties(javaClasses, typeMappingInfos);
    }

    /**
     * This constructor will process and apply the given XmlBindings as appropriate.  Classes
     * declared in the bindings will be amalgamated with any classes in the JavaModelInput.
     *
     * If xmlBindings is null or empty, AnnotationsProcessor will be used to process
     * annotations as per usual.
     *
     * @param jModelInput
     * @param defaultTargetNamespace
     * @param enableXmlAccessorFactory
     * @param javaClasses
     * @param typeMappingInfos
     * @param typeToTypeMappingInfo
     * @param xmlBindings map of XmlBindings keyed on package name
     * @param cLoader
     */
    public Generator(JavaModelInput jModelInput, TypeMappingInfo[] typeMappingInfos, JavaClass[] javaClasses, Map<Type, TypeMappingInfo> typeToTypeMappingInfo, Map<String, XmlBindings> xmlBindings, ClassLoader cLoader, String defaultTargetNamespace, boolean enableXmlAccessorFactory) {
        helper = new Helper(jModelInput.getJavaModel());
        configureFacetsGeneration(jModelInput);
        annotationsProcessor = new AnnotationsProcessor(helper);
        annotationsProcessor.setXmlAccessorFactorySupport(enableXmlAccessorFactory);
        annotationsProcessor.setDefaultTargetNamespace(defaultTargetNamespace);
        schemaGenerator = new SchemaGenerator(helper);
        mappingsGenerator = new MappingsGenerator(helper);
        this.typeToTypeMappingInfo = typeToTypeMappingInfo;
        if (xmlBindings != null && !xmlBindings.isEmpty()) {
            new XMLProcessor(xmlBindings).processXML(annotationsProcessor, jModelInput, typeMappingInfos, javaClasses);
        } else {
        annotationsProcessor.processClassesAndProperties(javaClasses, typeMappingInfos);
        }
    }

    /**
     * This event is called when mappings generation is completed,
     * and provides a chance to deference anything that is no longer
     * needed (to reduce the memory footprint of this object).
     */
    public void postInitialize() {
        mappingsGenerator = null;
        annotationsProcessor.postInitialize();
        schemaGenerator = null;
    }

    /**
     *
     */
    public boolean hasMarshalCallbacks() {
        return getMarshalCallbacks()!=null && !getMarshalCallbacks().isEmpty();
    }

    public boolean hasUnmarshalCallbacks() {
        return getUnmarshalCallbacks()!=null && !getUnmarshalCallbacks().isEmpty();
    }

    public CoreProject generateProject() throws Exception {
        mappingsGenerator.getClassToGeneratedClasses().putAll(annotationsProcessor.getArrayClassesToGeneratedClasses());
        CoreProject p = mappingsGenerator.generateProject(annotationsProcessor.getTypeInfoClasses(), annotationsProcessor.getTypeInfos(), annotationsProcessor.getUserDefinedSchemaTypes(), annotationsProcessor.getPackageToPackageInfoMappings(), annotationsProcessor.getGlobalElements(), annotationsProcessor.getLocalElements(), annotationsProcessor.getTypeMappingInfosToGeneratedClasses(), annotationsProcessor.getTypeMappingInfoToAdapterClasses(),annotationsProcessor.isDefaultNamespaceAllowed());
        annotationsProcessor.getArrayClassesToGeneratedClasses().putAll(mappingsGenerator.getClassToGeneratedClasses());
        return p;
    }

    public java.util.Collection<Schema> generateSchema() {
        schemaGenerator.generateSchema(annotationsProcessor.getTypeInfoClasses(), annotationsProcessor.getTypeInfos(), annotationsProcessor.getUserDefinedSchemaTypes(), annotationsProcessor.getPackageToPackageInfoMappings(), null, annotationsProcessor.getArrayClassesToGeneratedClasses());
        return schemaGenerator.getAllSchemas();
    }

    public Map<String, SchemaTypeInfo> generateSchemaFiles(String schemaPath, Map<QName, Type> additionalGlobalElements) throws FileNotFoundException {
        // process any additional global elements
        processAdditionalElements(additionalGlobalElements, annotationsProcessor);

        schemaGenerator.generateSchema(annotationsProcessor.getTypeInfoClasses(), annotationsProcessor.getTypeInfos(), annotationsProcessor.getUserDefinedSchemaTypes(), annotationsProcessor.getPackageToPackageInfoMappings(), annotationsProcessor.getGlobalElements(), annotationsProcessor.getArrayClassesToGeneratedClasses());
        CoreProject proj = new SchemaModelProject();
        XMLContext context = new XMLContext((Project)proj);
        XMLMarshaller marshaller = context.createMarshaller();
        Descriptor schemaDescriptor = (Descriptor)proj.getDescriptor(Schema.class);

        java.util.Collection<Schema> schemas = schemaGenerator.getAllSchemas();
        for(Schema schema : schemas) {
            File file = new File(schemaPath + "/" + schema.getName());
            NamespaceResolver schemaNamespaces = schema.getNamespaceResolver();
            schemaNamespaces.put(Constants.SCHEMA_PREFIX, XMLConstants.W3C_XML_SCHEMA_NS_URI);
            schemaDescriptor.setNamespaceResolver(schemaNamespaces);
            marshaller.marshal(schema, new FileOutputStream(file));
        }
        return schemaGenerator.getSchemaTypeInfo();
    }

    public Map<String, SchemaTypeInfo> generateSchemaFiles(SchemaOutputResolver outputResolver, Map<QName, Type> additionalGlobalElements) {
        // process any additional global elements
        processAdditionalElements(additionalGlobalElements, annotationsProcessor);

        schemaGenerator.generateSchema(annotationsProcessor.getTypeInfoClasses(), annotationsProcessor.getTypeInfos(), annotationsProcessor.getUserDefinedSchemaTypes(), annotationsProcessor.getPackageToPackageInfoMappings(), annotationsProcessor.getGlobalElements(), annotationsProcessor.getArrayClassesToGeneratedClasses(), outputResolver);
        CoreProject proj = new SchemaModelProject();
        XMLContext context = new XMLContext((Project)proj);
        XMLMarshaller marshaller = context.createMarshaller();

        Descriptor schemaDescriptor = (Descriptor)proj.getDescriptor(Schema.class);

        java.util.Collection<Schema> schemas = schemaGenerator.getAllSchemas();
        // make sure that schemas will be passed to the output in specified order
        if (schemas instanceof List && MOXySystemProperties.moxySchemaGeneratorSort) {
            ((List)schemas).sort(new SchemaCompareByNamespace());
        }
        for(Schema schema : schemas) {
            try {
                NamespaceResolver schemaNamespaces = schema.getNamespaceResolver();
                schemaNamespaces.put(Constants.SCHEMA_PREFIX, XMLConstants.W3C_XML_SCHEMA_NS_URI);
                schemaDescriptor.setNamespaceResolver(schemaNamespaces);
                // make sure we don't call into the provided output resolver more than once
                javax.xml.transform.Result target;
                if (schema.hasResult()) {
                    target = schema.getResult();
                } else {
                    target = outputResolver.createOutput(schema.getTargetNamespace(), schema.getName());
                }
                marshaller.marshal(schema, target);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return schemaGenerator.getSchemaTypeInfo();
    }

    /**
     * Convenience method that processes a given map of QName-Type entries.  For each an ElementDeclaration
     * is created and added to the given AnnotationsProcessor instance's map of global elements.
     *
     * It is assumed that the map of QName-Type entries contains Type instances that are either a Class or
     * a ParameterizedType.
     *
     * @param additionalGlobalElements
     * @param annotationsProcessor
     */
    private void processAdditionalElements(Map<QName, Type> additionalGlobalElements, AnnotationsProcessor annotationsProcessor) {
        if (additionalGlobalElements != null) {
            ElementDeclaration declaration;
            for(Entry<QName, Type> entry : additionalGlobalElements.entrySet()) {
                QName key = entry.getKey();
                Type type = entry.getValue();
                TypeMappingInfo tmi = null;
                if(this.typeToTypeMappingInfo != null) {
                    tmi = this.typeToTypeMappingInfo.get(type);
                }

                if(tmi != null) {
                    if(annotationsProcessor.getTypeMappingInfosToGeneratedClasses().get(tmi) != null) {
                        type =  annotationsProcessor.getTypeMappingInfosToGeneratedClasses().get(tmi);
                    }
                }
                JavaClass jClass = null;
                if (type instanceof Class) {
                    Class tClass = (Class) type;
                    jClass = helper.getJavaClass(tClass);
                }
                // if no type is available don't do anything
                if (jClass != null) {
                    declaration = new ElementDeclaration(key, jClass, jClass.getQualifiedName(), false);
                    annotationsProcessor.getGlobalElements().put(key, declaration);
                }
            }
        }
    }

    public Map getUnmarshalCallbacks() {
        return annotationsProcessor.getUnmarshalCallbacks();
    }

    public Map getMarshalCallbacks() {
        return annotationsProcessor.getMarshalCallbacks();
    }

    public MappingsGenerator getMappingsGenerator() {
        return this.mappingsGenerator;
    }

    public AnnotationsProcessor getAnnotationsProcessor() {
        return annotationsProcessor;
    }

    public void setTypeToTypeMappingInfo(Map<Type, TypeMappingInfo> typesToTypeMapping) {
        this.typeToTypeMappingInfo = typesToTypeMapping;
    }

    public Map<Type, TypeMappingInfo> getTypeToTypeMappingInfo() {
        return this.typeToTypeMappingInfo;
    }

    private void configureFacetsGeneration(JavaModelInput jModelInput) {
        if (jModelInput instanceof JavaModelInputImpl) {
            helper.setFacets(((JavaModelInputImpl) jModelInput).isFacets());
        } else {
            String msg = "MOXy BV: Facets generation could not be configured. EclipseLink''s JavaModelInputImpl was " +
                    "not detected, instead JavaModelInput is of class: {0}";
            AbstractSessionLog.getLog().log(SessionLog.WARNING, SessionLog.MOXY, msg, new Object[] {jModelInput.getClass()}, false);
        }
    }

}
