//
//  ========================================================================
//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.osgi.boot.warurl.internal;

import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.UrlEncoded;
import org.osgi.framework.Constants;

public class WarBundleManifestGenerator
{
    /**
     * missing version in the url and in the manifest
     * use this one.
     */
    private static final String MISSING_VERSION = "0.0.1.unknown";
    private static final String MISSING_MANIFEST_VERSION = "2";

    public static Manifest createBundleManifest(Manifest originalManifest, URL url, JarFile jarFile)
    {
        Manifest res = new Manifest();
        res.getMainAttributes().putAll(
            createBundleManifest(originalManifest.getMainAttributes(),
                url.toString(), jarFile));
        return res;
    }

    private static Attributes createBundleManifest(Attributes originalManifest, String url, JarFile jarFile)
    {
        HashMap<String, String> res = new HashMap<String, String>();
        for (Entry<Object, Object> entries : originalManifest.entrySet())
        {
            res.put(entries.getKey().toString(), String.valueOf(entries.getValue()));
        }
        MultiMap<String> params = parseQueryString(url);
        //follow RFC66 documentation:
        //#1 Bundle-Version
        String version = params.getString(Constants.BUNDLE_VERSION);
        if (version != null)
        {
            res.put(Constants.BUNDLE_VERSION, version);
        }
        else
        {
            String versionInManifest = res.get(Constants.BUNDLE_VERSION);
            if (versionInManifest == null)
            {
                res.put(Constants.BUNDLE_VERSION, MISSING_VERSION);
            }
        }

        //#2 Bundle_ManifestVersion
        String manversion = params.getString(Constants.BUNDLE_MANIFESTVERSION);
        if (manversion != null)
        {
            res.put(Constants.BUNDLE_MANIFESTVERSION, manversion);
        }
        else
        {
            int manv = 2;
            try
            {
                String versionInManifest = res.get(Constants.BUNDLE_MANIFESTVERSION);
                if (versionInManifest != null)
                {
                    manv = Integer.parseInt(versionInManifest.trim());
                }
            }
            catch (NumberFormatException ignored)
            {
            }
            res.put(Constants.BUNDLE_MANIFESTVERSION, String.valueOf(manv < 2 ? 2 : manv));
        }

        //#3 Bundle-SymbolicName
        String symbname = params.getString(Constants.BUNDLE_SYMBOLICNAME);
        if (symbname != null)
        {
            res.put(Constants.BUNDLE_SYMBOLICNAME, symbname);
        }
        else
        {
            symbname = res.get(Constants.BUNDLE_SYMBOLICNAME);
            if (symbname == null)
            {
                //derive the symbolic name from the url.
                int lastSlash = url.lastIndexOf('/');
                int beforeQueryString = url.indexOf(lastSlash, '?');
                if (beforeQueryString == -1)
                {
                    beforeQueryString = url.indexOf(lastSlash, '#');
                    if (beforeQueryString == -1)
                    {
                        beforeQueryString = url.length();
                    }
                }
                symbname = url.substring(lastSlash + 1, beforeQueryString);
                //todo: something better probably.
                res.put(Constants.BUNDLE_SYMBOLICNAME, symbname);
            }
        }

        //#4 Bundle-Classpath
        String extraBundleClasspath = params.getString(Constants.BUNDLE_CLASSPATH);
        String alreadyBundleClasspath = res.get(Constants.BUNDLE_CLASSPATH);
        if (alreadyBundleClasspath == null)
        {
            StringBuilder bundleClasspath = new StringBuilder();
            if (jarFile == null || jarFile.getJarEntry("WEB-INF/classes/") != null)
            {
                bundleClasspath.append("WEB-INF/classes");
            }
            if (jarFile != null)
            {
                List<String> libs = getJarsInWebInfLib(jarFile);
                if (extraBundleClasspath != null)
                {
                    libs.add(extraBundleClasspath);
                }
                for (String lib : libs)
                {
                    if (bundleClasspath.length() != 0)
                    {
                        bundleClasspath.append(",");
                    }
                    bundleClasspath.append(lib);
                }
            }
            alreadyBundleClasspath = bundleClasspath.toString();
        }

        //if there is already a manifest and it specifies the Bundle-Classpath.
        //for now let's trust that one.
        //please note that the draft of the spec implies that we should be parsing the existing
        //header and merge it with the missing stuff so this does not follow the spec yet.

        res.put(Constants.BUNDLE_CLASSPATH,
            alreadyBundleClasspath + (extraBundleClasspath == null ? "" : "," + extraBundleClasspath));

        //#5 Import-Package
        String extraImportPackage = params.getString(Constants.IMPORT_PACKAGE);
        String alreadyImportPackage = res.get(Constants.IMPORT_PACKAGE);
        if (alreadyImportPackage == null)
        {
            //The spec does not specify that the jsp imports are optional
            //kind of nice to have them optional so we can run simple wars in
            //simple environments.
            alreadyImportPackage = "javax.servlet; version=\"2.5\"," +
                "javax.servlet.http;version=\"2.5\"," +
                "javax.el;version=\"1.0\"" +
                "javax.jsp;version=\"2.1\";resolution:=optional," +
                "javax.jsp.tagext;version=\"2.1\";resolution:=optional";
        }
        if (extraImportPackage != null)
        {   //if there is already a manifest and it specifies the Bundle-Classpath.
            //for now let's trust that one.
            //please note that the draft of the spec implies that we should be parsing the existing
            //header and merge it with the missing stuff so this does not follow the spec yet.

            res.put(Constants.IMPORT_PACKAGE,
                (alreadyImportPackage == null ? "" : alreadyImportPackage + ",") +
                    extraImportPackage);
        }

        //#6 Export-Package
        String extraExportPackage = params.getString(Constants.EXPORT_PACKAGE);
        String alreadyExportPackage = res.get(Constants.EXPORT_PACKAGE);
        if (extraExportPackage != null)
        {   //if there is already a manifest and it specifies the Bundle-Classpath.
            //for now let's trust that one.
            //please note that the draft of the spec implies that we should be parsing the existing
            //header and merge it with the missing stuff so this does not follow the spec yet.
            res.put(Constants.EXPORT_PACKAGE,
                (alreadyExportPackage == null ? "" : alreadyExportPackage + ",") +
                    extraImportPackage);
        }

        //#7 Web-ContextPath
        String webContextPath = params.getString("Web-ContextPath");
        if (webContextPath != null)
        {
            res.put("Web-ContextPath", webContextPath);
        }
        else
        {
            webContextPath = res.get("Web-ContextPath");
            if (webContextPath == null)
            {
                //we choose to use the symbolic name as the default context path.
                if (symbname.endsWith(".war"))
                {
                    webContextPath = "/" + symbname.substring(0, symbname.length() - ".war".length());
                }
                else
                {
                    webContextPath = "/" + symbname;
                }
                res.put("Web-ContextPath", webContextPath);
            }
        }

        //#8 Web-JSPExtractLocation
        String jspExtractLocation = params.getString("Web-JSPExtractLocation");
        if (jspExtractLocation != null)
        {
            res.put("Web-JSPExtractLocation", jspExtractLocation);
        }
        else
        {
            //nothing to do.
        }
        Attributes newAttrs = new Attributes();
        for (Entry<String, String> e : res.entrySet())
        {
            newAttrs.putValue(e.getKey(), e.getValue());
        }
        return newAttrs;
    }

    /**
     * @return The key values pairs that are in the query string of this url.
     */
    private static MultiMap<String> parseQueryString(String url)
    {
        MultiMap<String> res = new MultiMap<String>();
        int questionMarkIndex = url.indexOf('?');
        if (questionMarkIndex == -1)
        {
            return res;
        }
        int poundIndex = url.indexOf('#');
        if (poundIndex == -1)
        {
            poundIndex = url.length();
        }
        UrlEncoded.decodeUtf8To(url, questionMarkIndex + 1,
            poundIndex - questionMarkIndex - 1, res);
        return res;
    }

    private static List<String> getJarsInWebInfLib(JarFile jarFile)
    {
        List<String> res = new ArrayList<String>();
        Enumeration<JarEntry> en = jarFile.entries();
        while (en.hasMoreElements())
        {
            JarEntry e = en.nextElement();
            if (e.getName().startsWith("WEB-INF/lib/") && e.getName().endsWith(".jar"))
            {
                res.add(e.getName());
            }
        }
        return res;
    }
}
