/*
 *  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
 *
 *      https://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.ivy.osgi.core;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Manifest;

import org.apache.ivy.Ivy;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.Configuration;
import org.apache.ivy.core.module.descriptor.DefaultArtifact;
import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor;
import org.apache.ivy.core.module.descriptor.DefaultExcludeRule;
import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
import org.apache.ivy.core.module.descriptor.ExtraInfoHolder;
import org.apache.ivy.core.module.id.ArtifactId;
import org.apache.ivy.core.module.id.ModuleId;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.osgi.util.Version;
import org.apache.ivy.osgi.util.VersionRange;
import org.apache.ivy.plugins.matcher.ExactOrRegexpPatternMatcher;
import org.apache.ivy.plugins.matcher.PatternMatcher;
import org.apache.ivy.plugins.parser.ModuleDescriptorParser;

import static org.apache.ivy.core.module.descriptor.Configuration.Visibility.PUBLIC;

public class BundleInfoAdapter {

    public static final String CONF_NAME_DEFAULT = "default";

    public static final Configuration CONF_DEFAULT = new Configuration(CONF_NAME_DEFAULT);

    public static final String CONF_NAME_OPTIONAL = "optional";

    public static final Configuration CONF_OPTIONAL = new Configuration(CONF_NAME_OPTIONAL,
            PUBLIC, "Optional dependencies", new String[] {CONF_NAME_DEFAULT}, true,
            null);

    public static final String CONF_NAME_TRANSITIVE_OPTIONAL = "transitive-optional";

    public static final Configuration CONF_TRANSITIVE_OPTIONAL = new Configuration(
            CONF_NAME_TRANSITIVE_OPTIONAL, PUBLIC, "Optional dependencies",
            new String[] {CONF_NAME_OPTIONAL}, true, null);

    public static final String CONF_USE_PREFIX = "use_";

    public static final String EXTRA_INFO_EXPORT_PREFIX = "_osgi_export_";

    public static DefaultModuleDescriptor toModuleDescriptor(ModuleDescriptorParser parser,
            URI baseUri, BundleInfo bundle, ExecutionEnvironmentProfileProvider profileProvider) {
        return toModuleDescriptor(parser, baseUri, bundle, null, profileProvider);
    }

    /**
     * @param parser ModuleDescriptorParser
     * @param baseUri
     *            uri to help build the absolute url if the bundle info has a relative uri.
     * @param bundle BundleInfo
     * @param manifest Manifest
     * @param profileProvider ExecutionEnvironmentProfileProvider
     * @return DefaultModuleDescriptor ditto
     * @throws ProfileNotFoundException if descriptor is not found
     */
    public static DefaultModuleDescriptor toModuleDescriptor(ModuleDescriptorParser parser,
            URI baseUri, BundleInfo bundle, Manifest manifest,
            ExecutionEnvironmentProfileProvider profileProvider) throws ProfileNotFoundException {
        DefaultModuleDescriptor md = new DefaultModuleDescriptor(parser, null);
        md.addExtraAttributeNamespace("o", Ivy.getIvyHomeURL() + "osgi");
        ModuleRevisionId mrid = asMrid(BundleInfo.BUNDLE_TYPE, bundle.getSymbolicName(),
            bundle.getVersion());
        md.setResolvedPublicationDate(new Date());
        md.setModuleRevisionId(mrid);

        md.addConfiguration(CONF_DEFAULT);
        md.addConfiguration(CONF_OPTIONAL);
        md.addConfiguration(CONF_TRANSITIVE_OPTIONAL);

        Set<String> exportedPkgNames = new HashSet<>(bundle.getExports().size());
        for (ExportPackage exportPackage : bundle.getExports()) {
            md.getExtraInfos().add(
                new ExtraInfoHolder(EXTRA_INFO_EXPORT_PREFIX + exportPackage.getName(),
                        exportPackage.getVersion().toString()));
            exportedPkgNames.add(exportPackage.getName());
            String[] confDependencies = new String[exportPackage.getUses().size() + 1];
            int i = 0;
            for (String use : exportPackage.getUses()) {
                confDependencies[i++] = CONF_USE_PREFIX + use;
            }
            confDependencies[i] = CONF_NAME_DEFAULT;
            md.addConfiguration(new Configuration(CONF_USE_PREFIX + exportPackage.getName(),
                    PUBLIC, "Exported package " + exportPackage.getName(),
                    confDependencies, true, null));
        }

        requirementAsDependency(md, bundle, exportedPkgNames);

        if (baseUri != null) {
            for (BundleArtifact bundleArtifact : bundle.getArtifacts()) {
                String type = "jar";
                String ext = "jar";
                String packaging = null;
                if (bundle.hasInnerClasspath() && !bundleArtifact.isSource()) {
                    packaging = "bundle";
                }
                if ("packed".equals(bundleArtifact.getFormat())) {
                    ext = "jar.pack.gz";
                    if (packaging != null) {
                        packaging += ",pack200";
                    } else {
                        packaging = "pack200";
                    }
                }
                if (bundleArtifact.isSource()) {
                    type = "source";
                }
                URI uri = bundleArtifact.getUri();
                if (uri != null) {
                    DefaultArtifact artifact = buildArtifact(mrid, baseUri, uri, type, ext,
                        packaging);
                    md.addArtifact(CONF_NAME_DEFAULT, artifact);
                }
            }
        }

        if (profileProvider != null) {
            for (String env : bundle.getExecutionEnvironments()) {
                ExecutionEnvironmentProfile profile = profileProvider.getProfile(env);
                if (profile == null) {
                    throw new ProfileNotFoundException("Execution environment profile " + env
                            + " not found");
                }
                for (String pkg : profile.getPkgNames()) {
                    ArtifactId id = new ArtifactId(ModuleId.newInstance(BundleInfo.PACKAGE_TYPE,
                        pkg), PatternMatcher.ANY_EXPRESSION, PatternMatcher.ANY_EXPRESSION,
                            PatternMatcher.ANY_EXPRESSION);
                    DefaultExcludeRule rule = new DefaultExcludeRule(id,
                            ExactOrRegexpPatternMatcher.INSTANCE, null);
                    for (String conf : md.getConfigurationsNames()) {
                        rule.addConfiguration(conf);
                    }
                    md.addExcludeRule(rule);
                }
            }
        }

        if (manifest != null) {
            for (Map.Entry<Object, Object> entries : manifest.getMainAttributes().entrySet()) {
                md.addExtraInfo(new ExtraInfoHolder(entries.getKey().toString(), entries.getValue()
                        .toString()));
            }
        }

        return md;
    }

    public static DefaultArtifact buildArtifact(ModuleRevisionId mrid, URI baseUri, URI uri,
            String type, String ext, String packaging) {
        DefaultArtifact artifact;
        if ("ivy".equals(uri.getScheme())) {
            artifact = decodeIvyURI(uri);
        } else {
            if (!uri.isAbsolute()) {
                uri = baseUri.resolve(uri);
            }
            Map<String, String> extraAtt = new HashMap<>();
            if (packaging != null) {
                extraAtt.put("packaging", packaging);
            }
            try {
                artifact = new DefaultArtifact(mrid, null, mrid.getName(), type, ext, new URL(
                        uri.toString()), extraAtt);
            } catch (MalformedURLException e) {
                throw new RuntimeException("Unable to make the uri into the url", e);
            }
        }
        return artifact;
    }

    public static List<String> getConfigurations(BundleInfo bundle) {
        List<String> confs = new ArrayList<>();
        confs.add(CONF_NAME_DEFAULT);
        confs.add(CONF_NAME_OPTIONAL);
        confs.add(CONF_NAME_TRANSITIVE_OPTIONAL);

        for (ExportPackage exportPackage : bundle.getExports()) {
            confs.add(CONF_USE_PREFIX + exportPackage.getName());
        }

        return confs;
    }

    public static URI buildIvyURI(Artifact artifact) {
        ModuleRevisionId mrid = artifact.getModuleRevisionId();
        return asIvyURI(mrid.getOrganisation(), mrid.getName(), mrid.getBranch(),
            mrid.getRevision(), artifact.getType(), artifact.getName(), artifact.getExt());
    }

    private static URI asIvyURI(String org, String name, String branch, String rev, String type,
            String art, String ext) {
        StringBuilder builder = new StringBuilder();
        builder.append("ivy:///").append(org).append('/').append(name).append('?');
        if (branch != null) {
            builder.append("branch=").append(branch);
        }
        if (rev != null) {
            builder.append("&rev=").append(rev);
        }
        if (type != null) {
            builder.append("&type=").append(type);
        }
        if (art != null) {
            builder.append("&art=").append(art);
        }
        if (ext != null) {
            builder.append("&ext=").append(ext);
        }
        try {
            return new URI(builder.toString());
        } catch (URISyntaxException e) {
            throw new RuntimeException("ill-formed ivy url", e);
        }
    }

    private static DefaultArtifact decodeIvyURI(final URI uri) {
        String org = null;
        String name = null;
        String branch = null;
        String rev = null;
        String art = null;
        String type = null;
        String ext = null;

        String path = uri.getPath();
        if (!path.startsWith("/")) {
            throw new IllegalArgumentException(
                    "An ivy url should be of the form ivy:///org/module but was : " + uri);
        }
        int i = path.indexOf('/', 1);
        if (i < 0) {
            throw new IllegalArgumentException("Expecting an organisation in the ivy url: " + uri);
        }
        org = path.substring(1, i);
        name = path.substring(i + 1);

        for (String parameter : uri.getQuery().split("&")) {
            if (parameter.length() == 0) {
                continue;
            }
            String[] nameAndValue = parameter.split("=");
            if (nameAndValue.length != 2) {
                throw new IllegalArgumentException("Malformed query string in the ivy url: " + uri);
            } else if (nameAndValue[0].equals("branch")) {
                branch = nameAndValue[1];
            } else if (nameAndValue[0].equals("rev")) {
                rev = nameAndValue[1];
            } else if (nameAndValue[0].equals("art")) {
                art = nameAndValue[1];
            } else if (nameAndValue[0].equals("type")) {
                type = nameAndValue[1];
            } else if (nameAndValue[0].equals("ext")) {
                ext = nameAndValue[1];
            } else {
                throw new IllegalArgumentException("Unrecognized parameter '" + nameAndValue[0]
                        + " in the query string of the ivy url: " + uri);
            }
        }

        ModuleRevisionId amrid = ModuleRevisionId.newInstance(org, name, branch, rev);
        return new DefaultArtifact(amrid, null, art, type, ext);
    }

    private static void requirementAsDependency(DefaultModuleDescriptor md, BundleInfo bundleInfo,
            Set<String> exportedPkgNames) {
        for (BundleRequirement requirement : bundleInfo.getRequirements()) {
            String type = requirement.getType();
            String name = requirement.getName();

            if (BundleInfo.PACKAGE_TYPE.equals(type) && exportedPkgNames.contains(name)) {
                // don't declare package exported by the current bundle
                continue;
            }

            if (BundleInfo.EXECUTION_ENVIRONMENT_TYPE.equals(type)) {
                // execution environment are handled elsewhere
                continue;
            }

            ModuleRevisionId ddmrid = asMrid(type, name, requirement.getVersion());
            DefaultDependencyDescriptor dd = new DefaultDependencyDescriptor(ddmrid, false);

            String conf = CONF_NAME_DEFAULT;
            if (BundleInfo.PACKAGE_TYPE.equals(type)) {
                // declare the configuration for the package
                conf = CONF_USE_PREFIX + name;
                md.addConfiguration(new Configuration(CONF_USE_PREFIX + name, PUBLIC,
                        "Exported package " + name, new String[] {CONF_NAME_DEFAULT}, true, null));
                dd.addDependencyConfiguration(conf, conf);
            }

            if ("optional".equals(requirement.getResolution())) {
                dd.addDependencyConfiguration(CONF_NAME_OPTIONAL, conf);
                dd.addDependencyConfiguration(CONF_NAME_TRANSITIVE_OPTIONAL,
                    CONF_NAME_TRANSITIVE_OPTIONAL);
            } else {
                dd.addDependencyConfiguration(CONF_NAME_DEFAULT, conf);
            }

            md.addDependency(dd);
        }

    }

    public static ModuleRevisionId asMrid(String type, String name, Version v) {
        return ModuleRevisionId.newInstance(type, name, v == null ? null : v.toString());
    }

    public static ModuleRevisionId asMrid(String type, String name, VersionRange v) {
        String revision;
        if (v == null) {
            revision = "[0,)";
        } else {
            revision = v.toIvyRevision();
        }
        return ModuleRevisionId.newInstance(type, name, revision);
    }

    @SuppressWarnings("serial")
    public static class ProfileNotFoundException extends RuntimeException {

        public ProfileNotFoundException(String msg) {
            super(msg);
        }

    }
}
