/*
 * 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.netbeans.modules.maven.classpath;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.platform.JavaPlatform;
import org.netbeans.api.java.platform.JavaPlatformManager;
import org.netbeans.modules.maven.NbMavenProjectImpl;
import org.netbeans.modules.maven.api.Constants;
import org.netbeans.modules.maven.api.FileUtilities;
import org.netbeans.modules.maven.api.PluginPropertyUtils;
import org.netbeans.modules.maven.indexer.api.NBVersionInfo;
import org.netbeans.modules.maven.indexer.api.RepositoryInfo;
import org.netbeans.modules.maven.indexer.api.RepositoryPreferences;
import org.netbeans.modules.maven.indexer.api.RepositoryQueries;
import org.netbeans.modules.maven.indexer.api.RepositoryUtil;
import org.netbeans.spi.java.classpath.ClassPathImplementation;
import org.netbeans.spi.java.classpath.PathResourceImplementation;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
import org.openide.filesystems.FileAttributeEvent;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileRenameEvent;
import org.openide.filesystems.FileUtil;
import org.openide.util.RequestProcessor;

/**
 * NO listening on changes here, let the BootClassPath deal with it..
 * @author  Milos Kleint
 */
@org.netbeans.api.annotations.common.SuppressWarnings("DMI_COLLECTION_OF_URLS")
public final class EndorsedClassPathImpl implements ClassPathImplementation, FileChangeListener {

    private static final Logger LOG = Logger.getLogger(EndorsedClassPathImpl.class.getName());
    static final RequestProcessor RP = new RequestProcessor(EndorsedClassPathImpl.class);

    private List<? extends PathResourceImplementation> resourcesCache;
    private boolean includeJDKCache;
    private boolean includeFXCache;
    private final PropertyChangeSupport support = new PropertyChangeSupport(this);
    private final NbMavenProjectImpl project;
    private BootClassPathImpl bcp;
    private String[] current;
    private final File endorsed;
    private final Map<File,File/*|null*/> endorsed2Repo = new HashMap<File,File>();

    @SuppressWarnings("LeakingThisInConstructor")
    EndorsedClassPathImpl(NbMavenProjectImpl project) {
        this.project = project;
        endorsed = new File(project.getPOMFile().getParentFile(), "target/endorsed"); // NOI18N
        FileUtil.addFileChangeListener(this, endorsed);
    }

    public @Override List<? extends PathResourceImplementation> getResources() {
        boolean[] arr = { false };
        return getResources(arr, arr);
    }
    
    final List<? extends PathResourceImplementation> getResources(boolean[] includeJDK, boolean[] includeFx) {
        assert bcp != null;
        synchronized (bcp.LOCK) {
            if (this.resourcesCache == null) {
                ArrayList<PathResourceImplementation> result = new ArrayList<PathResourceImplementation> ();
                String[] boot = getBootClasspath();
                includeJDKCache = true;
                includeFXCache = false;
                if (boot != null) {
                    for (String b : boot) {
                        if ("netbeans.ignore.jdk.bootclasspath".equals(b)) { // NOI18N
                            includeJDK[0] = false;
                            includeFXCache = false;
                            includeJDKCache = false;
                        }
                    }
                    StripPlatformResult res = stripDefaultJavaPlatform(boot);
                    includeFx[0] = res.hasFx;
                    includeFXCache = res.hasFx;
                    for (URL u :  res.urls) {
                        if (u != null) {
                            result.add (ClassPathSupport.createResource(u));
                        }
                    }
                }
                File[] jars = endorsed.listFiles();
                if (jars != null) {
                    for (final File jar : jars) {
                        if (jar.isFile()) {
                            if (endorsed2Repo.containsKey(jar)) {
                                File toScan = endorsed2Repo.get(jar);
                                if (toScan != null) {
                                    URL url = FileUtil.urlForArchiveOrDir(toScan);
                                    if (url != null) {
                                        result.add(ClassPathSupport.createResource(url));
                                    }
                                }
                            } else {
                                // #197510: blocking, must do this asynch
                                LOG.log(Level.FINE, "looking up {0}", jar);
                                RP.post(new Runnable() {
                                    public @Override void run() {
                                        synchronized (bcp.LOCK) {
                                            if (endorsed2Repo.containsKey(jar)) {
                                                // Another task beat us to it.
                                                return;
                                            }
                                        }
                                        if (!jar.isFile()) {
                                            return;
                                        }
                                        File toScan = null;
                                        REPO: for (RepositoryInfo repo : RepositoryPreferences.getInstance().getRepositoryInfos()) {
                                            LOG.log(Level.FINE, "checking {0}", repo);
                                            for (NBVersionInfo analogue : RepositoryQueries.findBySHA1Result(jar, Collections.singletonList(repo)).getResults()) {
                                                toScan = RepositoryUtil.createArtifact(analogue).getFile();
                                                LOG.log(Level.FINE, "found {0}", toScan);
                                                break REPO;
                                            }
                                        }
                                        if (toScan == null) {
                                            try {
                                                toScan = FileUtil.normalizeFile(new File(System.getProperty("java.io.tmpdir"), RepositoryUtil.calculateSHA1Checksum(jar) + ".jar"));
                                                if (!toScan.isFile()) {
                                                    FileUtils.copyFile(jar, toScan);
                                                }
                                            } catch (IOException x) {
                                                LOG.log(Level.INFO, "copying " + jar + " to " + toScan, x);
                                            }
                                        }
                                        LOG.log(Level.FINE, "mapping {0} -> {1}", new Object[] {jar, toScan});
                                        synchronized (bcp.LOCK) {
                                            endorsed2Repo.put(jar, toScan);
                                            resourcesCache = null;
                                        }
                                        support.firePropertyChange(PROP_RESOURCES, null, null);
                                    }
                                });
                            }
                        }
                    }
                }
                current = boot;
                resourcesCache = Collections.unmodifiableList (result);
            } else {
                includeJDK[0] = includeJDKCache;
                includeFx[0] = includeFXCache;
            }
            return this.resourcesCache;
        }
    }

    public @Override void addPropertyChangeListener(PropertyChangeListener listener) {
        this.support.addPropertyChangeListener (listener);
    }

    public @Override void removePropertyChangeListener(PropertyChangeListener listener) {
        this.support.removePropertyChangeListener (listener);
    }

    private String[] getBootClasspath() {
        String carg = PluginPropertyUtils.getPluginProperty(project, Constants.GROUP_APACHE_PLUGINS, Constants.PLUGIN_COMPILER, "compilerArgument", "compile", null);
        if (carg != null) {
            //TODO
        }
        Properties cargs = PluginPropertyUtils.getPluginPropertyParameter(project, Constants.GROUP_APACHE_PLUGINS, Constants.PLUGIN_COMPILER, "compilerArguments", "compile");
        if (cargs != null) {
            String carg2 = cargs.getProperty("bootclasspath");
            if (carg2 != null) {
                return StringUtils.split(carg2, File.pathSeparator);
            }
        }
        return null;
    }

    /**
     * Resets the cache and firesPropertyChange
     */
    boolean resetCache () {
        String[] newones = getBootClasspath();
        boolean fire = false;
        assert bcp != null;
        synchronized (bcp.LOCK) {
            if (!Arrays.equals(newones, current)) {
                resourcesCache = null;
                fire = true;
            }
        }
        if (fire) {
            support.firePropertyChange(PROP_RESOURCES, null, null);
        }
        return fire;
    }

    void setBCP(BootClassPathImpl aThis) {
        bcp = aThis;
    }
    private class StripPlatformResult {
        List<URL> urls;
        boolean hasFx = false;
    }

    private StripPlatformResult stripDefaultJavaPlatform(String[] boot) {
        StripPlatformResult res = new StripPlatformResult();
        List<URL> toRet = new ArrayList<URL>();
        res.urls = toRet;
        Set<URL> defs = getDefJavaPlatBCP();
        OUTER: for (String s : boot) {
            File f = FileUtilities.convertStringToFile(s);
            URL entry = FileUtil.urlForArchiveOrDir(f);
            if (entry != null && !defs.contains(entry)) {
                if (entry.getPath().endsWith("/jfxrt.jar!/")) {
                    //we need to iterate the defs and check again as jdk8 and jdk7 have these at different places
                    for (URL d : defs) {
                        if (d.getPath().endsWith("/jfxrt.jar!/")) {
                            res.hasFx = true;
                            continue OUTER;
                        }
                    }
                }
                toRet.add(entry);
            }
        }
        return res;
    }

    private final Set<URL> djpbcp = new HashSet<URL>();

    private Set<URL> getDefJavaPlatBCP() {
        synchronized (djpbcp) {
            if (djpbcp.isEmpty()) {
                JavaPlatformManager mngr = JavaPlatformManager.getDefault();
                JavaPlatform jp = mngr.getDefaultPlatform();
                ClassPath cp = jp.getBootstrapLibraries();
                for (ClassPath.Entry ent : cp.entries()) {
                    djpbcp.add(ent.getURL());
                }
            }
            return Collections.unmodifiableSet(djpbcp);
        }
    }

    private void fileChange() {
        assert bcp != null;
        synchronized (bcp.LOCK) {
            resourcesCache = null;
        }
        support.firePropertyChange(PROP_RESOURCES, null, null);
    }
    public @Override void fileFolderCreated(FileEvent fe) {
        fileChange();
    }
    public @Override void fileDataCreated(FileEvent fe) {
        fileChange();
    }
    public @Override void fileChanged(FileEvent fe) {
        fileChange();
    }
    public @Override void fileDeleted(FileEvent fe) {
        fileChange();
    }
    public @Override void fileRenamed(FileRenameEvent fe) {
        fileChange();
    }
    public @Override void fileAttributeChanged(FileAttributeEvent fe) {}

}