/*
 * Copyright 2011 the original author or authors.
 *
 * 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.gradle.plugins.ear.descriptor.internal;

import groovy.lang.Closure;
import groovy.util.Node;
import groovy.util.XmlParser;
import groovy.xml.QName;
import org.gradle.api.Action;
import org.gradle.api.UncheckedIOException;
import org.gradle.api.XmlProvider;
import org.gradle.api.internal.DomNode;
import org.gradle.api.model.ObjectFactory;
import org.gradle.internal.Cast;
import org.gradle.internal.IoActions;
import org.gradle.internal.UncheckedException;
import org.gradle.internal.file.PathToFileResolver;
import org.gradle.internal.xml.XmlTransformer;
import org.gradle.plugins.ear.descriptor.DeploymentDescriptor;
import org.gradle.plugins.ear.descriptor.EarModule;
import org.gradle.plugins.ear.descriptor.EarSecurityRole;
import org.gradle.plugins.ear.descriptor.EarWebModule;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;

import javax.inject.Inject;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class DefaultDeploymentDescriptor implements DeploymentDescriptor {

    private static final String ACCESS_EXTERNAL_DTD = "http://javax.xml.XMLConstants/property/accessExternalDTD";
    private static final String ALLOW_ANY_EXTERNAL_DTD = "all";

    private final XmlTransformer transformer = new XmlTransformer();
    private final PathToFileResolver fileResolver;

    private ObjectFactory objectFactory;

    private String fileName = "application.xml";
    private String version = "6";
    private String applicationName;
    private Boolean initializeInOrder = Boolean.FALSE;
    private String description;
    private String displayName;
    private String libraryDirectory;
    private Set<EarModule> modules = new LinkedHashSet<EarModule>();
    private Set<EarSecurityRole> securityRoles = new LinkedHashSet<EarSecurityRole>();
    private Map<String, String> moduleTypeMappings = new LinkedHashMap<String, String>();

    @Inject
    public DefaultDeploymentDescriptor(PathToFileResolver fileResolver, ObjectFactory objectFactory) {
        this.fileResolver = fileResolver;
        this.objectFactory = objectFactory;
    }

    @Override
    public String getFileName() {
        return fileName;
    }

    @Override
    public void setFileName(String fileName) {
        this.fileName = fileName;
        readFrom(new File("META-INF", fileName));
    }

    @Override
    public String getVersion() {
        return version;
    }

    @Override
    public void setVersion(String version) {
        this.version = version;
    }

    @Override
    public String getApplicationName() {
        return applicationName;
    }

    @Override
    public void setApplicationName(String applicationName) {
        this.applicationName = applicationName;
    }

    @Override
    public Boolean getInitializeInOrder() {
        return initializeInOrder;
    }

    @Override
    public void setInitializeInOrder(Boolean initializeInOrder) {
        this.initializeInOrder = initializeInOrder;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String getDisplayName() {
        return displayName;
    }

    @Override
    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    @Override
    public String getLibraryDirectory() {
        return libraryDirectory;
    }

    @Override
    public void setLibraryDirectory(String libraryDirectory) {
        this.libraryDirectory = libraryDirectory;
    }

    @Override
    public Set<EarModule> getModules() {
        return modules;
    }

    @Override
    public void setModules(Set<EarModule> modules) {
        this.modules = modules;
    }

    @Override
    public Set<EarSecurityRole> getSecurityRoles() {
        return securityRoles;
    }

    @Override
    public void setSecurityRoles(Set<EarSecurityRole> securityRoles) {
        this.securityRoles = securityRoles;
    }

    @Override
    public Map<String, String> getModuleTypeMappings() {
        return moduleTypeMappings;
    }

    @Override
    public void setModuleTypeMappings(Map<String, String> moduleTypeMappings) {
        this.moduleTypeMappings = moduleTypeMappings;
    }

    @Override
    public DefaultDeploymentDescriptor module(EarModule module, String type) {
        modules.add(module);
        moduleTypeMappings.put(module.getPath(), type);
        return this;
    }

    @Override
    public DefaultDeploymentDescriptor module(String path, String type) {
        return module(new DefaultEarModule(path), type);
    }

    @Override
    public DefaultDeploymentDescriptor webModule(String path, String contextRoot) {
        modules.add(new DefaultEarWebModule(path, contextRoot));
        moduleTypeMappings.put(path, "web");
        return this;
    }

    @Override
    public DefaultDeploymentDescriptor securityRole(EarSecurityRole role) {
        securityRoles.add(role);
        return this;
    }

    @Override
    public DeploymentDescriptor securityRole(String role) {
        securityRoles.add(new DefaultEarSecurityRole(role));
        return this;
    }

    @Override
    public DeploymentDescriptor securityRole(Action<? super EarSecurityRole> action) {
        EarSecurityRole role = objectFactory.newInstance(DefaultEarSecurityRole.class);
        action.execute(role);
        securityRoles.add(role);
        return this;
    }

    @Override
    public DeploymentDescriptor withXml(Closure closure) {
        transformer.addAction(closure);
        return this;
    }

    @Override
    public DeploymentDescriptor withXml(Action<? super XmlProvider> action) {
        transformer.addAction(action);
        return this;
    }

    @Override
    public boolean readFrom(Object path) {
        if (fileResolver == null) {
            return false;
        }
        File descriptorFile = fileResolver.resolve(path);
        if (descriptorFile == null || !descriptorFile.exists()) {
            return false;
        }
        try {
            FileReader reader = new FileReader(descriptorFile);
            readFrom(reader);
            return true;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private static XmlParser createParser() {
        try {
            XmlParser parser = new XmlParser(false, true, true);
            try {
                // If not set for >= JAXP 1.5 / Java8 won't allow referencing DTDs, e.g.
                // using http URLs, because Groovy's XmlParser requests FEATURE_SECURE_PROCESSING
                parser.setProperty(ACCESS_EXTERNAL_DTD, ALLOW_ANY_EXTERNAL_DTD);
            } catch (SAXNotRecognizedException ignore) {
                // property requires >= JAXP 1.5 / Java8
            }
            return parser;
        } catch (Exception ex) {
            throw UncheckedException.throwAsUncheckedException(ex);
        }
    }

    @Override
    public DeploymentDescriptor readFrom(Reader reader) {
        try {
            Node appNode = createParser().parse(reader);
            version = (String) appNode.attribute("version");
            for (final Node child : Cast.<List<Node>>uncheckedCast(appNode.children())) {
                String childLocalName = localNameOf(child);
                switch (childLocalName) {
                    case "application-name":

                        applicationName = child.text();

                        break;
                    case "initialize-in-order":

                        initializeInOrder = Boolean.valueOf(child.text());

                        break;
                    case "description":

                        description = child.text();

                        break;
                    case "display-name":

                        displayName = child.text();

                        break;
                    case "library-directory":

                        libraryDirectory = child.text();

                        break;
                    case "module":

                        EarModule module = null;
                        for (Node moduleNode : Cast.<List<Node>>uncheckedCast(child.children())) {
                            String moduleNodeLocalName = localNameOf(moduleNode);
                            if (moduleNodeLocalName.equals("web")) {
                                String webUri = childNodeText(moduleNode, "web-uri");
                                String contextRoot = childNodeText(moduleNode, "context-root");
                                module = new DefaultEarWebModule(webUri, contextRoot);
                                modules.add(module);
                                moduleTypeMappings.put(module.getPath(), "web");
                            } else if (moduleNodeLocalName.equals("alt-dd")) {
                                assert module != null;
                                module.setAltDeployDescriptor(moduleNode.text());
                            } else {
                                module = new DefaultEarModule(moduleNode.text());
                                modules.add(module);
                                moduleTypeMappings.put(module.getPath(), moduleNodeLocalName);
                            }
                        }

                        break;
                    case "security-role":

                        String roleName = childNodeText(child, "role-name");
                        String description = childNodeText(child, "description");
                        securityRoles.add(new DefaultEarSecurityRole(roleName, description));

                        break;
                    default:
                        withXml(new Action<XmlProvider>() {
                            @Override
                            public void execute(XmlProvider xmlProvider) {
                                xmlProvider.asNode().append(child);
                            }
                        });
                        break;
                }
            }
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        } catch (SAXException ex) {
            throw UncheckedException.throwAsUncheckedException(ex);
        } finally {
            IoActions.closeQuietly(reader);
        }
        return this;
    }

    private static String childNodeText(Node root, String name) {
        for (Node child : Cast.<List<Node>>uncheckedCast(root.children())) {
            if (localNameOf(child).equals(name)) {
                return child.text();
            }
        }
        return null;
    }

    private static String localNameOf(Node node) {
        return node.name() instanceof QName ? ((QName) node.name()).getLocalPart() : String.valueOf(node.name());
    }

    @Override
    public DefaultDeploymentDescriptor writeTo(Object path) {
        transformer.transform(toXmlNode(), fileResolver.resolve(path));
        return this;
    }

    @Override
    public DefaultDeploymentDescriptor writeTo(Writer writer) {
        transformer.transform(toXmlNode(), writer);
        return this;
    }

    private DomNode toXmlNode() {
        DomNode root = new DomNode(nodeNameFor("application"));
        root.attributes().put("version", version);
        if (!"1.3".equals(version)) {
            root.attributes().put("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
        }
        if ("1.3".equals(version)) {
            root.setPublicId("-//Sun Microsystems, Inc.//DTD J2EE Application 1.3//EN");
            root.setSystemId("http://java.sun.com/dtd/application_1_3.dtd");
        } else if ("1.4".equals(version)) {
            root.attributes().put("xsi:schemaLocation", "http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/application_1_4.xsd");
        } else if ("5".equals(version) || "6".equals(version)) {
            root.attributes().put("xsi:schemaLocation", "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/application_" + version + ".xsd");
        } else if ("7".equals(version)) {
            root.attributes().put("xsi:schemaLocation", "http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/application_" + version + ".xsd");
        }
        if (applicationName != null) {
            new Node(root, nodeNameFor("application-name"), applicationName);
        }
        if (description != null) {
            new Node(root, nodeNameFor("description"), description);
        }
        if (displayName != null) {
            new Node(root, nodeNameFor("display-name"), displayName);
        }
        if (initializeInOrder != null && initializeInOrder) {
            new Node(root, nodeNameFor("initialize-in-order"), initializeInOrder);
        }
        for (EarModule module : modules) {
            Node moduleNode = new Node(root, nodeNameFor("module"));
            module.toXmlNode(moduleNode, moduleNameFor(module));
        }
        if (securityRoles != null) {
            for (EarSecurityRole role : securityRoles) {
                Node roleNode = new Node(root, nodeNameFor("security-role"));
                if (role.getDescription() != null) {
                    new Node(roleNode, nodeNameFor("description"), role.getDescription());
                }
                new Node(roleNode, nodeNameFor("role-name"), role.getRoleName());
            }
        }
        if (libraryDirectory != null) {
            new Node(root, nodeNameFor("library-directory"), libraryDirectory);
        }
        return root;
    }

    private Object moduleNameFor(EarModule module) {
        String name = moduleTypeMappings.get(module.getPath());
        if (name == null) {
            if (module instanceof EarWebModule) {
                name = "web";
            } else {
                // assume EJB is the most common kind of EAR deployment
                name = "ejb";
            }
        }
        return nodeNameFor(name);
    }

    private Object nodeNameFor(String name) {
        if ("1.3".equals(version)) {
            return name;
        } else if ("1.4".equals(version)) {
            return new QName("http://java.sun.com/xml/ns/j2ee", name);
        } else if ("5".equals(version) || "6".equals(version)) {
            return new QName("http://java.sun.com/xml/ns/javaee", name);
        } else if ("7".equals(version)) {
            return new QName("http://xmlns.jcp.org/xml/ns/javaee", name);
        } else {
            return new QName(name);
        }
    }

    // For tests
    XmlTransformer getTransformer() {
        return transformer;
    }
}
