/*
 * 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.deltaspike.test.utils;


import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.*;
import java.util.logging.Logger;


/**
 * Lots of neat little helpers to more easily create JavaArchives from marker files on the classpath.
 * This should finally get moved to ShrinkWrap core!
 *
 * TODO This class should get moved to ShrinkWrap itself!
 */
public class ShrinkWrapArchiveUtil
{
    private static final Logger LOG = Logger.getLogger(ShrinkWrapArchiveUtil.class.getName());

    private ShrinkWrapArchiveUtil()
    {
        // private ct for utility class
    }

    /**
     * Resolve all markerFiles from the current ClassPath and package the root nodes
     * into a JavaArchive.
     *
     * @param classLoader            to use
     * @param markerFile             finding this marker file will trigger creating the JavaArchive.
     * @param includeIfPackageExists if not null, we will only create JavaArchives if the given package exists
     * @param excludeIfPackageExists if not null, we will <b>not</b> create JavaArchives if the given package exists.
     *                               This has a higher precedence than includeIfPackageExists.
     */
    public static JavaArchive[] getArchives(ClassLoader classLoader,
                                            String markerFile,
                                            String[] includeIfPackageExists,
                                            String[] excludeIfPackageExists,
                                            String archiveName)
    {
        if (classLoader == null)
        {
            classLoader = ShrinkWrapArchiveUtil.class.getClassLoader();
        }

        try
        {
            Enumeration<URL> foundFiles = classLoader.getResources(markerFile);

            List<JavaArchive> archives = new ArrayList<JavaArchive>();
            int numArchives = 0;

            while (foundFiles.hasMoreElements())
            {
                URL foundFile = foundFiles.nextElement();
                LOG.fine("Evaluating Java ClassPath URL " + foundFile.toExternalForm());
                String suffix = (numArchives == 0) ? "" : Integer.toString(numArchives);

                JavaArchive archive
                    = createArchive(foundFile, markerFile, includeIfPackageExists, excludeIfPackageExists, 
                        archiveName + suffix);
                if (archive != null)
                {
                    LOG.info("Test " + getTestName()
                            + " Adding Java ClassPath URL as JavaArchive " + foundFile.toExternalForm());
                    archives.add(archive);
                    numArchives++;
                }
            }

            return archives.toArray(new JavaArchive[archives.size()]);
        }
        catch (IOException ioe)
        {
            throw new RuntimeException(ioe);
        }

    }

    private static JavaArchive createArchive(URL foundFile, String markerFile,
                                             String[] includeIfPackageExists,
                                             String[] excludeIfPackageExists,
                                             String archiveName)
        throws IOException
    {
        String urlString = foundFile.toString();
        int idx = urlString.lastIndexOf(markerFile);
        urlString = urlString.substring(0, idx);

        String jarUrlPath = isJarUrl(urlString);
        if (jarUrlPath != null)
        {
            JavaArchive foundJar = ShrinkWrap.createFromZipFile(JavaArchive.class, new File(URI.create(jarUrlPath)));

            if (excludeIfPackageExists != null)
            {
                for (String excludePackage : excludeIfPackageExists)
                {
                    if (foundJar.contains(excludePackage.replaceAll("\\.", "\\/")))
                    {
                        return null;
                    }
                }
            }
            if (includeIfPackageExists != null)
            {
                for (String includePackage : includeIfPackageExists)
                {
                    if (foundJar.contains(includePackage.replaceAll("\\.", "\\/")))
                    {
                        return foundJar;
                    }
                }
            }
            return null; // couldn't find any jar
        }
        else
        {
            File f = new File( (new URL(ensureCorrectUrlFormat(urlString))).getFile() );
            if (!f.exists())
            {
                // try a fallback if the URL contains %20 -> spaces
                if (urlString.contains("%20"))
                {
                    urlString = urlString.replaceAll("%20", " ");
                    f = new File( (new URL(ensureCorrectUrlFormat(urlString))).getFile() );
                }

            }

            return addFileArchive(f, includeIfPackageExists, excludeIfPackageExists, archiveName);
        }
    }

    private static JavaArchive addFileArchive(File archiveBasePath,
                                              String[] includeIfPackageExists,
                                              String[] excludeIfPackageExists,
                                              String archiveName)
        throws IOException
    {
        if (!archiveBasePath.exists())
        {
            return null;
        }

        if (archiveName == null)
        {
            archiveName = UUID.randomUUID().toString();
        }

        JavaArchive ret = null;
        JavaArchive javaArchive = ShrinkWrap.create(JavaArchive.class, archiveName + ".jar");

        if (includeIfPackageExists == null)
        {
            // no include rule, thus add it immediately
            ret = javaArchive;
        }

        int basePathLength = archiveBasePath.getAbsolutePath().length() + 1;

        for (File archiveEntry : collectArchiveEntries(archiveBasePath) )
        {
            String entryName = archiveEntry.getAbsolutePath().substring(basePathLength);

            // exclude rule
            if (excludeIfPackageExists(entryName, excludeIfPackageExists))
            {
                continue;
            }

            // include rule
            if (ret == null && includeIfPackageExists(entryName, includeIfPackageExists))
            {
                ret = javaArchive;
            }

            if (entryName.endsWith(".class"))
            {
                String className
                    = pathToClassName(entryName.substring(0, entryName.length() - (".class".length())));

                try
                {
                    javaArchive.addClass(className);
                }
                catch (Throwable t)
                {
                    LOG.info("Ignoring class " + className + " due to " + t.getMessage());
                }
            }
            else
            {
                javaArchive.addAsResource(archiveEntry, entryName.replace('\\', '/'));
            }
        }

        return ret;
    }

    private static List<File> collectArchiveEntries(File archiveBasePath)
    {
        if (archiveBasePath.isDirectory())
        {
            List<File> archiveEntries = new ArrayList<File>();
            File[] files = archiveBasePath.listFiles();

            for (File file : files)
            {
                if (file.isDirectory())
                {
                    archiveEntries.addAll(collectArchiveEntries(file));
                }
                else
                {
                    archiveEntries.add(file);
                }
            }

            return archiveEntries;
        }

        return Collections.emptyList();
    }


    private static boolean excludeIfPackageExists(String jarEntryName, String[] excludeOnPackages)
    {
        if (excludeOnPackages != null)
        {
            String packageName = pathToClassName(jarEntryName);

            for (String excludeOnPackage : excludeOnPackages)
            {
                if (packageName.startsWith(excludeOnPackage))
                {
                    return true;
                }
            }
        }

        return false;
    }

    private static boolean includeIfPackageExists(String jarEntryName, String[] includeOnPackages)
    {
        if (includeOnPackages == null )
        {
            return true;
        }

        String packageName = pathToClassName(jarEntryName);

        for (String includeOnPackage : includeOnPackages)
        {
            if (packageName.startsWith(includeOnPackage))
            {
                return true;
            }
        }

        return false;
    }

    /**
     * check if the given url path is a Jar
     * @param urlPath to check
     */
    private static String isJarUrl(String urlPath)
    {
        // common prefixes of the url are: jar: (tomcat), zip: (weblogic) and wsjar: (websphere)
        final int jarColon = urlPath.indexOf(':');
        if (urlPath.endsWith("!/") && jarColon > 0)
        {
            urlPath = urlPath.substring(jarColon + 1, urlPath.length() - 2);
            return urlPath;
        }

        return null;
    }

    private static String ensureCorrectUrlFormat(String url)
    {
        //fix for wls
        if (!url.startsWith("file:/"))
        {
            url = "file:/" + url;
        }
        return url;
    }

    private static String pathToClassName(String pathName)
    {
        return pathName.replace('/', '.').replace('\\', '.');   // replace unix and windows separators
    }


    public static String getTestName()
    {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
        String testName = "unknown";
        for (StackTraceElement ste : stackTraceElements)
        {
            if (ste.getClassName().contains("Test"))
            {
                testName = ste.getClassName();
                break;
            }
        }

        return testName;
    }
}
