/*
 * 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.maven.plugin;

import org.apache.maven.plugins.annotations.Parameter;
import org.apache.openejb.OpenEJBRuntimeException;
import org.apache.openejb.assembler.Deployer;
import org.apache.openejb.client.RemoteInitialContextFactory;
import org.apache.openejb.config.RemoteServer;
import org.apache.openejb.loader.Files;
import org.codehaus.plexus.util.FileUtils;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

public abstract class UpdatableTomEEMojo extends AbstractTomEEMojo {
    public static final int INITIAL_DELAY = 5000;
    public static final String RELOAD_CMD = "reload";

    @Parameter
    private Synchronization synchronization;

    @Parameter
    private List<Synch> synchronizations;

    @Parameter(property = "tomee-plugin.baseDir", defaultValue = "${project.basedir}", readonly = true)
    private File baseDir;

    @Parameter(property = "tomee-plugin.reload-on-update", defaultValue = "false")
    private boolean reloadOnUpdate;

    private Timer timer;
    private SynchronizerRedeployer task;

    @Override
    protected void run() {
        if (synchronization != null) {
            initSynchronization(synchronization);
            avoidAutoReload();
        }
        if (synchronizations != null) {
            for (final Synch s : synchronizations) {
                if (s.getSource() == null || s.getTarget() == null) {
                    getLog().warn("Source or Target directory missing to a <synch> block, skipping");
                    continue;
                }
                initSynch(s);
                avoidAutoReload();
            }
        }

        if (startSynchronizers()) {
            forceReloadable = true;
        }

        if (removeTomeeWebapp && !ejbRemote) {
            getLog().warn("TomEE webapp is asked to be removed (<ejbRemote>true> or <removeTomeeWebapp>true</removeTomeeWebapp>) so you can use reload feature");
        }

        super.run();
    }

    private void avoidAutoReload() {
        if (systemVariables == null) {
            systemVariables = new HashMap<String, String>();
        }
        if (!systemVariables.containsKey("tomee.classloader.skip-background-process")) {
            systemVariables.put("tomee.classloader.skip-background-process", "true");
        }
    }

    private void initSynch(final AbstractSynchronizable s) {
        s.getExtensions().addAll(s.getUpdateOnlyExtenions());
        if (reloadOnUpdate) {
            deployOpenEjbApplication = true;
            if (systemVariables == null) {
                systemVariables = new HashMap<>();
                systemVariables.put("tomee.remote.support", "true");
            }
        }
    }

    private void initSynchronization(final Synchronization synchronization) {
        // defaults values for main synchronization block
        final String destination = destinationName().replaceAll("\\.[jew]ar", "");
        if (synchronization.getBinariesDir() == null) {
            synchronization.setBinariesDir(classes);
        }
        if (synchronization.getResourcesDir() == null) {
            synchronization.setResourcesDir(new File(baseDir, "src/main/webapp"));
        }
        if (synchronization.getTargetResourcesDir() == null) {
            synchronization.setTargetResourcesDir(new File(catalinaBase, webappDir + "/" + destination));
        }
        if (synchronization.getTargetBinariesDir() == null) {
            synchronization.setTargetBinariesDir(new File(catalinaBase, webappDir + "/" + destination + "/WEB-INF/classes"));
        }
        if (synchronization.getUpdateInterval() <= 0) {
            synchronization.setUpdateInterval(5); // sec
        }
        if (synchronization.getExtensions() == null) {
            synchronization.setExtensions(new ArrayList<String>(Arrays.asList(".html", ".css", ".js", ".xhtml")));
        }
        if (synchronization.getUpdateOnlyExtenions() == null) {
            synchronization.setUpdateOnlyExtensions(Collections.<String>emptyList());
        }

        initSynch(synchronization);
    }

    @Override
    protected void addShutdownHooks(final RemoteServer server) {
        if (synchronization != null || synchronizations != null) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    task.cancel();
                    timer.cancel();
                }
            });
        }
        super.addShutdownHooks(server);
    }

    protected boolean startSynchronizers() {
        timer = new Timer("tomee-maven-plugin-synchronizer");

        final Collection<Synchronizer> synchronizers = new ArrayList<Synchronizer>();

        long interval = 5000; // max of all sync interval

        if (synchronization != null) {
            synchronizers.add(new Synchronizer(synchronization));
            interval = TimeUnit.SECONDS.toMillis(synchronization.getUpdateInterval());
        }
        if (synchronizations != null) {
            for (final AbstractSynchronizable s : synchronizations) {
                synchronizers.add(new Synchronizer(s));
                if (interval < s.getUpdateInterval()) {
                    interval = TimeUnit.SECONDS.toMillis(s.getUpdateInterval());
                }
            }
        }

        // serializing synchronizers to avoid multiple updates at the same time and reload a single time the app
        if (!synchronizers.isEmpty()) {
            task = new SynchronizerRedeployer(synchronizers);
            getLog().info("Starting synchronizer with an update interval of " + interval);
            if (interval > INITIAL_DELAY) {
                timer.scheduleAtFixedRate(task, interval, interval);
            } else {
                timer.scheduleAtFixedRate(task, INITIAL_DELAY, interval);
            }
            return true;
        }
        return false;
    }

    @Override
    protected Collection<String> availableCommands() {
        final Collection<String> cmds = new ArrayList<String>();
        cmds.addAll(super.availableCommands());
        cmds.add(RELOAD_CMD);
        return cmds;
    }

    @Override
    protected boolean handleLine(final String line) {
        if (super.handleLine(line)) {
            return true;
        } else if (isReload(line)) {
            reload();
            return true;
        }
        return false;
    }

    private static boolean isReload(String line) {
        if (RELOAD_CMD.equalsIgnoreCase(line)) {
            return true;
        }

        //http://youtrack.jetbrains.com/issue/IDEA-94826
        line = new StringBuilder(line).reverse().toString();

        return RELOAD_CMD.equalsIgnoreCase(line);
    }

    public synchronized void reload() {
        if (deployOpenEjbApplication) {
            String path = deployedFile.getAbsolutePath();
            if (path.endsWith(".war") || path.endsWith(".ear")) {
                path = path.substring(0, path.length() - ".war".length());
            }
            getLog().info("Reloading " + path);
            deployer().reload(path);
        } else {
            getLog().warn("Reload command needs to activate openejb internal application. " +
                    "Add <deployOpenEjbApplication>true</deployOpenEjbApplication> to the plugin configuration to force it.");
        }
    }

    private class SynchronizerRedeployer extends TimerTask {
        private final Collection<Synchronizer> delegates;

        public SynchronizerRedeployer(final Collection<Synchronizer> synchronizers) {
            delegates = synchronizers;
        }

        @Override
        public void run() {
            int updated = 0;
            for (final Synchronizer s : delegates) {
                try {
                    updated += s.call();
                } catch (final Exception e) {
                    getLog().error(e.getMessage(), e);
                }
            }

            if (updated > 0 && reloadOnUpdate) {
                if (deployedFile != null && deployedFile.exists()) {
                    reload();
                }
            }
        }
    }

    private class Synchronizer implements Callable<Integer> {
        private final FileFilter fileFilter;
        private final FileFilter updateOnlyFilter;
        private final AbstractSynchronizable synchronization;
        private long lastUpdate = System.currentTimeMillis();

        public Synchronizer(final AbstractSynchronizable synch) {
            synchronization = synch;
            updateOnlyFilter = new SuffixesFileFilter(synchronization.getUpdateOnlyExtenions());
            if (synchronization.getRegex() != null) {
                fileFilter = new SuffixesAndRegexFileFilter(synchronization.getExtensions(), Pattern.compile(synchronization.getRegex()));
            } else {
                fileFilter = new SuffixesFileFilter(synchronization.getExtensions());
            }
        }

        @Override
        public Integer call() throws Exception {
            final long ts = System.currentTimeMillis();
            int updated = 0;
            for (final Map.Entry<File, File> pair : synchronization.updates().entrySet()) {
                updated += updateFiles(pair.getKey(), pair.getValue(), ts);
            }
            lastUpdate = ts;
            return updated;
        }

        private int updateFiles(final File source, final File output, final long ts) {
            if (!source.exists()) {
                getLog().debug(source.getAbsolutePath() + " doesn't exist");
                return 0;
            }

            if (source.isFile()) {
                if (source.lastModified() < lastUpdate) {
                    return 0;
                }

                return updateFile(source, output, source, ts);
            }

            if (!source.isDirectory()) {
                getLog().warn(source.getAbsolutePath() + " is not a directory, skipping");
                return 0;
            }

            final Collection<File> files = Files.collect(source, fileFilter);
            int updated = 0;
            for (final File file : files) {
                if (file.isDirectory()
                        || file.lastModified() < lastUpdate) {
                    continue;
                }

                updated += updateFile(source, output, file, ts);
            }

            return updated;
        }

        private int updateFile(final File source, final File target, final File file, final long ts) {
            final File output;
            if (target.isFile() && target.exists()) {
                output = target;
            } else {
                String relativized = file.getAbsolutePath().replace(source.getAbsolutePath(), "");
                if (relativized.startsWith(File.separator)) {
                    relativized = relativized.substring(1);
                }
                output = new File(target, relativized);
            }

            if (file.exists()) {
                getLog().info("[Updating] " + file.getAbsolutePath() + " to " + output.getAbsolutePath());
            } else {
                getLog().info("[Creating] " + file.getAbsolutePath() + " to " + output.getAbsolutePath());
            }
            try {
                if (!output.getParentFile().exists()) {
                    FileUtils.forceMkdir(output.getParentFile());
                }
                FileUtils.copyFile(file, output);
                if (!output.setLastModified(ts)) {
                    getLog().debug("Can't update last modified date of " + file);
                }
            } catch (final IOException e) {
                getLog().error(e);
            }

            if (updateOnlyFilter.accept(file)) {
                return 0;
            }
            return 1;
        }
    }

    private Deployer deployer() {
        if (removeTomeeWebapp && !ejbRemote) {
            throw new OpenEJBRuntimeException("Can't use reload feature without TomEE Webapp, please set removeTomeeWebapp to false or ejbRemote to true");
        }

        final Properties properties = new Properties();
        properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, RemoteInitialContextFactory.class.getName());
        properties.setProperty(Context.PROVIDER_URL, "http://" + tomeeHost + ":" + tomeeHttpPort + "/tomee/ejb");
        try {
            final Context context = new InitialContext(properties);
            return (Deployer) context.lookup("openejb/DeployerBusinessRemote");
        } catch (final NamingException e) {
            throw new OpenEJBRuntimeException("Can't lookup Deployer", e);
        }
    }

    private static class SuffixesFileFilter implements FileFilter {
        private final String[] suffixes;

        public SuffixesFileFilter(final List<String> extensions) {
            if (extensions == null) {
                suffixes = new String[0];
            } else {
                suffixes = extensions.toArray(new String[extensions.size()]);
            }
        }

        @Override
        public boolean accept(final File file) {
            if (file.isDirectory()) {
                return true;
            }

            for (final String suffix : suffixes) {
                if (file.getName().endsWith(suffix)) {
                    return true;
                }
            }

            return false;
        }
    }

    private class SuffixesAndRegexFileFilter extends SuffixesFileFilter {
        private final Pattern pattern;

        public SuffixesAndRegexFileFilter(final List<String> extensions, final Pattern pattern) {
            super(extensions);
            this.pattern = pattern;
        }

        @Override
        public boolean accept(final File file) {
            return file.isDirectory() || (super.accept(file) && pattern.matcher(file.getAbsolutePath()).matches());

        }
    }
}
