/*******************************************************************************
 * Copyright (c) 2008, 2014 Sonatype Inc. and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Sonatype Inc. - initial API and implementation
 *******************************************************************************/
package org.eclipse.tycho.core.osgitools;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.maven.execution.MavenSession;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.eclipse.osgi.container.namespaces.EclipsePlatformNamespace;
import org.eclipse.osgi.internal.framework.FilterImpl;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.service.resolver.State;
import org.eclipse.tycho.ArtifactDescriptor;
import org.eclipse.tycho.ArtifactKey;
import org.eclipse.tycho.ArtifactType;
import org.eclipse.tycho.PackagingType;
import org.eclipse.tycho.ReactorProject;
import org.eclipse.tycho.artifacts.DependencyArtifacts;
import org.eclipse.tycho.classpath.ClasspathEntry;
import org.eclipse.tycho.classpath.ClasspathEntry.AccessRule;
import org.eclipse.tycho.core.ArtifactDependencyVisitor;
import org.eclipse.tycho.core.ArtifactDependencyWalker;
import org.eclipse.tycho.core.BundleProject;
import org.eclipse.tycho.core.PluginDescription;
import org.eclipse.tycho.core.TargetPlatformConfiguration;
import org.eclipse.tycho.core.TychoConstants;
import org.eclipse.tycho.core.TychoProject;
import org.eclipse.tycho.core.ee.StandardExecutionEnvironment;
import org.eclipse.tycho.core.ee.shared.ExecutionEnvironment;
import org.eclipse.tycho.core.ee.shared.ExecutionEnvironmentConfiguration;
import org.eclipse.tycho.core.osgitools.DefaultClasspathEntry.DefaultAccessRule;
import org.eclipse.tycho.core.osgitools.DependencyComputer.DependencyEntry;
import org.eclipse.tycho.core.osgitools.project.BuildOutputJar;
import org.eclipse.tycho.core.osgitools.project.EclipsePluginProject;
import org.eclipse.tycho.core.osgitools.project.EclipsePluginProjectImpl;
import org.eclipse.tycho.core.resolver.shared.PlatformPropertiesUtils;
import org.eclipse.tycho.core.shared.BuildPropertiesParser;
import org.eclipse.tycho.core.shared.TargetEnvironment;
import org.eclipse.tycho.core.utils.TychoProjectUtils;
import org.eclipse.tycho.model.Feature;
import org.eclipse.tycho.model.ProductConfiguration;
import org.eclipse.tycho.model.UpdateSite;
import org.osgi.framework.BundleException;
import org.osgi.framework.InvalidSyntaxException;

@Component(role = TychoProject.class, hint = PackagingType.TYPE_ECLIPSE_PLUGIN)
public class OsgiBundleProject extends AbstractTychoProject implements BundleProject {

    private static final String CTX_ARTIFACT_KEY = TychoConstants.CTX_BASENAME + "/osgiBundle/artifactKey";

    @Requirement
    private BundleReader bundleReader;

    @Requirement
    private BuildPropertiesParser buildPropertiesParser;

    @Requirement
    private EquinoxResolver resolver;

    @Requirement
    private DependencyComputer dependencyComputer;

    @Override
    public ArtifactDependencyWalker getDependencyWalker(MavenProject project, TargetEnvironment environment) {
        return getDependencyWalker(project);
    }

    @Override
    public ArtifactDependencyWalker getDependencyWalker(MavenProject project) {
        final DependencyArtifacts artifacts = getDependencyArtifacts(project);

        final List<ClasspathEntry> cp = getClasspath(project);

        return new ArtifactDependencyWalker() {
            @Override
            public void walk(ArtifactDependencyVisitor visitor) {
                for (ClasspathEntry entry : cp) {
                    ArtifactDescriptor artifact = artifacts.getArtifact(entry.getArtifactKey());

                    ArtifactKey key = artifact.getKey();
                    File location = artifact.getLocation();
                    ReactorProject project = artifact.getMavenProject();
                    String classifier = artifact.getClassifier();
                    Set<Object> installableUnits = artifact.getInstallableUnits();

                    PluginDescription plugin = new DefaultPluginDescription(key, location, project, classifier, null,
                            installableUnits);

                    visitor.visitPlugin(plugin);
                }
            }

            @Override
            public void traverseFeature(File location, Feature feature, ArtifactDependencyVisitor visitor) {
            }

            @Override
            public void traverseUpdateSite(UpdateSite site, ArtifactDependencyVisitor artifactDependencyVisitor) {
            }

            @Override
            public void traverseProduct(ProductConfiguration productConfiguration, ArtifactDependencyVisitor visitor) {
            }
        };
    }

    @Override
    public ArtifactKey getArtifactKey(ReactorProject project) {
        ArtifactKey key = (ArtifactKey) project.getContextValue(CTX_ARTIFACT_KEY);
        if (key == null) {
            throw new IllegalStateException("Project has not been setup yet " + project.toString());
        }

        return key;
    }

    @Override
    public void setupProject(MavenSession session, MavenProject project) {
        ArtifactKey key = readArtifactKey(project.getBasedir());
        project.setContextValue(CTX_ARTIFACT_KEY, key);
    }

    public ArtifactKey readArtifactKey(File location) {
        OsgiManifest mf = bundleReader.loadManifest(location);
        return mf.toArtifactKey();
    }

    @Override
    public String getManifestValue(String key, MavenProject project) {
        return getManifest(project).getValue(key);
    }

    private OsgiManifest getManifest(MavenProject project) {
        return bundleReader.loadManifest(project.getBasedir());
    }

    @Override
    public void resolveClassPath(MavenSession session, MavenProject project) {
        DependencyArtifacts artifacts = getDependencyArtifacts(project);

        State state = getResolverState(project, artifacts);

        if (getLogger().isDebugEnabled() && DebugUtils.isDebugEnabled(session, project)) {
            getLogger().debug(resolver.toDebugString(state));
        }

        BundleDescription bundleDescription = state.getBundleByLocation(project.getBasedir().getAbsolutePath());

        List<ClasspathEntry> classpath = new ArrayList<>();

        // project itself
        ArtifactDescriptor artifact = getArtifact(artifacts, project.getBasedir(), bundleDescription.getSymbolicName());
        ReactorProject projectProxy = DefaultReactorProject.adapt(project);
        List<File> projectClasspath = getThisProjectClasspath(artifact, projectProxy);
        classpath.add(new DefaultClasspathEntry(projectProxy, artifact.getKey(), projectClasspath, null));

        // build.properties/jars.extra.classpath
        addExtraClasspathEntries(classpath, projectProxy, artifacts);

        // dependencies
        List<AccessRule> strictBootClasspathAccessRules = new ArrayList<>();
        strictBootClasspathAccessRules.add(new DefaultAccessRule("java/**", false));
        for (DependencyEntry entry : dependencyComputer.computeDependencies(state.getStateHelper(), bundleDescription)) {
            if (EquinoxResolver.SYSTEM_BUNDLE_ID == entry.desc.getBundleId()) {
                if (entry.rules != null) {
                    strictBootClasspathAccessRules.addAll(entry.rules);
                }
                if (EquinoxResolver.SYSTEM_BUNDLE_SYMBOLIC_NAME.equals(entry.desc.getSymbolicName())) {
                    // synthetic system.bundle has no filesystem location
                    continue;
                }
            }
            File location = new File(entry.desc.getLocation());
            ArtifactDescriptor otherArtifact = getArtifact(artifacts, location, entry.desc.getSymbolicName());
            ReactorProject otherProject = otherArtifact.getMavenProject();
            List<File> locations;
            if (otherProject != null) {
                locations = getOtherProjectClasspath(otherArtifact, otherProject, null);
            } else {
                locations = getBundleClasspath(otherArtifact);
            }

            if (locations.isEmpty() && !entry.rules.isEmpty()) {
                getLogger().warn("Empty classpath of required bundle " + otherArtifact);
            }

            classpath.add(new DefaultClasspathEntry(otherProject, otherArtifact.getKey(), locations, entry.rules));
        }
        project.setContextValue(TychoConstants.CTX_ECLIPSE_PLUGIN_CLASSPATH, classpath);

        project.setContextValue(TychoConstants.CTX_ECLIPSE_PLUGIN_STRICT_BOOTCLASSPATH_ACCESSRULES,
                strictBootClasspathAccessRules);
        project.setContextValue(TychoConstants.CTX_ECLIPSE_PLUGIN_BOOTCLASSPATH_EXTRA_ACCESSRULES,
                dependencyComputer.computeBootClasspathExtraAccessRules(state.getStateHelper(), bundleDescription));

        addPDESourceRoots(project);
    }

    protected ArtifactDescriptor getArtifact(DependencyArtifacts artifacts, File location, String id) {
        Map<String, ArtifactDescriptor> classified = artifacts.getArtifact(location);
        if (classified != null) {
            for (ArtifactDescriptor artifact : classified.values()) {
                if (id.equals(artifact.getKey().getId())) {
                    return artifact;
                }
            }
        }
        return null;
    }

    private void addPDESourceRoots(MavenProject project) {
        EclipsePluginProjectImpl eclipsePluginProject = getEclipsePluginProject(DefaultReactorProject.adapt(project));
        for (BuildOutputJar outputJar : eclipsePluginProject.getOutputJars()) {
            for (File sourceFolder : outputJar.getSourceFolders()) {
                removeDuplicateTestCompileRoot(sourceFolder, project.getTestCompileSourceRoots());
                project.addCompileSourceRoot(sourceFolder.getAbsolutePath());
            }
        }
    }

    private void removeDuplicateTestCompileRoot(File sourceFolder, List<String> testCompileSourceRoots) {
        for (Iterator<String> iterator = testCompileSourceRoots.iterator(); iterator.hasNext();) {
            String testCompileRoot = iterator.next();
            if (sourceFolder.equals(new File(testCompileRoot))) {
                // avoid duplicate source folders (bug 368445)
                iterator.remove();
                getLogger().debug(
                        "Removed duplicate test compile root " + testCompileRoot + " from maven project model");
                return;
            }
        }
    }

    private State getResolverState(MavenProject project, DependencyArtifacts artifacts) {
        try {
            ExecutionEnvironmentConfiguration eeConfiguration = TychoProjectUtils
                    .getExecutionEnvironmentConfiguration(project);
            ExecutionEnvironment executionEnvironment = eeConfiguration.getFullSpecification();
            return resolver.newResolvedState(project, executionEnvironment, eeConfiguration.isIgnoredByResolver(),
                    artifacts);
        } catch (BundleException e) {
            throw new RuntimeException(e);
        }
    }

    public EclipsePluginProjectImpl getEclipsePluginProject(ReactorProject otherProject) {
        EclipsePluginProjectImpl pdeProject = (EclipsePluginProjectImpl) otherProject
                .getContextValue(TychoConstants.CTX_ECLIPSE_PLUGIN_PROJECT);
        if (pdeProject == null) {
            try {
                pdeProject = new EclipsePluginProjectImpl(otherProject, buildPropertiesParser);
                otherProject.setContextValue(TychoConstants.CTX_ECLIPSE_PLUGIN_PROJECT, pdeProject);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return pdeProject;
    }

    @Override
    public List<ClasspathEntry> getClasspath(MavenProject project) {
        @SuppressWarnings("unchecked")
        List<ClasspathEntry> classpath = (List<ClasspathEntry>) project
                .getContextValue(TychoConstants.CTX_ECLIPSE_PLUGIN_CLASSPATH);
        if (classpath == null) {
            throw new IllegalStateException();
        }
        return classpath;
    }

    @Override
    public List<ClasspathEntry.AccessRule> getBootClasspathExtraAccessRules(MavenProject project) {
        @SuppressWarnings("unchecked")
        List<ClasspathEntry.AccessRule> rules = (List<AccessRule>) project
                .getContextValue(TychoConstants.CTX_ECLIPSE_PLUGIN_BOOTCLASSPATH_EXTRA_ACCESSRULES);
        if (rules == null) {
            throw new IllegalStateException();
        }
        return rules;
    }

    /**
     * Returns project compile classpath entries.
     */
    private List<File> getThisProjectClasspath(ArtifactDescriptor bundle, ReactorProject project) {
        LinkedHashSet<File> classpath = new LinkedHashSet<>();

        EclipsePluginProject pdeProject = getEclipsePluginProject(project);

        Map<String, BuildOutputJar> outputJars = pdeProject.getOutputJarMap();

        // unconditionally add all output jars (even if does not exist or not on Bundle-ClassPath)
        for (BuildOutputJar jar : outputJars.values()) {
            classpath.add(jar.getOutputDirectory());
        }

        // Bundle-ClassPath entries that do not have associated output folders
        // => assume it's checked into SCM or will be copied here later during build
        for (String cp : parseBundleClasspath(bundle)) {
            if (!outputJars.containsKey(cp)) {
                classpath.add(new File(project.getBasedir(), cp));
            }
        }

        return new ArrayList<>(classpath);
    }

    /**
     * Returns bundle classpath entries. If <code>nestedPath</code> is not <code>null</code>,
     * returns single class folder that corresponds specified nestedPath. If <code>nestedPath</code>
     * is <code>null</code>, returns entries specified in Bundle-ClassPath.
     */
    private List<File> getOtherProjectClasspath(ArtifactDescriptor bundle, ReactorProject otherProject,
            String nestedPath) {
        LinkedHashSet<File> classpath = new LinkedHashSet<>();

        EclipsePluginProject pdeProject = getEclipsePluginProject(otherProject);

        Map<String, BuildOutputJar> outputJars = pdeProject.getOutputJarMap();
        String[] bundleClassPath;
        if (nestedPath == null) {
            bundleClassPath = parseBundleClasspath(bundle);
        } else {
            bundleClassPath = new String[] { nestedPath };
        }
        for (String cp : bundleClassPath) {
            if (outputJars.containsKey(cp)) {
                // add output folder even if it does not exist (yet)
                classpath.add(outputJars.get(cp).getOutputDirectory());
            } else {
                // no associated output folder 
                // => assume it's checked into SCM or will be copied here later during build
                classpath.add(new File(otherProject.getBasedir(), cp));
            }
        }
        return new ArrayList<>(classpath);
    }

    private void addExtraClasspathEntries(List<ClasspathEntry> classpath, ReactorProject project,
            DependencyArtifacts artifacts) {
        EclipsePluginProject pdeProject = getEclipsePluginProject(project);
        Collection<BuildOutputJar> outputJars = pdeProject.getOutputJarMap().values();
        for (BuildOutputJar buildOutputJar : outputJars) {
            List<String> entries = buildOutputJar.getExtraClasspathEntries();
            for (String entry : entries) {
                Pattern platformURL = Pattern.compile("platform:/(plugin|fragment)/([^/]*)/*(.*)");
                Matcher m = platformURL.matcher(entry.trim());
                String bundleId = null;
                String path = null;
                if (m.matches()) {
                    bundleId = m.group(2).trim();
                    path = m.group(3).trim();

                    if (path != null && path.length() <= 0) {
                        path = null;
                    }

                    ArtifactDescriptor matchingBundle = artifacts.getArtifact(ArtifactType.TYPE_ECLIPSE_PLUGIN,
                            bundleId, null);
                    if (matchingBundle != null) {
                        List<File> locations;
                        if (matchingBundle.getMavenProject() != null) {
                            locations = getOtherProjectClasspath(matchingBundle, matchingBundle.getMavenProject(), path);
                        } else if (path != null) {
                            locations = getBundleEntry(matchingBundle, path);
                        } else {
                            locations = getBundleClasspath(matchingBundle);
                        }
                        classpath.add(new DefaultClasspathEntry(matchingBundle.getMavenProject(), matchingBundle
                                .getKey(), locations, null));
                    } else {
                        getLogger().warn("Missing extra classpath entry " + entry.trim());
                    }
                } else {
                    entry = entry.trim();
                    File file = new File(project.getBasedir(), entry).getAbsoluteFile();
                    if (file.exists()) {
                        List<File> locations = Collections.singletonList(file);
                        ArtifactKey projectKey = getArtifactKey(project);
                        classpath.add(new DefaultClasspathEntry(project, projectKey, locations, null));
                    } else {
                        getLogger().warn("Missing extra classpath entry " + entry);
                    }
                }
            }
        }
    }

    private List<File> getBundleClasspath(ArtifactDescriptor bundle) {
        LinkedHashSet<File> classpath = new LinkedHashSet<>();

        for (String cp : parseBundleClasspath(bundle)) {
            File entry;
            if (".".equals(cp)) {
                entry = bundle.getLocation();
            } else {
                entry = getNestedJarOrDir(bundle, cp);
            }

            if (entry != null) {
                classpath.add(entry);
            }
        }

        return new ArrayList<>(classpath);
    }

    private List<File> getBundleEntry(ArtifactDescriptor bundle, String nestedPath) {
        LinkedHashSet<File> classpath = new LinkedHashSet<>();

        File entry;
        if (".".equals(nestedPath)) {
            entry = bundle.getLocation();
        } else {
            entry = getNestedJarOrDir(bundle, nestedPath);
        }

        if (entry != null) {
            classpath.add(entry);
        }

        return new ArrayList<>(classpath);
    }

    private String[] parseBundleClasspath(ArtifactDescriptor bundle) {
        OsgiManifest mf = bundleReader.loadManifest(bundle.getLocation());
        return mf.getBundleClasspath();
    }

    private File getNestedJarOrDir(ArtifactDescriptor bundle, String cp) {
        return bundleReader.getEntry(bundle.getLocation(), cp);
    }

    @Override
    public TargetEnvironment getImplicitTargetEnvironment(MavenProject project) {
        String filterStr = getManifestValue(EclipsePlatformNamespace.ECLIPSE_PLATFORM_FILTER_HEADER, project);

        if (filterStr != null) {
            try {
                FilterImpl filter = FilterImpl.newInstance(filterStr);

                String ws = sn(filter.getPrimaryKeyValue(PlatformPropertiesUtils.OSGI_WS));
                String os = sn(filter.getPrimaryKeyValue(PlatformPropertiesUtils.OSGI_OS));
                String arch = sn(filter.getPrimaryKeyValue(PlatformPropertiesUtils.OSGI_ARCH));

                // validate if os/ws/arch are not null and actually match the filter
                if (ws != null && os != null && arch != null) {
                    Map<String, String> properties = new HashMap<>();
                    properties.put(PlatformPropertiesUtils.OSGI_WS, ws);
                    properties.put(PlatformPropertiesUtils.OSGI_OS, os);
                    properties.put(PlatformPropertiesUtils.OSGI_ARCH, arch);

                    if (filter.matches(properties)) {
                        return new TargetEnvironment(os, ws, arch);
                    }
                }
            } catch (InvalidSyntaxException e) {
                // at least we tried...
            }
        }

        return null;
    }

    private static String sn(String str) {
        if (str != null && !"".equals(str.trim())) {
            return str;
        }
        return null;
    }

    @Override
    public void readExecutionEnvironmentConfiguration(MavenProject project, ExecutionEnvironmentConfiguration sink) {
        // read packaging-type independent configuration
        super.readExecutionEnvironmentConfiguration(project, sink);

        // only in plugin projects, the profile may also be ...
        // ... specified in build.properties (for PDE compatibility)
        String pdeProfile = getEclipsePluginProject(DefaultReactorProject.adapt(project)).getBuildProperties()
                .getJreCompilationProfile();
        if (pdeProfile != null) {
            sink.setProfileConfiguration(pdeProfile.trim(), "build.properties");

        } else {
            // ... derived from BREE in bundle manifest
            StandardExecutionEnvironment[] manifestBREEs = getManifest(project).getExecutionEnvironments();
            if (manifestBREEs.length > 0) {
                TargetPlatformConfiguration tpConfiguration = TychoProjectUtils.getTargetPlatformConfiguration(project);

                switch (tpConfiguration.getBREEHeaderSelectionPolicy()) {
                case first:
                    sink.setProfileConfiguration(manifestBREEs[0].getProfileName(),
                            "Bundle-RequiredExecutionEnvironment (first entry)");
                    break;

                case minimal:
                    ExecutionEnvironment manifestMinimalEE = Collections.min(Arrays.asList(manifestBREEs));
                    sink.setProfileConfiguration(manifestMinimalEE.getProfileName(),
                            "Bundle-RequiredExecutionEnvironment (minimal entry)");
                }

            }
        }
    }

}
