/*   Copyright 2004 The Apache Software Foundation
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.xmlbeans.impl.tool;

import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.xmlbeans.XmlError;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.apache.xmlbeans.impl.tool.SchemaCompiler.parsePartialMethods;

@SuppressWarnings("unused")
@Mojo(name = "compile", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class MavenPlugin extends AbstractMojo {
    // ******************************************************************************************
    // As we don't use the maven plugin-plugin, the defaultValues and others need to be manually
    // copied into resources/maven/plugin.xml
    // ******************************************************************************************

    /** The maven project */
    @Parameter( readonly = true, defaultValue = "${project}" )
    private MavenProject project;

    /** sourceDir is a base directory for the list in sourceschema */
    @Parameter( defaultValue = "${project.basedir}/src/main/schema" )
    private String sourceDir;

    /** sourceSchemas is a comma-delimited list of all the schemas you want to compile */
    @Parameter( defaultValue = "*.xsd,*.wsdl,*.java" )
    private String sourceSchemas;

    /** xmlConfigs points to your xmlconfig.xml file */
    @Parameter( defaultValue = "${project.basedir}/src/schema/xmlconfig.xml" )
    private String xmlConfigs;

    /** javaTargetdir is where you want generated java source to appear */
    @Parameter( defaultValue = "${project.basedir}/target/generated-sources" )
    private String javaTargetDir;

    /** classTargetDir is where you want compiled class files to appear */
    @Parameter( defaultValue = "${project.basedir}/target/generated-resources" )
    private String classTargetDir;

    /** catalogLocation is the location of an entity resolver catalog to use for resolving namespace to schema locations. */
    @Parameter
    private String catalogLocation;

    @Parameter
    private String classPath;

    @Parameter
    private List<Resource> resources;

    /** buildSchemas sets build process of the generated sources */
    @Parameter(defaultValue = "true")
    private boolean buildSchemas;

    /** destination directory of the copied xsd files - default: schemaorg_apache_xmlbeans/src */
    @Parameter( defaultValue = "schemaorg_apache_xmlbeans/src" )
    private String baseSchemaLocation;

    /** schema system name - default: ${project.artifactId} */
    @Parameter( defaultValue = "${project.artifactId}" )
    private String name;

    /** verbose output  - default: false */
    @Parameter( defaultValue = "false" )
    private boolean verbose;

    /** no output  - default: true */
    @Parameter( defaultValue = "true" )
    private boolean quiet;

    /** no output (deprecated - use quiet instead) - default: true */
    @Parameter( defaultValue = "true" )
    private boolean quite;

    /** deactivate unique particle attribution - default: false */
    @Parameter( defaultValue = "false" )
    private boolean noUpa;

    /** deactivate particle valid (restriction) - default: false */
    @Parameter( defaultValue = "false" )
    private boolean noPvr;

    /** deactivate annotation generation - default: false */
    @Parameter( defaultValue = "false" )
    private boolean noAnn;

    /** do not validate contents of documentation-tags - default: false */
    @Parameter( defaultValue = "false" )
    private boolean noVDoc;

    /** Metadata package name. If explicitly set empty, generates to org.apache.xmlbeans.metadata - default: ${project.groupId}.${project.artifactId}.metadata */
    @Parameter( defaultValue = "${project.groupId}.${project.artifactId}.metadata" )
    private String repackage;

    /**
     * If this option is set, then the schema compiler will permit and
     * ignore multiple definitions of the same component (element, attribute,
     * type, etc) names in the given namespaces.  If multiple definitions
     * with the same name appear, the definitions that happen to be processed
     * last will be ignored.
     *
     * a list of namespace URIs
     */
    @Parameter
    private List<String> mdefNamespaces;

    /**
     * Only generate a subset of the bean methods. Comma-seperated list of the following method types:
     * GET, XGET, IS_SET, IS_NIL, IS_NIL_IDX, SET, SET_NIL, SET_NIL_IDX, XSET, UNSET,
     * GET_ARRAY, XGET_ARRAY, GET_IDX, XGET_IDX, XSET_ARRAY, XSET_IDX,
     * SIZE_OF_ARRAY, SET_ARRAY, SET_IDX,
     * INSERT_IDX, INSERT_NEW_IDX,
     * ADD, ADD_NEW, REMOVE_IDX,
     * GET_LIST, XGET_LIST, SET_LIST,
     * INSTANCE_TYPE
     *
     * Example: "ALL,-GET_LIST,-XGET_LIST" excludes GET_LIST and XGET_LIST methods
     */
    @Parameter
    private String partialMethods;

    @Parameter( defaultValue = "false" )
    private boolean download;

    @Parameter( defaultValue = "true" )
    private boolean sourceOnly;

    @Parameter
    private File basedir;

    @Parameter
    private String compiler;

    @Parameter( defaultValue = CodeGenUtil.DEFAULT_MEM_START )
    private String memoryInitialSize;

    @Parameter( defaultValue = CodeGenUtil.DEFAULT_MEM_MAX )
    private String memoryMaximumSize;

    @Parameter( defaultValue = "${project.basedir}/target/${project.artifactId}-${project.version}-xmltypes.jar" )
    private File outputJar;

    @Parameter( defaultValue = "false" )
    private boolean debug;

    /** copy annotations to javadoc of generated sources - default: false */
    @Parameter( defaultValue = "false" )
    private boolean copyAnn;

    @Parameter
    private List<Extension> extensions;

    public void execute() throws MojoExecutionException, MojoFailureException {
        if (sourceDir == null || sourceDir.isEmpty() || !new File(sourceDir).isDirectory()) {
            throw new MojoFailureException("Set configuration <sourceDir> (='"+sourceDir+"') to a valid directory containing *.xsd,*.wsdl files.");
        }

        if (baseSchemaLocation == null || baseSchemaLocation.isEmpty()) {
            throw new MojoFailureException("baseSchemaLocation is empty");
        }

        if (sourceSchemas == null) {
            getLog().debug("sourceSchemas is null");
        }

        if (classPath == null) {
            getLog().debug("classPath is null");
        }

        final List<File> xsds = new ArrayList<>();
        final List<File> wsdls = new ArrayList<>();
        final List<File> javas = new ArrayList<>();
        File base = new File(sourceDir);
        Resource resource = new Resource();
        resource.setDirectory(sourceDir);
        resource.setTargetPath(baseSchemaLocation);

        // if sourceSchemas is not specified use all found schemas
        // otherwise convert comma-separated string to regex including glob parameter
        Pattern pat = Pattern.compile(sourceSchemas == null ? ".*" :
            "(" + sourceSchemas
                .replace(",","|")
                .replace(".", "\\.")
                .replace("*",".*") +
            ")");

        File[] schemaFiles = Objects.requireNonNull(base.listFiles((dir, name) ->
            !name.endsWith(".xsdconfig") && pat.matcher(name).matches()));
        for (File sf : schemaFiles) {
            String name = sf.getName();
            switch (name.replaceAll(".*\\.", "")) {
                case "wsdl":
                    wsdls.add(sf);
                    break;
                case "java":
                    javas.add(sf);
                    break;
                default:
                    xsds.add(sf);
                    break;
            }
            resource.addInclude(name);
        }

        resources = Collections.singletonList(resource);

        if (buildSchemas) {
            List<File> configs = (xmlConfigs == null || xmlConfigs.isEmpty()) ? Collections.emptyList()
                : Stream.of(xmlConfigs.split(",")).flatMap(s ->
                    Stream.of(new File(s), new File(base, s)).filter(File::exists)
                ).collect(Collectors.toList());

            List<File> classPathList = new ArrayList<>();
            List<URL> urls = new ArrayList<>();
            if (classPath != null) {
                for (String classpathElement : classPath.split(",")) {
                    File file = new File(classpathElement);
                    classPathList.add(file);
                    try {
                        urls.add(file.toURI().toURL());
                    } catch (MalformedURLException e) {
                        throw new MojoFailureException("invalid classpath: "+file, e);
                    }
                }
            }
            ClassLoader cl = new URLClassLoader(urls.toArray(new URL[0]));
            EntityResolver entityResolver = MavenPluginResolver.getResolver(catalogLocation);
            URI sourceDirURI = new File(sourceDir).toURI();
            entityResolver = new PassThroughResolver(cl, entityResolver, sourceDirURI, baseSchemaLocation);

            Parameters params = new Parameters();
            params.setXsdFiles(files(xsds));
            params.setWsdlFiles(files(wsdls));
            params.setJavaFiles(files(javas));
            params.setConfigFiles(files(configs));
            params.setClasspath(files(classPathList));
            params.setName(name);
            params.setSrcDir(new File(javaTargetDir));
            params.setClassesDir(new File(classTargetDir));
            params.setNojavac(sourceOnly);
            params.setVerbose(verbose);
            params.setEntityResolver(entityResolver);
            params.setQuiet(quiet && quite); //setting either quiet or quite to false will disable quiet mode
            params.setNoUpa(noUpa);
            params.setNoPvr(noPvr);
            params.setNoAnn(noAnn);
            params.setCopyAnn(copyAnn);
            params.setNoVDoc(noVDoc);
            if (repackage != null && !repackage.isEmpty()) {
                params.setRepackage("org.apache.xmlbeans.metadata:"+repackage);
            }
            if (mdefNamespaces != null && !mdefNamespaces.isEmpty()) {
                params.setMdefNamespaces(new HashSet<>(mdefNamespaces));
            }
            List<XmlError> errorList = new ArrayList<>();
            params.setErrorListener(errorList);

            if (partialMethods != null && !partialMethods.isEmpty()) {
                params.setPartialMethods(parsePartialMethods(partialMethods));
            }
            params.setDownload(download);
            params.setBaseDir(basedir);
            params.setCompiler(compiler);
            params.setMemoryInitialSize(memoryInitialSize);
            params.setMemoryMaximumSize(memoryMaximumSize);
            params.setOutputJar(outputJar);
            params.setDebug(debug);
            params.setExtensions(extensions);

            boolean result = SchemaCompiler.compile(params);

            if (!result) {
                throw new MojoFailureException("Schema compilation failed!\n"+
                    errorList.stream().map(XmlError::toString).collect(Collectors.joining("\n"))
                );
            }

            Resource genResource = new Resource();
            genResource.setDirectory(classTargetDir);
            project.addResource(genResource);
            project.addCompileSourceRoot(javaTargetDir);
        }

    }

    private static File[] files(List<File> files) {
        return (files == null || files.isEmpty()) ? null : files.toArray(new File[0]);
    }

    private static class PassThroughResolver implements EntityResolver {
        private final ClassLoader cl;
        private final EntityResolver delegate;
        private final URI sourceDir;
        //this copy has an / appended
        private final String baseSchemaLocation;

        public PassThroughResolver(ClassLoader cl, EntityResolver delegate, URI sourceDir, String baseSchemaLocation) {
            this.cl = cl;
            this.delegate = delegate;
            this.sourceDir = sourceDir;
            this.baseSchemaLocation = baseSchemaLocation + "/";
        }

        public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
            if (delegate != null) {
                InputSource is = delegate.resolveEntity(publicId, systemId);
                if (is != null) {
                    return is;
                }
            }
            System.out.println("Could not resolve publicId: " + publicId + ", systemId: " + systemId + " from catalog");
            String localSystemId;
            try {
                localSystemId = sourceDir.relativize(new URI(systemId)).toString();
            } catch (URISyntaxException e) {
                throw new IOException("Could not relativeize systemId", e);
            }
            InputStream in = cl.getResourceAsStream(localSystemId);
            if (in != null) {
                System.out.println("found in classpath at: " + localSystemId);
                return new InputSource(in);
            }
            in = cl.getResourceAsStream(baseSchemaLocation + localSystemId);
            if (in != null) {
                System.out.println("found in classpath at: META-INF/" + localSystemId);
                return new InputSource(in);
            }
            System.out.println("Not found in classpath, looking in current directory: " + systemId);
            return new InputSource(systemId);
        }
    }

}
