/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.openejb.config.sys;

import static java.util.Arrays.asList;

import org.apache.openejb.OpenEJBException;
import org.apache.openejb.config.SystemProperty;
import org.apache.openejb.jee.JAXBContextFactory;
import org.apache.openejb.loader.IO;
import org.apache.openejb.util.Join;
import org.apache.openejb.util.Messages;
import org.apache.openejb.util.Saxs;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLFilterImpl;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.MarshalException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.ValidationException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.transform.sax.SAXSource;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

public abstract class JaxbOpenejb {

    @SuppressWarnings({"unchecked"})
    public static <T> T create(final Class<T> type) {
        if (type == null) {
            throw new NullPointerException("type is null");
        }

        if (type == ConnectionManager.class) {
            return (T) createConnectionManager();
        } else if (type == Connector.class) {
            return (T) createConnector();
        } else if (type == Container.class) {
            return (T) createContainer();
        } else if (type == Deployments.class) {
            return (T) createDeployments();
        } else if (type == JndiProvider.class) {
            return (T) createJndiProvider();
        } else if (type == Openejb.class) {
            return (T) createOpenejb();
        } else if (type == ProxyFactory.class) {
            return (T) createProxyFactory();
        } else if (type == Resource.class) {
            return (T) createResource();
        } else if (type == SecurityService.class) {
            return (T) createSecurityService();
        } else if (type == ServiceProvider.class) {
            return (T) createServiceProvider();
        } else if (type == ServicesJar.class) {
            return (T) createServicesJar();
        } else if (type == TransactionManager.class) {
            return (T) createTransactionManager();
        } else if (type == Tomee.class) {
            return (T) createTomee();
        }
        throw new IllegalArgumentException("Unknown type " + type.getName());
    }

    public static <T> T create(final String type) {
        if (type == null) {
            throw new NullPointerException("type is null");
        }

        if (type.equalsIgnoreCase("ConnectionManager")) {
            return (T) createConnectionManager();
        } else if (type.equalsIgnoreCase("Connector")) {
            return (T) createConnector();
        } else if (type.equalsIgnoreCase("Container")) {
            return (T) createContainer();
        } else if (type.equalsIgnoreCase("Deployments")) {
            return (T) createDeployments();
        } else if (type.equalsIgnoreCase("JndiProvider")) {
            return (T) createJndiProvider();
        } else if (type.equalsIgnoreCase("Openejb")) {
            return (T) createOpenejb();
        } else if (type.equalsIgnoreCase("Tomee")) {
            return (T) createTomee();
        } else if (type.equalsIgnoreCase("ProxyFactory")) {
            return (T) createProxyFactory();
        } else if (type.equalsIgnoreCase("Resource")) {
            return (T) createResource();
        } else if (type.equalsIgnoreCase("SecurityService")) {
            return (T) createSecurityService();
        } else if (type.equalsIgnoreCase("ServiceProvider")) {
            return (T) createServiceProvider();
        } else if (type.equalsIgnoreCase("ServicesJar")) {
            return (T) createServicesJar();
        } else if (type.equalsIgnoreCase("TransactionManager")) {
            return (T) createTransactionManager();
        } else if (type.equalsIgnoreCase("Service")) {
            return (T) createService();
        } else if (type.equalsIgnoreCase("System-Property")) {
            return (T) createSystemProperty();
        }
        throw new IllegalArgumentException("Unknown type " + type);
    }

    public static ServicesJar readServicesJar(final String providerPath) throws OpenEJBException {

        final ClassLoader loader = Thread.currentThread().getContextClassLoader();

        final List<URL> sources = new ArrayList<>();

        { // Find URLs in the classpath
            final String path1 = "META-INF/" + providerPath + "/service-jar.xml";
            final String path2 = providerPath.replace('.', '/') + "/service-jar.xml";
            final List<String> paths = Arrays.asList(path1, path2);

            for (final String p : paths) {
                try {
                    final Enumeration<URL> resources = loader.getResources(p);
                    if (resources != null) {
                        sources.addAll(Collections.list(resources));
                    }
                } catch (final IOException e) {
                    throw new OpenEJBException(String.format("Unable to scan for service-jar.xml file: getResource('%s')", p), e);
                }
            }

            if (sources.size() == 0) {
                throw new OpenEJBException("No service-jar.xml files found: searched " + Join.join(" and ", paths));
            }
        }

        final ServicesJar servicesJar = new ServicesJar();

        {
            for (final URL url : sources) {
                try {
                    final InputStream read = IO.read(url);
                    try {
                        final ServicesJar jar = parseServicesJar(read);
                        servicesJar.getServiceProvider().addAll(jar.getServiceProvider());
                    } catch (final Exception e) {
                        throw new OpenEJBException("Unable to parse service-jar.xml file for provider " + providerPath + " at " + url, e);
                    } finally {
                        IO.close(read);
                    }
                } catch (final IOException e) {
                    throw new OpenEJBException("Unable to read service-jar.xml file for provider " + providerPath + " at " + url, e);
                }
            }
        }

        return servicesJar;
    }

    public static ServicesJar parseServicesJar(final InputStream in) throws ParserConfigurationException, SAXException, IOException {
        final InputSource inputSource = new InputSource(in);

        final SAXParser parser = Saxs.namespaceAwareFactory().newSAXParser();

        final ServicesJar servicesJar1 = new ServicesJar();

        parser.parse(inputSource, new OpenEJBHandler(servicesJar1));
        final ServicesJar servicesJar = servicesJar1;
        return servicesJar;
    }

    public static Openejb readConfig(final String configFile) throws OpenEJBException {
        InputStream in = null;
        try {
            if (configFile.startsWith("jar:")) {
                final URL url = new URL(configFile);
                in = IO.read(url);
            } else if (configFile.startsWith("file:")) {
                final URL url = new URL(configFile);
                in = IO.read(url);
            } else {
                in = IO.read(new File(configFile));
            }

            if (configFile.endsWith(".json")) {
                return JSonConfigReader.read(Openejb.class, in);
            }

            return readConfig(new InputSource(in));
        } catch (final MalformedURLException e) {
            throw new OpenEJBException("Unable to resolve location " + configFile, e);
        } catch (final Exception e) {
            throw new OpenEJBException("Unable to read OpenEJB configuration file at " + configFile, e);
        } finally {
            IO.close(in);
        }
    }

    public static Openejb readConfig(final InputSource in) throws IOException, SAXException, ParserConfigurationException {
        return SaxOpenejb.parse(in);
    }

    public static void writeConfig(final String configFile, final Openejb openejb) throws OpenEJBException {
        OutputStream out = null;
        try {
            final File file = new File(configFile);
            out = IO.write(file);
            marshal(Openejb.class, openejb, out);
        } catch (final IOException e) {
            throw new OpenEJBException(messages().format("conf.1040", configFile, e.getLocalizedMessage()), e);
        } catch (final MarshalException e) {
            if (e.getCause() instanceof IOException) {
                throw new OpenEJBException(messages().format("conf.1040", configFile, e.getLocalizedMessage()), e);
            } else {
                throw new OpenEJBException(messages().format("conf.1050", configFile, e.getLocalizedMessage()), e);
            }
        } catch (final ValidationException e) {
            /* TODO: Implement informative error handling here.
               The exception will say "X doesn't match the regular
               expression Y"
               This should be checked and more relevant information
               should be given -- not everyone understands regular
               expressions.
             */
            /* NOTE: This doesn't seem to ever happen. When the object graph
             * is invalid, the MarshalException is thrown, not this one as you
             * would think.
             */
            throw new OpenEJBException(messages().format("conf.1060", configFile, e.getLocalizedMessage()), e);
        } catch (final JAXBException e) {
            throw new OpenEJBException(e);
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (final Exception e) {
                    // no-op
                }
            }
        }
    }

    private static Messages messages() { // new is fine cause for errors only
        return new Messages("org.apache.openejb.util.resources");
    }

    public static final ThreadLocal<Set<String>> currentPublicId = new ThreadLocal<Set<String>>();

    private static final Map<Class, JAXBContext> jaxbContexts = new HashMap<Class, JAXBContext>();

    public static <T> String marshal(final Class<T> type, final Object object) throws JAXBException {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();

        marshal(type, object, baos);

        return new String(baos.toByteArray());
    }

    public static <T> void marshal(final Class<T> type, final Object object, final OutputStream out) throws JAXBException {
        final JAXBContext jaxbContext = getContext(type);
        final Marshaller marshaller = jaxbContext.createMarshaller();

        marshaller.setProperty("jaxb.formatted.output", true);

        marshaller.marshal(object, out);
    }

    public static <T> JAXBContext getContext(final Class<T> type) throws JAXBException {
        JAXBContext jaxbContext = jaxbContexts.get(type);
        if (jaxbContext == null) {
            final Thread thread = Thread.currentThread();
            final ClassLoader old = thread.getContextClassLoader();
            thread.setContextClassLoader(JaxbOpenejb.class.getClassLoader());
            try {
                jaxbContext = JAXBContextFactory.newInstance(type);
            } finally {
                thread.setContextClassLoader(old);
            }
            jaxbContexts.put(type, jaxbContext);
        }
        return jaxbContext;
    }

    public static <T> T unmarshal(final Class<T> type, final InputStream in, final boolean filter,
                                  final String expectedNamespace, final String... aliasNamespaces)
            throws ParserConfigurationException, SAXException, JAXBException {
        final InputSource inputSource = new InputSource(in);

        final SAXParser parser = Saxs.namespaceAwareFactory().newSAXParser();

        final JAXBContext ctx = getContext(type);
        final Unmarshaller unmarshaller = ctx.createUnmarshaller();
        unmarshaller.setEventHandler(validationEvent -> false);

        final SAXSource source;
        if (filter) {
            final NamespaceFilter xmlFilter = new NamespaceFilter(parser.getXMLReader());
            xmlFilter.setContentHandler(unmarshaller.getUnmarshallerHandler());
            source = new SAXSource(xmlFilter, inputSource);
        } else if (expectedNamespace != null) {
            final WhitelistFilter xmlFilter = new WhitelistFilter(parser.getXMLReader(), expectedNamespace, aliasNamespaces);
            xmlFilter.setContentHandler(unmarshaller.getUnmarshallerHandler());
            source = new SAXSource(xmlFilter, inputSource);
        } else {
            source = new SAXSource(inputSource);
        }

        currentPublicId.set(new TreeSet<>());
        try {
            return unmarshaller.unmarshal(source, type).getValue();
        } finally {
            currentPublicId.set(null);
        }
    }

    public static <T> T unmarshal(final Class<T> type, final InputStream in, final boolean filter) throws ParserConfigurationException, SAXException, JAXBException {
        return unmarshal(type, in, filter, null);
    }

    @SuppressWarnings({"unchecked"})
    public static <T> T unmarshal(final Class<T> type, final InputStream in) throws ParserConfigurationException, SAXException, JAXBException {
        return unmarshal(type, in, true);
    }

    public static class WhitelistFilter extends XMLFilterImpl {

        private final Set<String> aliases;
        private final String forcedNamespace;

        public WhitelistFilter(final XMLReader xmlReader, final String forcedNamespace, final String[] aliases) {
            super(xmlReader);
            this.forcedNamespace = forcedNamespace;
            this.aliases = new HashSet<>(asList(aliases));
        }

        public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException, IOException {
            final Set<String> publicIds = currentPublicId.get();
            if (publicIds != null) {
                publicIds.add(publicId);
            }
            return super.resolveEntity(publicId, systemId);
        }

        @Override
        public void startElement(final String inputUri, final String localName, final String qname, final Attributes atts) throws SAXException {
            super.startElement(aliases.contains(inputUri) ? forcedNamespace : inputUri, localName, qname, atts);
        }
    }

    public static class NamespaceFilter extends XMLFilterImpl {

        public NamespaceFilter(final XMLReader xmlReader) {
            super(xmlReader);
        }

        public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException, IOException {
            final Set<String> publicIds = currentPublicId.get();
            if (publicIds != null) {
                publicIds.add(publicId);
            }
            return super.resolveEntity(publicId, systemId);
        }

        public void startElement(final String uri, final String localName, final String qname, final Attributes atts) throws SAXException {
            super.startElement("http://www.openejb.org/System/Configuration", localName, qname, atts);
        }
    }


    public static ConnectionManager createConnectionManager() {
        return new ConnectionManager();
    }

    public static Connector createConnector() {
        return new Connector();
    }

    public static Container createContainer() {
        return new Container();
    }

    public static Deployments createDeployments() {
        return new Deployments();
    }

    public static Service createService() {
        return new Service();
    }

    public static SystemProperty createSystemProperty() {
        return new SystemProperty();
    }

    public static JndiProvider createJndiProvider() {
        return new JndiProvider();
    }

    public static Openejb createOpenejb() {
        return new Openejb();
    }

    private static Tomee createTomee() {
        return new Tomee();
    }

    public static ProxyFactory createProxyFactory() {
        return new ProxyFactory();
    }

    public static Resource createResource() {
        return new Resource();
    }

    public static SecurityService createSecurityService() {
        return new SecurityService();
    }

    public static ServiceProvider createServiceProvider() {
        return new ServiceProvider();
    }

    public static ServicesJar createServicesJar() {
        return new ServicesJar();
    }

    public static TransactionManager createTransactionManager() {
        return new TransactionManager();
    }

    private static class OpenEJBHandler extends DefaultHandler {
        private final PropertiesAdapter propertiesAdapter = new PropertiesAdapter();
        private final ServicesJar servicesJar;

        private ServiceProvider provider;
        private StringBuilder content;

        public OpenEJBHandler(final ServicesJar servicesJar1) {
            this.servicesJar = servicesJar1;
        }

        public void startDocument() throws SAXException {
        }

        public void startElement(final String uri, final String localName, final String qName, final Attributes att) throws SAXException {
            if (!localName.equals("ServiceProvider")) {
                return;
            }

            provider = new ServiceProvider();
            provider.setId(att.getValue("", "id"));
            provider.setService(att.getValue("", "service"));
            provider.setFactoryName(att.getValue("", "factory-name"));
            provider.setConstructor(att.getValue("", "constructor"));
            provider.setClassName(att.getValue("", "class-name"));
            provider.setParent(att.getValue("", "parent"));
            final String typesString = att.getValue("", "types");
            if (typesString != null) {
                final ListAdapter listAdapter = new ListAdapter();
                final List<String> types = listAdapter.unmarshal(typesString);
                provider.getTypes().addAll(types);
            }
            servicesJar.getServiceProvider().add(provider);
        }

        public void characters(final char[] ch, final int start, final int length) throws SAXException {
            if (content == null) {
                content = new StringBuilder();
            }
            content.append(ch, start, length);
        }

        public void endElement(final String uri, final String localName, final String qName) throws SAXException {
            if (provider == null || content == null) {
                return;
            }

            try {
                provider.getProperties().putAll(propertiesAdapter.unmarshal(content.toString()));
            } catch (final Exception e) {
                throw new SAXException(e);
            }
            provider = null;
            content = null;
        }
    }
}
