/*
 * 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.arquillian.common;

import org.apache.openejb.loader.ProvisioningUtil;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.loader.provisining.ProvisioningResolver;
import org.apache.tomee.util.QuickServerXmlParser;
import org.codehaus.swizzle.stream.ReplaceStringsInputStream;
import org.jboss.arquillian.container.spi.client.container.LifecycleException;
import org.jboss.shrinkwrap.api.ArchivePaths;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;

import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import static java.util.Collections.emptyMap;

/**
 * @version $Rev$ $Date$
 */
public class Setup {
    private static final Logger LOGGER = Logger.getLogger(Setup.class.getName()); // JUL is used by arquillian so that's fine
    public static final String TOMEE_BEAN_DISCOVERER_JAR = "lib" + File.separator + "xx-arquillian-tomee-bean-discoverer.jar"; // starts with xx to not be filtered
    private static final String DEFAULT_MEM_CONFIG = (javaVersion() >= 1.8 ? "" : "-XX:PermSize=64m -XX:MaxPermSize=256m ")
                                                            + "-Xmx512m -Xms256m -XX:ReservedCodeCacheSize=64m";

    private static double javaVersion() {
        try {
            return Double.parseDouble(System.getProperty("java.version", "1.7").substring(0, 3));
        } catch (final Exception nfe) {
            return 1.6;
        }
    }

    public static void exportProperties(final File tomeeHome, final TomEEConfiguration c, final boolean defaultMem) {
        final int httpPort = c.getHttpPort();
        System.setProperty("java.naming.provider.url", "http://" + c.getHost() + ":" + httpPort + "/tomee/ejb");
        System.setProperty("connect.tries", "90");
        System.setProperty("server.http.port", String.valueOf(httpPort));
        System.setProperty("server.shutdown.port", String.valueOf(c.getStopPort()));
        if (defaultMem) {
            System.setProperty("java.opts", DEFAULT_MEM_CONFIG + " -Dtomee.httpPort=" + httpPort);
        } else {
            System.setProperty("java.opts", "-Dtomee.httpPort=" + httpPort);
        }
        System.setProperty("openejb.home", tomeeHome.getAbsolutePath());
        System.setProperty("tomee.home", tomeeHome.getAbsolutePath());
    }

    public static void updateServerXml(final File tomeeHome, final TomEEConfiguration configuration) throws IOException {
        final File serverXml = Files.path(new File(tomeeHome.getAbsolutePath()), "conf", "server.xml");
        if (!serverXml.exists()) {
            return;
        }

        final QuickServerXmlParser ports = QuickServerXmlParser.parse(serverXml);
        if (configuration.getKeepServerXmlAsThis()) {
            // force ports to be able to stop the server and get @ArquillianResource
            configuration.setHttpPort(Integer.parseInt(ports.http()));
            configuration.setStopPort(Integer.parseInt(ports.stop()));
            return; // in this case we don't want to override the conf
        }

        final Map<String, String> replacements = new HashMap<>();
        replacements.put(ports.http(), String.valueOf(configuration.getHttpPort()));
        replacements.put(ports.https(), String.valueOf(configuration.getHttpsPort()));
        replacements.put(ports.stop(), String.valueOf(configuration.getStopPort()));
        replacements.put(ports.ajp(), String.valueOf(ajpPort(configuration)));

        if (configuration.isUnpackWars()) {
            replacements.put("unpackWARs=\"false\"", "unpackWARs=\"true\"");
        } else {
            replacements.put("unpackWARs=\"true\"", "unpackWARs=\"false\"");
        }

        replace(replacements, serverXml, true);
    }

    public static File findHome(File directory) {

        directory = directory.getAbsoluteFile();

        final File f = findHomeImpl(directory);

        if (null == f) {
            LOGGER.log(Level.INFO, "Unable to find home in: " + directory);
        }

        return f;
    }

    public static File findHomeImpl(final File directory) {
        final File conf = new File(directory, "conf").getAbsoluteFile();
        final File webapps = new File(directory, "webapps").getAbsoluteFile();

        if (conf.exists() && conf.isDirectory() && webapps.exists() && webapps.isDirectory()) {
            return directory;
        }

        final File[] files = directory.listFiles();
        if (null != files) {

            for (final File file : files) {
                if (".".equals(file.getName()) || "..".equals(file.getName())) {
                    continue;
                }

                final File found = findHome(file);

                if (found != null) {
                    return found;
                }
            }
        }

        return null;
    }

    public static File downloadAndUnpack(final File dir, final String artifactID, final String defaultTempDir) throws LifecycleException {

        final File zipFile = downloadFile(artifactID, null, defaultTempDir);

        Zips.unzip(zipFile, dir);

        return findHome(dir);
    }

    public static File downloadFile(final String artifactName, final String altUrl, final String defaultTempDir) {
        final String cache = SystemInstance.isInitialized() ?
            SystemInstance.get().getOptions().get(ProvisioningResolver.OPENEJB_DEPLOYER_CACHE_FOLDER, (String) null) :
            System.getProperty(ProvisioningResolver.OPENEJB_DEPLOYER_CACHE_FOLDER);
        if (cache == null && defaultTempDir != null) { // let the user override it
            System.setProperty(ProvisioningResolver.OPENEJB_DEPLOYER_CACHE_FOLDER, defaultTempDir);
        }

        try {
            final File artifact = MavenCache.getArtifact(artifactName, altUrl);
            if (artifact == null) {
                throw new NullPointerException(String.format("No such artifact: %s", artifactName));
            }
            return artifact.getAbsoluteFile();
        } finally {
            if (cache == null) {
                System.clearProperty(ProvisioningResolver.OPENEJB_DEPLOYER_CACHE_FOLDER);
            }
        }
    }

    public static boolean isRunning(final String host, final int port) {
        Socket socket = null;
        try {
            socket = new Socket(host, port);
            socket.getOutputStream().close();
            return true;
        } catch (final Exception e) {
            return false;
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (final IOException ignored) {
                    // no-op
                }
            }
        }
    }

    public static void replace(final Map<String, String> replacements, final File file, final boolean escape) throws IOException {

        InputStream in = IO.read(file);

        if (escape) {
            final Map<String, String> escaped = new HashMap<>();
            for (final Map.Entry<String, String> entry : replacements.entrySet()) {
                final String key = entry.getKey();
                final String value = entry.getValue();
                if (key.startsWith("\"") && key.endsWith("\"")) {
                    escaped.put(key, value);
                } else {
                    escaped.put("\"" + key + "\"", "\"" + value + "\"");
                }
            }

            replacements.clear();
            replacements.putAll(escaped);
        }

        in = new ReplaceStringsInputStream(in, replacements);

        final String data = IO.slurp(in);

        IO.copy(data.getBytes(), file);

        if (LOGGER.isLoggable(Level.FINE)) {
            IO.copy(data.getBytes(), System.out);
        }
    }

    public static void removeUselessWebapps(final File tomeeHome, final String... exceptions) {
        final File webapps = new File(tomeeHome, "webapps");
        if (webapps.isDirectory()) {
            final File[] files = webapps.listFiles();
            if (files != null) {
                for (final File webapp : files) {
                    boolean delete = true;
                    if (exceptions != null) {
                        for (final String ignore : exceptions) {
                            if (webapp.getName().equals(ignore)) {
                                delete = false;
                                break;
                            }
                        }
                    }
                    if (delete) {
                        org.apache.openejb.loader.Files.delete(webapp);
                    }
                }
            }
        }
    }

    public static void configureServerXml(final File tomeeHome, final TomEEConfiguration configuration) throws IOException {

        if (configuration.getServerXml() != null) {

            final File serverXml = new File(configuration.getServerXml());

            if (!serverXml.exists()) {
                LOGGER.severe("Provided server.xml doesn't exist: '" + serverXml.getPath() + "'");
            } else {

                // Read in the contents to memory so we can avoid re-reading for parsing
                final String data = IO.slurp(serverXml);

                IO.copy(data.getBytes(), Files.path(tomeeHome, "conf", "server.xml"));
            }
        }
        updateServerXml(tomeeHome, configuration);
    }

    private static int ajpPort(final TomEEConfiguration config) {
        try {
            final Method ajbPort = config.getClass().getMethod("getAjpPort");
            return (Integer) ajbPort.invoke(config);
        } catch (final Exception e) {
            return Integer.parseInt(QuickServerXmlParser.DEFAULT_AJP_PORT);
        }
    }

    public static void configureSystemProperties(final File tomeeHome, final TomEEConfiguration configuration) {
        final File file = Files.path(tomeeHome, "conf", "system.properties");

        // Must use an actual properties object to avoid duplicate keys
        final Properties properties = new Properties();

        if (file.exists()) {
            try {
                IO.readProperties(file, properties);
            } catch (final IOException e) {
                LOGGER.log(Level.SEVERE, "Can't read " + file.getAbsolutePath(), e);
            }
        }

        if (configuration.getProperties() != null) {
            try {
                final InputStream bytes = IO.read(configuration.getProperties().getBytes());
                IO.readProperties(bytes, properties);
            } catch (final IOException e) {
                LOGGER.log(Level.SEVERE, "Can't parse <property name=\"properties\"> value '" + configuration.getProperties() + "'", e);
            }
        }

        if (configuration.isQuickSession()) {
            properties.put("openejb.session.manager", "org.apache.tomee.catalina.session.QuickSessionManager");
        }

        if (configuration.isUnsafeEjbd() && "*".equals(properties.getProperty("tomee.serialization.class.blacklist", "-").trim())) {
            properties.remove("tomee.serialization.class.blacklist");
            properties.put("tomee.serialization.class.whitelist", "*");
            System.setProperty("tomee.serialization.class.blacklist", System.getProperty("tomee.serialization.class.blacklist", "-"));
            reloadClientSerializationConfig();
        }

        try {
            IO.writeProperties(file, properties);
        } catch (final IOException e) {
            LOGGER.log(Level.SEVERE, "Can't save system properties " + file.getAbsolutePath(), e);
        }
    }

    public static void reloadClientSerializationConfig() {
        try {
            Thread.currentThread().getContextClassLoader().loadClass("org.apache.openejb.client.EjbObjectInputStream")
                .getMethod("reloadResolverConfig").invoke(null);
        } catch (final IllegalAccessException | InvocationTargetException | ClassNotFoundException | NoSuchMethodException e) {
            // not a pb normally
        }
    }


    public static void synchronizeFolder(final File tomeeHome, final String src, final String dir) {
        if (src != null && !src.isEmpty()) {
            final File confSrc = new File(src);
            if (confSrc.exists()) {
                final File conf = new File(tomeeHome, dir);
                final Collection<File> files = org.apache.openejb.loader.Files.collect(confSrc, TrueFilter.INSTANCE);
                files.remove(confSrc);
                for (final File f : files) {
                    if (f.equals(confSrc) || f.getName().startsWith(".")) {
                        continue;
                    }

                    final String relativize = relativize(f, confSrc);
                    if (f.isDirectory()) {
                        final String path = dir + '/' + relativize;
                        Files.mkdir(new File(tomeeHome, path));
                        synchronizeFolder(tomeeHome, f.getAbsolutePath(), path);
                    } else {
                        try {
                            final File to = new File(conf, relativize);
                            Files.mkdir(to.getParentFile());
                            org.apache.openejb.loader.IO.copy(f, to);
                        } catch (final Exception e) {
                            LOGGER.log(Level.WARNING, "Ignoring copy of " + f.getAbsolutePath(), e);
                        }
                    }
                }
            } else {
                LOGGER.warning("Can't find " + confSrc.getAbsolutePath());
            }
        }
    }

    private static String relativize(final File f, final File base) {
        return f.getAbsolutePath().substring(base.getAbsolutePath().length() + 1);
    }

    public static void installArquillianBeanDiscoverer(final File home) {
        final File destination = new File(home, TOMEE_BEAN_DISCOVERER_JAR);
        ShrinkWrap.create(JavaArchive.class, destination.getName())
                .addManifest() // avoid NPE from org.apache.tomcat.util.scan.StandardJarScanner.processManifest()
                .addClasses(BeanDicovererInstaller.class, TestClassDiscoverer.class, ArquillianFilterRunner.class, Discover.class)
                .addAsManifestResource(new StringAsset(BeanDicovererInstaller.class.getName()), ArchivePaths.create("org.apache.openejb.extension"))
                .as(ZipExporter.class).exportTo(destination, false);
    }

    public static void removeArquillianBeanDiscoverer(final File home) {
        final File destination = new File(home, TOMEE_BEAN_DISCOVERER_JAR);
        Files.delete(destination);
    }

    public static Map<File, String> addTomEELibraries(final File libFolder, final String additionalLibs, final boolean parseArgs) {
        if (additionalLibs == null) {
            return emptyMap();
        }
        final String libs = additionalLibs.trim();
        if (libs.isEmpty()) {
            return emptyMap();
        }

        try { // initializing the SystemInstance because we'll need it for configuration
            if (!SystemInstance.isInitialized()) {
                SystemInstance.init(new Properties());
            }
        } catch (final Exception e) {
            // no-op
        }

        final Map<File, String> configs = new HashMap<>();
        for (final String lib : libs.split("\n")) {
            final String trim = lib.trim();
            if (trim.isEmpty() || trim.startsWith("#")) {
                continue;
            }

            final String args;
            if (parseArgs) { // javaagent friendly
                int argsIdx = trim.indexOf('=');
                if (argsIdx < 0) {
                    argsIdx = trim.indexOf('?');
                }
                if (argsIdx > 0) {
                    args = trim.substring(argsIdx);
                } else {
                    args = "";
                }
            } else {
                args = null;
            }

            if (trim.startsWith("remove:")) { // like mvn plugin, needed to use plus but switch something like the jpa provider
                final String prefix = trim.substring("remove:".length());
                final File[] children = libFolder.listFiles(new FilenameFilter() {
                    @Override
                    public boolean accept(final File dir, final String name) {
                        return name.startsWith(prefix);
                    }
                });
                if (children != null && children.length > 0) {
                    for (final File child : children) {
                        if (!IO.delete(child) && child.getName().endsWith(".jar")) { // try to rename it to have it ignored
                            child.renameTo(new File(child.getParentFile(), child.getName() + "_renamed"));
                        }
                    }
                }
            } else {
                final Set<String> locations = ProvisioningUtil.realLocation(trim);
                for (final String location : locations) {
                    final File from = new File(location);
                    try {
                        final File to = new File(libFolder, from.getName());
                        org.apache.openejb.loader.IO.copy(from, to);
                        if (args != null) {
                            configs.put(to, args);
                        }
                    } catch (final IOException e) {
                        throw new IllegalArgumentException(e);
                    }
                }
            }
        }
        return configs;
    }

    private static class TrueFilter implements FileFilter {
        private static final TrueFilter INSTANCE = new TrueFilter();

        @Override
        public boolean accept(final File pathname) {
            return true;
        }
    }
}
