/*
 * 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 org.apache.openejb.config.AppModule;
import org.apache.openejb.config.BeanProperties;
import org.apache.openejb.config.DeploymentModule;
import org.apache.openejb.config.EjbModule;
import org.apache.openejb.config.EnvEntriesPropertiesDeployer;
import org.apache.openejb.config.PojoConfiguration;
import org.apache.openejb.config.WebModule;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
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.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;

public class SaxAppCtxConfig {
    private static final Logger LOGGER = Logger.getInstance(LogCategory.OPENEJB_STARTUP_CONFIG, SaxAppContextConfig.class);

    public static void parse(final AppModule appModule, final InputSource source,
                             final EnvEntriesPropertiesDeployer envEntriesDeployer, final BeanProperties beanProperties)
        throws SAXException, ParserConfigurationException, IOException {
        Saxs.factory()
            .newSAXParser()
            .parse(source, new SaxAppContextConfig(appModule, envEntriesDeployer, beanProperties));
    }

    private static class SaxAppContextConfig extends StackHandler {
        private static final Collection<String> IMPORT_ALIASES = Arrays.asList("import", "include");
        private static final Collection<String> APPLICATION_ALIASES = Arrays.asList("appcontext", "app-context", "application");
        private static final Collection<String> POJOS_ALIASES = Arrays.asList("pojocontexts", "pojo-contexts", "pojos");
        private static final Collection<String> POJO_ALIASES = Collections.singletonList("pojo");
        private static final Collection<String> BEAN_CONTEXTS_ALIASES = Arrays.asList("beancontexts", "bean-contexts", "ejbs");
        private static final Collection<String> WEBAPP_ALIASES = Arrays.asList("webapps", "webcontexts", "web-contexts", "wars");
        private static final Collection<String> MODULE_ALIASES = Arrays.asList("modulecontext", "module");
        private static final Collection<String> BEAN_CONTEXT_ALIASES = Arrays.asList("ejb", "beancontext", "bean-context");
        private static final Collection<String> CONFIGURATION_ALIASES = Arrays.asList("configuration", "properties", "settings");
        private static final Collection<String> RESOURCES_ALIASES = Collections.singletonList("resources");
        private static final Collection<String> SERVICE_ALIASES = Collections.singletonList("service");
        private static final Collection<String> RESOURCE_ALIASES = Collections.singletonList("resource");
        private static final Collection<String> ENV_ENTRIES_ALIASES = Arrays.asList("enventries", "env-entries");
        private static final Collection<String> ENV_ENTRY_ALIASES = Arrays.asList("enventry", "env-entry");

        private final AppModule module;
        private final EnvEntriesPropertiesDeployer envEntriesDeployer;
        private final BeanProperties beanPropertiesDeployer;

        public SaxAppContextConfig(final AppModule appModule, final EnvEntriesPropertiesDeployer envEntriesDeployer, final BeanProperties beanProperties) {
            this.module = appModule;
            this.envEntriesDeployer = envEntriesDeployer;
            this.beanPropertiesDeployer = beanProperties;
        }

        @Override
        public void startDocument() throws SAXException {
            push(new Document());
        }

        private class Document extends DefaultHandler {
            @Override
            public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
                // by default don't care about root tag
                if (!APPLICATION_ALIASES.contains(localName.toLowerCase())
                    && SystemInstance.get().getOptions().get("openejb.configuration.strict-tags", false)) {
                    throw new IllegalStateException("Unsupported Element: " + localName);

                }
                push(new Root());
            }
        }

        private class Root extends DefaultHandler {
            @Override
            public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
                final String name = localName.toLowerCase();
                if (CONFIGURATION_ALIASES.contains(name)) {
                    push(new Configuration("", module.getProperties()));
                } else if (ENV_ENTRIES_ALIASES.contains(name)) {
                    push(new EnvEntries());
                } else if (BEAN_CONTEXTS_ALIASES.contains(name)) {
                    push(new BeanContexts(null));
                } else if (MODULE_ALIASES.contains(name)) {
                    push(new ModuleContext(attributes.getValue("id")));
                } else if (WEBAPP_ALIASES.contains(name)) {
                    push(new WebAppContext(attributes.getValue("id")));
                } else if (POJOS_ALIASES.contains(name)) {
                    push(new Pojos());
                } else if (RESOURCES_ALIASES.contains(name)) {
                    push(new ResourcesConfig());
                } else if (IMPORT_ALIASES.contains(name)) {
                    importFile(attributes.getValue("path"));
                    push(new DefaultHandler()); // just to keep the stack consistent
                } else {
                    throw new IllegalStateException("Unsupported Element: " + localName);
                }
            }

            private void importFile(final String path) throws SAXException {
                final File file = new File(path);
                if (file.exists()) {
                    try (final InputStream inputStream = new FileInputStream(file)) {
                        parse(module, new InputSource(inputStream), envEntriesDeployer, beanPropertiesDeployer);
                    } catch (final ParserConfigurationException | IOException e) {
                        throw new SAXException(e);
                    }
                } else { // try in the classpath
                    final ClassLoader cl = module.getClassLoader();
                    if (cl != null) {
                        try (final InputStream is = cl.getResourceAsStream(path)) {
                            if (is != null) {
                                try {
                                    parse(module, new InputSource(is), envEntriesDeployer, beanPropertiesDeployer);
                                } catch (final ParserConfigurationException | IOException e) {
                                    throw new SAXException(e);
                                }
                            } else {
                                LOGGER.warning("Can't find " + path);
                            }
                        } catch (final IOException e) {
                            LOGGER.warning("Can't find " + path, e);
                        }
                    } else {
                        LOGGER.warning("Can't find " + path + ", no classloader for the module " + module);
                    }
                }
            }
        }

        private final class Configuration extends Content {
            private final Properties properties;

            private final String prefix;

            private Configuration(final String prefix, final Properties properties) {
                this.properties = properties;
                this.prefix = prefix;
            }

            @Override
            public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
                push(new Configuration(prefix + localName + ".", properties));
            }

            @Override
            public void setValue(final String text) {
                if (properties == null) {
                    return;
                }

                try {
                    for (final Map.Entry<Object, Object> entry : new PropertiesAdapter().unmarshal(text).entrySet()) {
                        properties.put(prefix + entry.getKey(), entry.getValue());
                    }
                } catch (final Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }

        private final class MultipleConfiguration extends Content {
            private final Collection<Properties> properties;

            private final String prefix;

            private MultipleConfiguration(final String prefix, final Collection<Properties> properties) {
                this.properties = properties;
                this.prefix = prefix;
            }

            @Override
            public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
                push(new MultipleConfiguration(prefix + localName + ".", properties));
            }

            @Override
            public void setValue(final String text) {
                if (properties == null) {
                    return;
                }

                try {
                    for (final Properties p : properties) {
                        for (final Map.Entry<Object, Object> entry : new PropertiesAdapter().unmarshal(text).entrySet()) {
                            p.put(prefix + entry.getKey(), entry.getValue());
                        }
                    }
                } catch (final Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }

        private class Pojos extends DefaultHandler {
            protected final List<PojoConfig> genericConfigs = new ArrayList<>();

            protected final Collection<String> aliases;

            private Pojos() {
                this(POJO_ALIASES);
            }

            protected Pojos(final Collection<String> aliases) {
                this.aliases = aliases;
            }

            @Override
            public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
                if (aliases.contains(localName.toLowerCase())) {
                    final PojoConfig beanConfig = newItem();
                    final String id = attributes.getValue("id");
                    if (id == null || "*".equals(id)) {
                        genericConfigs.add(beanConfig);
                    }
                    push(childParser(id, beanConfig));
                } else {
                    throw new IllegalStateException("Unsupported Element: " + localName);
                }
            }

            protected DefaultHandler childParser(final String id, final PojoConfig beanConfig) {
                return new Pojo(id, beanConfig);
            }

            protected PojoConfig newItem() {
                return new PojoConfig();
            }

            @Override
            public void endElement(final String uri, final String localName, final String qName) throws SAXException {
                for (final PojoConfig generic : genericConfigs) {
                    for (final PojoConfiguration config : module.getPojoConfigurations().values()) {
                        for (final String key : generic.getProperties().stringPropertyNames()) {
                            if (!config.getProperties().containsKey(key)) {
                                config.getProperties().put(key, generic.getProperties().get(key));
                            }
                        }
                    }
                }
            }
        }

        private class ModuleContext extends DefaultHandler {
            protected final String id;

            ModuleContext(final String id) {
                this.id = id;
            }

            @Override
            public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
                final String name = localName.toLowerCase();
                if (BEAN_CONTEXTS_ALIASES.contains(name)) {
                    push(new BeanContexts(id));
                } else if (POJOS_ALIASES.contains(name)) {
                    push(new Pojos());
                } else if (CONFIGURATION_ALIASES.contains(name)) {
                    push(new MultipleConfiguration("", propertiesForModule(id)));
                } else {
                    throw new IllegalStateException("Unsupported Element: " + localName);
                }
            }

            protected Collection<Properties> propertiesForModule(final String id) {
                final Collection<Properties> props = new ArrayList<>();
                for (final DeploymentModule m : module.getDeploymentModule()) {
                    if (acceptModule(id, m)) {
                        props.add(m.getProperties());
                    }
                }
                return props;
            }
        }

        private final class WebAppContext extends ModuleContext {
            private WebAppContext(final String id) {
                super(id);
            }

            @Override
            protected Collection<Properties> propertiesForModule(final String id) {
                final Collection<Properties> props = new ArrayList<>();
                for (final WebModule m : module.getWebModules()) {
                    if (acceptModule(id, m)) {
                        props.add(m.getProperties());
                    }
                }
                return props;
            }
        }

        private class BeanContexts extends Pojos {
            private final String id;

            public BeanContexts(final String id) {
                super(BEAN_CONTEXT_ALIASES);
                this.id = id;
            }

            @Override
            protected DefaultHandler childParser(final String beanId, final PojoConfig beanConfig) {
                return new BeanContext(id, beanId, (BeanContextConfig) beanConfig);
            }

            @Override
            protected PojoConfig newItem() {
                return new BeanContextConfig();
            }

            @Override
            public void endElement(final String uri, final String localName, final String qName) throws SAXException {
                for (final PojoConfig generic : genericConfigs) { // BeanContextConfig
                    if (generic.hasProperties()) {
                        beanPropertiesDeployer.addGlobalProperties(id, generic.getProperties());
                    }
                }
            }
        }

        private class Pojo extends DefaultHandler {
            protected final PojoConfig pojoConfig;
            protected final String id;

            public Pojo(final String id, final PojoConfig beanConfig) {
                this.id = id;
                pojoConfig = beanConfig;
            }

            @Override
            public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
                if (CONFIGURATION_ALIASES.contains(localName.toLowerCase())) {
                    push(new Configuration("", pojoConfig.getProperties()));
                } else {
                    throw new IllegalStateException("Unsupported Element: " + localName);
                }
            }

            @Override
            public void endElement(final String uri, final String localName, final String qName) throws SAXException {
                module.getPojoConfigurations().put(id, new PojoConfiguration(pojoConfig.getProperties()));
            }
        }

        private class BeanContext extends Pojo {
            private final String moduleId;

            public BeanContext(final String moduleId, final String id, final BeanContextConfig beanConfig) {
                super(id, beanConfig);
                this.moduleId = moduleId;
            }

            @Override
            public void endElement(final String uri, final String localName, final String qName) throws SAXException {
                for (final EjbModule ejbModule : module.getEjbModules()) {
                    if (!acceptModule(moduleId, ejbModule)) {
                        continue;
                    }

                    if (pojoConfig.hasProperties()) {
                        beanPropertiesDeployer.addProperties(id, pojoConfig.getProperties());
                    }
                }
            }
        }

        private class ResourcesConfig extends DefaultHandler {
            @Override
            public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
                final String name = localName.toLowerCase();
                if (SERVICE_ALIASES.contains(name)) {
                    push(new DeclaredServiceElement(module.getServices()));
                } else if (RESOURCE_ALIASES.contains(name)) {
                    push(new ResourceElement(module.getResources()));
                } else {
                    throw new IllegalStateException("Unsupported Element: " + localName);
                }
                get().startElement(uri, localName, qName, attributes);
            }
        }

        private class EnvEntries extends DefaultHandler {
            @Override
            public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
                if (ENV_ENTRY_ALIASES.contains(localName.toLowerCase())) {
                    push(new EnvEntry());
                    get().startElement(uri, localName, qName, attributes);
                } else {
                    throw new IllegalStateException("Unsupported Element: " + localName);
                }
            }
        }

        private class EnvEntry extends Content {
            private String key;

            @Override
            public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
                if (ENV_ENTRY_ALIASES.contains(localName.toLowerCase())) {
                    key = attributes.getValue("key");
                } else {
                    throw new IllegalStateException("Unsupported Element: " + localName);
                }
            }

            @Override
            public void setValue(final String text) {
                envEntriesDeployer.addEnvEntries(key, text);
            }
        }
    }

    private static boolean acceptModule(final String id, final DeploymentModule ejbModule) {
        return id == null || id.equals(ejbModule.getModuleId());
    }
}
