/*
 * 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.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Unmarshaller;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.apache.tomcat.vwar.ActivateNamedModules;
import org.apache.tomcat.vwar.AddDependency;
import org.apache.tomcat.vwar.AddExports;
import org.apache.tomcat.vwar.AddOpens;
import org.apache.tomcat.vwar.AddReads;
import org.apache.tomcat.vwar.AdditionalRepository;
import org.apache.tomcat.vwar.Application;
import org.apache.tomcat.vwar.AttachDependencies;
import org.apache.tomcat.vwar.Config;
import org.apache.tomcat.vwar.ExceptIn;
import org.apache.tomcat.vwar.Launcher;
import org.apache.tomcat.vwar.MavenClassType;
import org.apache.tomcat.vwar.MavenConfig;
import org.apache.tomcat.vwar.Mode;
import org.apache.tomcat.vwar.ModifyDependency;
import org.apache.tomcat.vwar.ModifyLoadedDependency;
import org.apache.tomcat.vwar.ModifyScope;
import org.apache.tomcat.vwar.ModulePackageName;
import org.apache.tomcat.vwar.ObjectFactory;
import org.apache.tomcat.vwar.ReplaceDependency;

import fr.gaellalire.vestige.spi.job.DummyJobHelper;
import fr.gaellalire.vestige.spi.resolver.AttachedClassLoader;
import fr.gaellalire.vestige.spi.resolver.PartiallyVerifiedAttachment;
import fr.gaellalire.vestige.spi.resolver.ResolvedClassLoaderConfiguration;
import fr.gaellalire.vestige.spi.resolver.ResolverException;
import fr.gaellalire.vestige.spi.resolver.Scope;
import fr.gaellalire.vestige.spi.resolver.VestigeJar;
import fr.gaellalire.vestige.spi.resolver.maven.CreateClassLoaderConfigurationRequest;
import fr.gaellalire.vestige.spi.resolver.maven.MavenArtifact;
import fr.gaellalire.vestige.spi.resolver.maven.MavenContext;
import fr.gaellalire.vestige.spi.resolver.maven.MavenContextBuilder;
import fr.gaellalire.vestige.spi.resolver.maven.MavenResolvedClassLoaderConfiguration;
import fr.gaellalire.vestige.spi.resolver.maven.ModifyDependencyRequest;
import fr.gaellalire.vestige.spi.resolver.maven.ModifyLoadedDependencyRequest;
import fr.gaellalire.vestige.spi.resolver.maven.ReplaceDependencyRequest;
import fr.gaellalire.vestige.spi.resolver.maven.ResolveMavenArtifactRequest;
import fr.gaellalire.vestige.spi.resolver.maven.ResolveMode;
import fr.gaellalire.vestige.spi.resolver.maven.ResolvedMavenArtifact;
import fr.gaellalire.vestige.spi.resolver.maven.VestigeMavenResolver;

/**
 * @author Gael Lalire
 */
public class VestigeWar implements Closeable {

    private static ThreadLocal<TomcatVestigeLauncher> tomcatVestigeLauncher = new InheritableThreadLocal<>();

    private static ClassLoader appContextClassLoader;

    public static void init(TomcatVestigeLauncher tomcatVestigeLauncher) {
        VestigeWar.tomcatVestigeLauncher.set(tomcatVestigeLauncher);
        appContextClassLoader = Thread.currentThread().getContextClassLoader();
    }

    public static VestigeWar create(File vestigeWarFile, String baseName) {
        TomcatVestigeLauncher tomcatVestigeLauncher = VestigeWar.tomcatVestigeLauncher.get();
        VestigeMavenResolver vestigeMavenResolver = tomcatVestigeLauncher.getVestigeMavenResolver();

        String selfVerificationMetadata = null;
        String sharedDependenciesVerificationMetadata = null;
        String dependenciesVerificationMetadata = null;

        String verificationMetadata = null;
        VestigeJar warVestigeJar;
        List<VestigeJar> vestigeWarDependencies = new ArrayList<VestigeJar>();

        ResolvedClassLoaderConfiguration selfClassLoaderConfiguration = null;
        boolean sharedDependenciesJarExcluded = false;
        MavenResolvedClassLoaderConfiguration sharedDependenciesClassLoaderConfiguration = null;
        ResolvedClassLoaderConfiguration dependenciesClassLoaderConfiguration = null;

        boolean cacheRestored = false;
        File cacheFile;
        String confPath = tomcatVestigeLauncher.getConf().getAbsolutePath();
        String warPath = vestigeWarFile.getAbsolutePath();
        if (warPath.startsWith(confPath)) {
            cacheFile = new File(tomcatVestigeLauncher.getData(), warPath.substring(confPath.length()) + ".cache");
            if (!cacheFile.getParentFile().exists()) {
                cacheFile.getParentFile().mkdirs();
            }
        } else {
            cacheFile = new File(vestigeWarFile.getParentFile(), vestigeWarFile.getName() + ".cache");
        }

        if (cacheFile.exists() && cacheFile.lastModified() > vestigeWarFile.lastModified()) {
            try {
                FileInputStream in = new FileInputStream(cacheFile);
                try {
                    ObjectInputStream objectInputStream = new ObjectInputStream(in);
                    try {
                        selfVerificationMetadata = (String) objectInputStream.readObject();
                        sharedDependenciesVerificationMetadata = (String) objectInputStream.readObject();
                        dependenciesVerificationMetadata = (String) objectInputStream.readObject();
                        selfClassLoaderConfiguration = vestigeMavenResolver.restoreSavedResolvedClassLoaderConfiguration(objectInputStream);
                        if (objectInputStream.readBoolean()) {
                            sharedDependenciesClassLoaderConfiguration = vestigeMavenResolver.restoreSavedResolvedClassLoaderConfiguration(objectInputStream);
                            sharedDependenciesJarExcluded = objectInputStream.readBoolean();
                        }
                        dependenciesClassLoaderConfiguration = vestigeMavenResolver.restoreSavedResolvedClassLoaderConfiguration(objectInputStream);
                        cacheRestored = true;
                    } finally {
                        objectInputStream.close();
                    }
                } finally {
                    in.close();
                }
            } catch (Exception e) {
                // unable to restore, recreate
            }
        }

        boolean attachmentSuccessful = false;
        List<AttachedClassLoader> attachedClassLoaders = new ArrayList<AttachedClassLoader>();
        AttachedClassLoader sharedAttachedClassLoader = null;
        try {
            createClassLoaderConfigurations: while (true) {
                if (!cacheRestored) {
                    Unmarshaller unMarshaller = null;
                    try {
                        Thread currentThread = Thread.currentThread();
                        ClassLoader contextClassLoader = currentThread.getContextClassLoader();
                        currentThread.setContextClassLoader(appContextClassLoader);
                        try {
                            JAXBContext jc = JAXBContext.newInstance(ObjectFactory.class.getPackage().getName());
                            unMarshaller = jc.createUnmarshaller();

                            URL xsdURL = VestigeWar.class.getResource("vwar-1.0.0.xsd");
                            SchemaFactory schemaFactory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
                            Schema schema = schemaFactory.newSchema(xsdURL);
                            unMarshaller.setSchema(schema);
                        } finally {
                            currentThread.setContextClassLoader(contextClassLoader);
                        }
                    } catch (Exception e) {
                        throw new RuntimeException("Unable to initialize settings parser", e);
                    }
                    try {
                        FileInputStream inputStream = new FileInputStream(vestigeWarFile);
                        try {
                            @SuppressWarnings("unchecked")
                            Application value = ((JAXBElement<Application>) unMarshaller.unmarshal(inputStream)).getValue();

                            MavenContextBuilder mavenContextBuilder = vestigeMavenResolver.createMavenContextBuilder();
                            Launcher launcher = value.getLauncher();
                            MavenClassType mavenResolver = launcher.getMavenResolver();
                            Config configurations = value.getConfigurations();
                            if (configurations != null) {
                                MavenConfig mavenConfig = configurations.getMavenConfig();
                                if (mavenConfig != null) {
                                    List<Object> modifyDependencyOrReplaceDependencyOrAdditionalRepository = mavenConfig
                                            .getModifyDependencyOrReplaceDependencyOrAdditionalRepository();
                                    for (Object object : modifyDependencyOrReplaceDependencyOrAdditionalRepository) {
                                        if (object instanceof AdditionalRepository) {
                                            AdditionalRepository additionalRepository = (AdditionalRepository) object;
                                            mavenContextBuilder.addAdditionalRepository(additionalRepository.getId(), additionalRepository.getLayout(),
                                                    additionalRepository.getUrl());
                                        } else if (object instanceof ModifyDependency) {
                                            ModifyDependency modifyDependency = (ModifyDependency) object;
                                            ModifyDependencyRequest addModifyDependency = mavenContextBuilder.addModifyDependency(modifyDependency.getGroupId(),
                                                    modifyDependency.getArtifactId());
                                            AddDependency patch = modifyDependency.getPatch();
                                            if (patch != null) {
                                                addModifyDependency.setPatch(patch.getGroupId(), patch.getArtifactId(), patch.getVersion());
                                            }
                                            for (AddDependency addDependency : modifyDependency.getAddDependency()) {
                                                addModifyDependency.addDependency(addDependency.getGroupId(), addDependency.getArtifactId(), addDependency.getVersion(), addDependency.getExtension(), addDependency.getClassifier());
                                            }
                                            addModifyDependency.execute();
                                        } else if (object instanceof ReplaceDependency) {
                                            ReplaceDependency replaceDependency = (ReplaceDependency) object;
                                            ReplaceDependencyRequest addReplaceDependency = mavenContextBuilder.addReplaceDependency(replaceDependency.getGroupId(),
                                                    replaceDependency.getArtifactId());
                                            for (AddDependency addDependency : replaceDependency.getAddDependency()) {
                                                addReplaceDependency.addDependency(addDependency.getGroupId(), addDependency.getArtifactId(), addDependency.getVersion(), addDependency.getExtension(), addDependency.getClassifier());
                                            }
                                            for (ExceptIn exceptIn : replaceDependency.getExceptIn()) {
                                                addReplaceDependency.addExcept(exceptIn.getGroupId(), exceptIn.getArtifactId());
                                            }
                                            addReplaceDependency.execute();
                                        }
                                    }
                                }
                            }

                            if (launcher.getVerificationMetadata() != null) {
                                verificationMetadata = launcher.getVerificationMetadata().getText();
                            }

                            AttachDependencies attachDependencies = mavenResolver.getAttachDependencies();

                            MavenContext build = mavenContextBuilder.build();
                            ResolveMavenArtifactRequest request = build.resolve(mavenResolver.getGroupId(), mavenResolver.getArtifactId(), mavenResolver.getVersion());
                            request.setExtension("war");
                            String classifier = mavenResolver.getClassifier();
                            if (classifier != null) {
                                request.setClassifier(classifier);
                            }

                            ResolvedMavenArtifact resolvedMavenArtifact;
                            try {
                                resolvedMavenArtifact = request.execute(DummyJobHelper.INSTANCE);
                            } catch (ResolverException e) {
                                throw new RuntimeException("Unable to fetch war", e);
                            }
                            try {
                                CreateClassLoaderConfigurationRequest createClassLoaderConfigurationRequest = resolvedMavenArtifact
                                        .createClassLoaderConfiguration("webapp-" + baseName + "-war", ResolveMode.FIXED_DEPENDENCIES, Scope.PLATFORM);
                                createClassLoaderConfigurationRequest.setDependenciesExcluded(true);
                                selfClassLoaderConfiguration = createClassLoaderConfigurationRequest.execute();

                                if (attachDependencies != null) {
                                    ResolveMode resolveMode;
                                    Mode mode = attachDependencies.getMode();
                                    switch (mode) {
                                    case CLASSPATH:
                                        resolveMode = ResolveMode.CLASSPATH;
                                        break;
                                    case FIXED_DEPENDENCIES:
                                    default:
                                        resolveMode = ResolveMode.FIXED_DEPENDENCIES;
                                        break;
                                    }

                                    Scope vestigeScope;
                                    org.apache.tomcat.vwar.Scope scope = attachDependencies.getScope();
                                    switch (scope) {
                                    case ATTACHMENT:
                                        vestigeScope = Scope.ATTACHMENT;
                                        break;
                                    case PLATFORM:
                                    default:
                                        vestigeScope = Scope.PLATFORM;
                                        break;
                                    }

                                    createClassLoaderConfigurationRequest = resolvedMavenArtifact.createClassLoaderConfiguration("webapp-" + baseName + "-sharedDependencies",
                                            resolveMode, vestigeScope);
                                    createClassLoaderConfigurationRequest.setSelfExcluded(true);

                                    for (ModifyScope modifyScope : attachDependencies.getModifyScope()) {
                                        Scope modifiedVestigeScope;
                                        switch (modifyScope.getScope()) {
                                        case ATTACHMENT:
                                            modifiedVestigeScope = Scope.ATTACHMENT;
                                            break;
                                        case PLATFORM:
                                        default:
                                            modifiedVestigeScope = Scope.PLATFORM;
                                            break;
                                        }
                                        createClassLoaderConfigurationRequest.addModifyScope(modifyScope.getGroupId(), modifyScope.getArtifactId(), modifiedVestigeScope);
                                    }

                                    for (ModifyLoadedDependency modifyLoadedDependency : attachDependencies.getModifyLoadedDependency()) {
                                        ModifyLoadedDependencyRequest addModifyLoadedDependency = createClassLoaderConfigurationRequest
                                                .addModifyLoadedDependency(modifyLoadedDependency.getGroupId(), modifyLoadedDependency.getArtifactId());
                                        if (modifyLoadedDependency.getAddBeforeParent() != null) {
                                            addModifyLoadedDependency.setBeforeParent(true);
                                        }
                                        for (ModulePackageName addExport : modifyLoadedDependency.getAddExports()) {
                                            addModifyLoadedDependency.addExports(addExport.getModule(), addExport.getPackage());
                                        }
                                        for (ModulePackageName addOpens : modifyLoadedDependency.getAddOpens()) {
                                            addModifyLoadedDependency.addOpens(addOpens.getModule(), addOpens.getPackage());
                                        }
                                    }

                                    ActivateNamedModules activateNamedModules = attachDependencies.getActivateNamedModules();
                                    if (activateNamedModules != null) {
                                        createClassLoaderConfigurationRequest.setNamedModuleActivated(true);
                                        for (AddExports addExport : activateNamedModules.getAddExports()) {
                                            createClassLoaderConfigurationRequest.addExports(addExport.getSource(), addExport.getPn(), addExport.getTarget());
                                        }
                                        for (AddOpens addOpen : activateNamedModules.getAddOpens()) {
                                            createClassLoaderConfigurationRequest.addOpens(addOpen.getSource(), addOpen.getPn(), addOpen.getTarget());
                                        }
                                        for (AddReads addRead : activateNamedModules.getAddReads()) {
                                            createClassLoaderConfigurationRequest.addReads(addRead.getSource(), addRead.getTarget());
                                        }
                                    }

                                    List<ExceptIn> excludeWithParents = attachDependencies.getExcludeWithParents();
                                    for (ExceptIn exceptIn : excludeWithParents) {
                                        createClassLoaderConfigurationRequest.addExcludeWithParents(exceptIn.getGroupId(), exceptIn.getArtifactId());
                                    }
                                    sharedDependenciesClassLoaderConfiguration = createClassLoaderConfigurationRequest.execute();
                                    sharedDependenciesJarExcluded = attachDependencies.isExcludeJarsFromTomcatLoader();
                                } else {
                                    sharedDependenciesClassLoaderConfiguration = null;
                                }

                                createClassLoaderConfigurationRequest = resolvedMavenArtifact.createClassLoaderConfiguration("webapp-" + baseName + "-localDependencies",
                                        ResolveMode.FIXED_DEPENDENCIES, Scope.PLATFORM);
                                createClassLoaderConfigurationRequest.setSelfExcluded(true);
                                if (sharedDependenciesClassLoaderConfiguration != null) {
                                    // exclude all artifact from shared
                                    Enumeration<MavenArtifact> usedArtifacts = sharedDependenciesClassLoaderConfiguration.getUsedArtifacts();
                                    while (usedArtifacts.hasMoreElements()) {
                                        MavenArtifact usedArtifact = usedArtifacts.nextElement();
                                        createClassLoaderConfigurationRequest.addExclude(usedArtifact.getGroupId(), usedArtifact.getArtifactId());
                                    }
                                }
                                dependenciesClassLoaderConfiguration = createClassLoaderConfigurationRequest.execute();
                            } catch (ResolverException e) {
                                throw new RuntimeException("Unable to fetch war", e);
                            }

                        } finally {
                            inputStream.close();
                        }
                    } catch (Exception e) {
                        throw new RuntimeException("Unable to read vwar file", e);
                    }
                }
                try {
                    AttachedClassLoader attachedClassLoader;
                    PartiallyVerifiedAttachment partiallyVerifiedAttachment = null;
                    if (verificationMetadata == null && selfVerificationMetadata == null) {
                        attachedClassLoader = selfClassLoaderConfiguration.attach();
                    } else if (selfVerificationMetadata != null) {
                        attachedClassLoader = selfClassLoaderConfiguration.verifiedAttach(selfVerificationMetadata);
                    } else {
                        partiallyVerifiedAttachment = selfClassLoaderConfiguration.partiallyVerifiedAttach(verificationMetadata);
                        selfVerificationMetadata = partiallyVerifiedAttachment.getUsedVerificationMetadata();
                        attachedClassLoader = partiallyVerifiedAttachment.getAttachedClassLoader();
                    }
                    attachedClassLoaders.add(attachedClassLoader);
                    warVestigeJar = attachedClassLoader.getAttachableClassLoader().getVestigeJarEnumeration().nextElement();

                    if (sharedDependenciesClassLoaderConfiguration != null) {
                        if (verificationMetadata == null && sharedDependenciesVerificationMetadata == null) {
                            sharedAttachedClassLoader = sharedDependenciesClassLoaderConfiguration.attach();
                        } else if (sharedDependenciesVerificationMetadata != null) {
                            sharedAttachedClassLoader = sharedDependenciesClassLoaderConfiguration.verifiedAttach(sharedDependenciesVerificationMetadata);
                        } else {
                            partiallyVerifiedAttachment = sharedDependenciesClassLoaderConfiguration
                                    .partiallyVerifiedAttach(partiallyVerifiedAttachment.getRemainingVerificationMetadata());
                            sharedDependenciesVerificationMetadata = partiallyVerifiedAttachment.getUsedVerificationMetadata();
                            dependenciesVerificationMetadata = partiallyVerifiedAttachment.getRemainingVerificationMetadata();
                            sharedAttachedClassLoader = partiallyVerifiedAttachment.getAttachedClassLoader();
                        }
                        if (!sharedDependenciesJarExcluded) {
                            vestigeWarDependencies.addAll(Collections.list(sharedAttachedClassLoader.getAttachableClassLoader().getVestigeJarEnumeration()));
                        }
                    } else if (partiallyVerifiedAttachment != null) {
                        dependenciesVerificationMetadata = partiallyVerifiedAttachment.getRemainingVerificationMetadata();
                    }

                    if (dependenciesVerificationMetadata == null) {
                        attachedClassLoader = dependenciesClassLoaderConfiguration.attach();
                    } else {
                        attachedClassLoader = dependenciesClassLoaderConfiguration.verifiedAttach(dependenciesVerificationMetadata);
                    }
                    attachedClassLoaders.add(attachedClassLoader);
                    vestigeWarDependencies.addAll(Collections.list(attachedClassLoader.getAttachableClassLoader().getVestigeJarEnumeration()));
                } catch (Exception e) {
                    if (cacheRestored) {
                        // try without cache
                        for (AttachedClassLoader attachedClassLoader : attachedClassLoaders) {
                            attachedClassLoader.detach();
                        }
                        attachedClassLoaders.clear();
                        if (sharedAttachedClassLoader != null) {
                            sharedAttachedClassLoader.detach();
                        }
                        cacheRestored = false;
                        sharedAttachedClassLoader = null;
                        selfClassLoaderConfiguration = null;
                        sharedDependenciesJarExcluded = false;
                        sharedDependenciesClassLoaderConfiguration = null;
                        dependenciesClassLoaderConfiguration = null;
                        continue createClassLoaderConfigurations;
                    }
                    throw new RuntimeException("Unable to attach vwar classloaders", e);
                }
                break createClassLoaderConfigurations;
            }
            if (!cacheRestored) {
                try {
                    ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(cacheFile));
                    try {
                        os.writeObject(selfVerificationMetadata);
                        os.writeObject(sharedDependenciesVerificationMetadata);
                        os.writeObject(dependenciesVerificationMetadata);
                        selfClassLoaderConfiguration.save(os);
                        if (sharedDependenciesClassLoaderConfiguration != null) {
                            os.writeBoolean(true);
                            sharedDependenciesClassLoaderConfiguration.save(os);
                            os.writeBoolean(sharedDependenciesJarExcluded);
                        } else {
                            os.writeBoolean(false);
                        }
                        dependenciesClassLoaderConfiguration.save(os);
                    } finally {
                        os.close();
                    }
                } catch (Exception e) {
                    throw new RuntimeException("Unable to save cache", e);
                }
            }

            VestigeWar vestigeWar = new VestigeWar(warVestigeJar, vestigeWarDependencies, sharedAttachedClassLoader, attachedClassLoaders);
            attachmentSuccessful = true;
            return vestigeWar;
        } finally {
            if (!attachmentSuccessful) {
                for (AttachedClassLoader attachedClassLoader : attachedClassLoaders) {
                    attachedClassLoader.detach();
                }
                attachedClassLoaders.clear();
                if (sharedAttachedClassLoader != null) {
                    sharedAttachedClassLoader.detach();
                }
                sharedAttachedClassLoader = null;
            }
        }
    }

    private VestigeJar vestigeWar;

    private List<? extends VestigeJar> vestigeWarDependencies;

    private AttachedClassLoader sharedAttachedClassLoader;

    private List<AttachedClassLoader> attachedClassLoaders;

    public VestigeWar(VestigeJar vestigeWar, List<? extends VestigeJar> vestigeWarDependencies, AttachedClassLoader sharedAttachedClassLoader,
            List<AttachedClassLoader> attachedClassLoaders) {
        this.vestigeWar = vestigeWar;
        this.vestigeWarDependencies = vestigeWarDependencies;
        this.sharedAttachedClassLoader = sharedAttachedClassLoader;
        this.attachedClassLoaders = attachedClassLoaders;
    }

    public VestigeJar getVestigeWar() {
        return vestigeWar;
    }

    public List<? extends VestigeJar> getVestigeWarDependencies() {
        return vestigeWarDependencies;
    }

    public AttachedClassLoader getSharedAttachedClassLoader() {
        return sharedAttachedClassLoader;
    }

    public List<AttachedClassLoader> getAttachedClassLoaders() {
        return attachedClassLoaders;
    }

    @Override
    public void close() throws IOException {
        if (sharedAttachedClassLoader != null) {
            sharedAttachedClassLoader.detach();
        }
        for (AttachedClassLoader attachedClassLoader : attachedClassLoaders) {
            attachedClassLoader.detach();
        }
    }

}
