/*
 * Copyright 2021 The Apache Software Foundation.
 *
 * 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.apache.tomcat.vestige.launcher;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.jar.JarFile;
import java.util.regex.Pattern;

import org.apache.catalina.Host;
import org.apache.catalina.TomcatController;
import org.apache.catalina.WebResource;
import org.apache.catalina.WebResourceRoot.ResourceSetType;
import org.apache.catalina.WebResourceSet;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.HostConfig;
import org.apache.catalina.startup.HostConfig.DeployWar;
import org.apache.catalina.startup.HostConfig.DeployedApplication;
import org.apache.catalina.util.ContextName;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.Jar;
import org.apache.tomcat.util.scan.TomcatUtilScanController;
import org.apache.tomcat.vestige.launcher.webresources.VestigeJarEntryConnection;
import org.apache.tomcat.vestige.launcher.webresources.VestigeJarEntryFromVestigeJar;
import org.apache.tomcat.vestige.launcher.webresources.VestigeJarResourceSet;
import org.apache.tomcat.vestige.launcher.webresources.VestigeWebResource;

import fr.gaellalire.vestige.spi.resolver.AttachedClassLoader;
import fr.gaellalire.vestige.spi.resolver.VestigeJar;
import fr.gaellalire.vestige.spi.resolver.VestigeJarEntry;

/**
 * @author Gael Lalire
 */
public class VestigeTomcatController implements TomcatController, TomcatUtilScanController, Closeable {

    private static Pattern VWAR_PATTERN = Pattern.compile("(?i:(.*)\\.(vwar))");

    @Override
    public boolean specificFixDocBase(String originalDocBase) {
        return originalDocBase.toLowerCase(Locale.ENGLISH).endsWith(".vwar");
    }

    private static List<VestigeWar> vestigeWars = new ArrayList<VestigeWar>();

    private static ThreadLocal<VestigeWar> vestigeWarThreadLocal = new InheritableThreadLocal<VestigeWar>();

    @Override
    public WebResourceSet createMainResourceSet(StandardRoot caller, File f, String docBase, String baseName) {
        if (f.isFile() && docBase.endsWith(".vwar")) {
            VestigeWar vestigeWar = VestigeWar.create(f, baseName);
            vestigeWarThreadLocal.set(vestigeWar);
            synchronized (vestigeWars) {
                vestigeWars.add(vestigeWar);
            }
            return new VestigeJarResourceSet(caller, "/", vestigeWar.getVestigeWar(), vestigeWar.getVestigeWarDependencies(), "/");
        }
        return null;
    }

    public ClassLoader createSharedLoader(ClassLoader parent, boolean parentFirst) {
        VestigeWar vestigeWar = vestigeWarThreadLocal.get();
        if (vestigeWar == null) {
            return parent;
        }
        AttachedClassLoader sharedAttachedClassLoader = vestigeWar.getSharedAttachedClassLoader();
        if (sharedAttachedClassLoader == null) {
            return parent;
        }
        ClassLoader sharedClassLoader = sharedAttachedClassLoader.getAttachableClassLoader().getClassLoader();
        return new DelegateClassLoader(parent, sharedClassLoader, parentFirst);
    }

    @Override
    public void deploySpecificApp(HostConfig caller, Host host, Set<String> invalidWars, Map<String, DeployedApplication> deployed, File appBase, String[] files) {
        if (files == null)
            return;

        ExecutorService es = host.getStartStopExecutor();
        List<Future<?>> results = new ArrayList<>();

        for (int i = 0; i < files.length; i++) {

            if (files[i].equalsIgnoreCase("META-INF"))
                continue;
            if (files[i].equalsIgnoreCase("WEB-INF"))
                continue;
            File war = new File(appBase, files[i]);
            if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".vwar") && war.isFile() && !invalidWars.contains(files[i])) {

                ContextName cn = new ContextName(files[i], true);

                if (caller.isServiced(cn.getName())) {
                    continue;
                }
                if (caller.deploymentExists(cn.getName())) {
                    DeployedApplication app = deployed.get(cn.getName());

                    boolean unpackWAR = caller.isUnpackWARs();
                    if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) {
                        unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR();
                    }
                    if (!unpackWAR && app != null) {
                        // Need to check for a directory that should not be
                        // there
                        File dir = new File(appBase, cn.getBaseName());
                        if (dir.exists()) {
                            if (!app.loggedDirWarning) {
                                // log.warn(sm.getString("hostConfig.deployWar.hiddenDir",
                                // dir.getAbsoluteFile(),
                                // war.getAbsoluteFile()));
                                app.loggedDirWarning = true;
                            }
                        } else {
                            app.loggedDirWarning = false;
                        }
                    }
                    continue;
                }

                // Check for WARs with /../ /./ or similar sequences in the name
                if (!caller.validateContextPath(appBase, cn.getBaseName())) {
                    // log.error(sm.getString("hostConfig.illegalWarName",
                    // files[i]));
                    invalidWars.add(files[i]);
                    continue;
                }
                cn.setExtension("vwar");

                results.add(es.submit(new DeployWar(caller, cn, war)));
            }
        }

        for (Future<?> result : results) {
            try {
                result.get();
            } catch (Exception e) {
                // log.error(sm.getString("hostConfig.deployWar.threaded.error"),
                // e);
            }
        }

    }

    @Override
    public boolean processWebInfLib(StandardRoot caller, WebResource possibleJar, List<WebResourceSet> classResources) {
        if (possibleJar instanceof VestigeWebResource) {
            VestigeJarEntry vestigeJarEntry = ((VestigeWebResource) possibleJar).getVestigeJarEntry();
            if (vestigeJarEntry instanceof VestigeJarEntryFromVestigeJar) {
                VestigeJarEntryFromVestigeJar vestigeJarEntryFromVestigeJar = (VestigeJarEntryFromVestigeJar) vestigeJarEntry;
                VestigeJarResourceSet resourceSet = new VestigeJarResourceSet(caller, "/WEB-INF/classes", vestigeJarEntryFromVestigeJar.getEntryVestigeJar(),
                        Collections.<VestigeJar> emptyList(), "/");
                resourceSet.setClassLoaderOnly(true);
                classResources.add(resourceSet);
            } else {
                caller.createWebResourceSet(ResourceSetType.CLASSES_JAR, "/WEB-INF/classes", possibleJar.getURL(), "/");
            }
        } else {
            caller.createWebResourceSet(ResourceSetType.CLASSES_JAR, "/WEB-INF/classes", possibleJar.getURL(), "/");
        }
        return true;
    }

    @Override
    public Pattern getSpecificExtensions() {
        return VWAR_PATTERN;
    }

    @Override
    public Jar specificNewInstance(URL url) throws IOException {
        String urlString = url.toString();
        if (urlString.startsWith("jar:mvn:")) {
            URLConnection connection = url.openConnection();
            if (connection instanceof VestigeJarEntryConnection) {
                VestigeJarEntryConnection vestigeJarEntryConnection = (VestigeJarEntryConnection) connection;
                VestigeJar vestigeJar = vestigeJarEntryConnection.getEntryVestigeJar();
                if (vestigeJar == null) {
                    vestigeJar = vestigeJarEntryConnection.getVestigeJar();
                }
                return new VestigeJarJar(url, vestigeJar, urlString.substring(urlString.indexOf("!/") + 2));
            }
        }
        return null;
    }

    @SuppressWarnings("deprecation")
    @Override
    public JarFile getJarFile(URL url) {
        if (url.toString().startsWith("jar:mvn:")) {
            try {
                URLConnection connection = url.openConnection();
                if (connection instanceof VestigeJarEntryConnection) {
                    VestigeJarEntryConnection vestigeJarEntryConnection = (VestigeJarEntryConnection) connection;
                    VestigeJar vestigeJar = vestigeJarEntryConnection.getEntryVestigeJar();
                    if (vestigeJar == null) {
                        vestigeJar = vestigeJarEntryConnection.getVestigeJar();
                    }
                    return new JarFile(vestigeJar.getFile());
                }
            } catch (IOException e) {
            }
        }
        return null;
    }

    @Override
    public boolean createWebResourceSet(StandardRoot standardRoot, List<WebResourceSet> resourceList, String webAppMount, URL url, String internalPath, boolean classLoaderOnly,
            boolean staticOnly) {
        if (url.toString().startsWith("jar:mvn:")) {
            try {
                URLConnection connection = url.openConnection();
                if (connection instanceof VestigeJarEntryConnection) {
                    VestigeJar entryVestigeJar = ((VestigeJarEntryConnection) connection).getEntryVestigeJar();
                    if (entryVestigeJar == null) {
                        return true;
                    }
                    VestigeJarResourceSet vestigeJarResourceSet = new VestigeJarResourceSet(standardRoot, webAppMount,
                            entryVestigeJar, Collections.<VestigeJar> emptyList(), internalPath);
                    resourceList.add(vestigeJarResourceSet);
                    if (classLoaderOnly) {
                        vestigeJarResourceSet.setClassLoaderOnly(true);
                    }
                    if (staticOnly) {
                        vestigeJarResourceSet.setStaticOnly(true);
                    }
                    return true;
                }
            } catch (IOException e) {
            }
        }
        return false;
    }
    
    public URL getJarEntryURL(URL baseUrl, String entryName) {
        String urlString = baseUrl.toString();
        if (urlString.startsWith("jar:mvn:")) {
            try {
                URLConnection connection = baseUrl.openConnection();
                if (connection instanceof VestigeJarEntryConnection) {
                    VestigeJarEntryConnection vestigeJarEntryConnection = (VestigeJarEntryConnection) connection;
                    VestigeJar vestigeJar = vestigeJarEntryConnection.getEntryVestigeJar();
                    if (vestigeJar == null) {
                        vestigeJar = vestigeJarEntryConnection.getVestigeJar();
                    }
                    
                    final VestigeJar finalVestigeJar = vestigeJar;
                    
                    final String root = urlString.substring(urlString.indexOf("!/") + 2);

                    try {
                        return new URL(null, baseUrl + entryName, new URLStreamHandler() {
                            
                            @Override
                            protected URLConnection openConnection(URL u) throws IOException {
                                return new VestigeJarEntryConnection(u, finalVestigeJar, finalVestigeJar.getEntry(root + entryName));
                            }
                        });
                    } catch (MalformedURLException e) {
                        return null;
                    }
                    
                }
            } catch (IOException e) {
            }
        }
        return null;
    }


    @Override
    public String getJarName(URL url) {
        if (url.toString().startsWith("jar:mvn:")) {
            try {
                URLConnection connection = url.openConnection();
                if (connection instanceof VestigeJarEntryConnection) {
                    VestigeJarEntryConnection vestigeJarEntryConnection = (VestigeJarEntryConnection) connection;
                    String name = vestigeJarEntryConnection.getName();
                    if (name.endsWith("/")) {
                        return vestigeJarEntryConnection.getVestigeJar().getName();
                    }
                    int lastIndexOf = name.lastIndexOf('/');
                    if (lastIndexOf == -1) {
                        return name;
                    }
                    return name.substring(lastIndexOf + 1);
                }
            } catch (IOException e) {
            }
        }
        return null;
    }

    @Override
    public Closeable getCloseable() {
        return vestigeWarThreadLocal.get();
    }
    
    @Override
    public void warUndeployed(Closeable closeable) {
        synchronized (vestigeWars) {
            vestigeWars.remove(closeable);
        }
    }

    @Override
    public void warDeployed() {
        vestigeWarThreadLocal.remove();
    }

    @Override
    public void close() throws IOException {
        synchronized (vestigeWars) {
            for (VestigeWar vestigeWar : vestigeWars) {
                vestigeWar.close();
            }
        }

    }

}
