/*
 * 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.autoupdate.services;

import java.beans.PropertyVetoException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.Module;
import org.netbeans.api.autoupdate.OperationException;
import org.netbeans.api.autoupdate.UpdateElement;
import org.netbeans.core.startup.MainLookup;
import org.netbeans.core.startup.layers.LocalFileSystemEx;
import org.netbeans.spi.autoupdate.AutoupdateClusterCreator;
import org.netbeans.updater.ModuleDeactivator;
import org.netbeans.updater.UpdateTracking;
import org.openide.filesystems.FileUtil;
import org.openide.modules.InstalledFileLocator;
import org.openide.util.Lookup;
import org.openide.util.lookup.ServiceProvider;

/**
 *
 * @author Jiri Rechtacek
 */
@ServiceProvider(service=InstalledFileLocator.class)
public class InstallManager extends InstalledFileLocator{
    
    // special directories in NB files layout
    static final String NBM_LIB = "lib"; // NOI18N
    static final String NBM_CORE = "core"; // NOI18N
    static final String NETBEANS_DIRS = "netbeans.dirs"; // NOI18N

    private static int countOfWarnings = 0;
    private static final int MAX_COUNT_OF_WARNINGS = 5;

    private static final Logger ERR = Logger.getLogger ("org.netbeans.modules.autoupdate.services.InstallManager");
    private static final List<File> clusters = new ArrayList<File>();
    
    static File findTargetDirectory (UpdateElement installed, UpdateElementImpl update, Boolean globalOrLocal, boolean useUserdirAsFallback) throws OperationException {
        File res;
        if (globalOrLocal == null) {
            globalOrLocal = isGlobalInstallation();
        }
        boolean isGlobal = globalOrLocal == null ? false : globalOrLocal;
        
        if (Boolean.FALSE.equals(globalOrLocal)) {
            ERR.log(Level.INFO, "Forced installation in userdir only for " + update.getUpdateElement());
            return getUserDir();
        }
        
        // if an update, overwrite the existing location, wherever that is.
        if (installed != null) {
            
            // adjust isGlobal to forced global if present
            isGlobal |= update.getInstallInfo ().isGlobal () != null && update.getInstallInfo ().isGlobal ();
            res = getInstallDir (installed, update, isGlobal, useUserdirAsFallback);
            
        } else {

            // #111384: fixed modules must be installed globally
            isGlobal |= update.isFixed ();

            // adjust isGlobal to forced global if present
            isGlobal |= update.getInstallInfo ().isGlobal () != null && update.getInstallInfo ().isGlobal ();
            
            final String targetCluster = update.getInstallInfo ().getTargetCluster ();

            // global or local
            if ((targetCluster != null && targetCluster.length () > 0) || isGlobal) {
                res = checkTargetCluster(update, targetCluster, isGlobal, useUserdirAsFallback);
                
                // handle non-existing clusters
                if (res == null && targetCluster != null) {
                    res = createNonExistingCluster (targetCluster);
                    if (res != null) {
                        res = checkTargetCluster(update, targetCluster, isGlobal, useUserdirAsFallback);
                    }
                }
                
                // target cluster still not found
                if (res == null) {
                    
                    // create UpdateTracking.EXTRA_CLUSTER_NAME
                    createNonExistingCluster (UpdateTracking.EXTRA_CLUSTER_NAME);
                    // check writable installation
                    res = checkTargetCluster(update, UpdateTracking.EXTRA_CLUSTER_NAME, isGlobal, useUserdirAsFallback);
                    
                    // no new cluster was created => use userdir
                    res = res == null? getUserDir () : res;
                    
                    if (targetCluster != null) {
                        ERR.log (Level.INFO, "Declared target cluster " + targetCluster + 
                                " in " + update.getUpdateElement () + " wasn't found or was read only. Will be used " + res);
                    } else {
                        ERR.log (Level.INFO, res + " will be used as target cluster");
                    }
                    
                }
                
            } else {
                // is local
                res = getUserDir ();
            }
        }
        ERR.log (Level.FINEST, "UpdateElement " + update.getUpdateElement () + " has the target cluster " + res);
        return res;
    }
    
    private static File checkTargetCluster(UpdateElementImpl update, String targetCluster, boolean isGlobal, boolean useUserdirAsFallback) throws OperationException {
        if (targetCluster == null || targetCluster.length () == 0) {
            return null;
        }
        File res = null;
        // is global or
        // does have a target cluster?
        for (File cluster : UpdateTracking.clusters (true)) {
            if (targetCluster.equals (cluster.getName ())) {
                boolean wasNew = ! cluster.exists ();
                if (Utilities.canWriteInCluster (cluster)) {
                    if (wasNew) {
                        cluster.mkdirs ();
                        extendSystemFileSystem (cluster);
                    }
                    res = cluster;
                } else {
                    if (! useUserdirAsFallback && isGlobal) {
                        ERR.log(Level.WARNING, "There is no write permission to write in target cluster " + targetCluster + " for " + update.getUpdateElement());
                        throw new OperationException(OperationException.ERROR_TYPE.WRITE_PERMISSION, update.getCodeName());
                    }
                    if (countOfWarnings++ < MAX_COUNT_OF_WARNINGS) {
                        ERR.log(Level.WARNING, "There is no write permission to write in target cluster " + targetCluster + " for " + update.getUpdateElement());
                    }
                    if (countOfWarnings == MAX_COUNT_OF_WARNINGS) {
                        ERR.log(Level.WARNING, "There is no write permission to write in target cluster " + targetCluster + " for more updates or plugins.");
                    }
                }
                break;
            }
        }

        return res;
    }
    
    private static File createNonExistingCluster (String targetCluster) {
        File res = null;
        for (AutoupdateClusterCreator creator : Lookup.getDefault ().lookupAll (AutoupdateClusterCreator.class)) {
            File possibleCluster = Trampoline.SPI.findCluster (targetCluster, creator);
            if (possibleCluster != null) {
                try {
                    ERR.log (Level.FINE, "Found cluster candidate " + possibleCluster + " for declared target cluster " + targetCluster);
                    File[] dirs = Trampoline.SPI.registerCluster (targetCluster, possibleCluster, creator);

                    // it looks good, generate new netbeans.dirs
                    res = possibleCluster;

                    StringBuffer sb = new StringBuffer ();
                    String sep = "";
                    for (File dir : dirs) {
                        sb.append (sep);
                        sb.append(dir.getPath());
                        sep = File.pathSeparator;
                    }

                    System.setProperty(NETBEANS_DIRS, sb.toString ());
                    File f = new File(new File(getUserDir(), Utilities.DOWNLOAD_DIR), NETBEANS_DIRS);
                    if (!f.exists()) {
                        f.getParentFile().mkdirs();
                        f.createNewFile();
                    }
                    OutputStream os = new FileOutputStream(f);
                    try {
                        os.write(sb.toString().getBytes());
                    } finally {
                        os.close();
                    }
                    ERR.log (Level.FINE, "Was written new netbeans.dirs " + sb);

                    break;

                } catch (IOException ioe) {
                    ERR.log (Level.INFO, ioe.getMessage (), ioe);
                }
            }
        }
        return res;
    }

    private static void extendSystemFileSystem(File cluster) {
        try {
            File extradir = new File(cluster, ModuleDeactivator.CONFIG);
            extradir.mkdir();
            LocalFileSystemEx lfse = new LocalFileSystemEx();
            lfse.setRootDirectory(extradir);
            MainLookup.register(lfse);
            synchronized (InstallManager.class) {
                clusters.add(cluster);
            }
        } catch (PropertyVetoException ioe) {
            ERR.log (Level.INFO, ioe.getMessage (), ioe);
        } catch (IOException ioe) {
            ERR.log (Level.INFO, ioe.getMessage (), ioe);
        }
    }
    
    // can be null for fixed modules
    private static File getInstallDir (UpdateElement installed, UpdateElementImpl update, boolean isGlobal, boolean useUserdirAsFallback) throws OperationException {
        File res = null;
        UpdateElementImpl i = Trampoline.API.impl (installed);
        assert i instanceof ModuleUpdateElementImpl : "Impl of " + installed + " instanceof ModuleUpdateElementImpl";
        
        Module m = Utilities.toModule (((ModuleUpdateElementImpl) i).getModuleInfo ());
        File jarFile = m == null ? null : m.getJarFile ();
        
        if (jarFile == null) {
            // only fixed module cannot be located
            ERR.log (Level.FINE, "No install dir for " + installed + " (It's ok for fixed). Is fixed? " + Trampoline.API.impl (installed).isFixed ());
            String targetCluster = update.getInstallInfo ().getTargetCluster ();
            if (targetCluster != null) {
                for (File cluster : UpdateTracking.clusters (false)) {
                    if (targetCluster.equals (cluster.getName ())) {
                        res = cluster;
                        break;
                    }
                }
            }
            if (res == null) {
                // go to platform if no cluster is known
                res = UpdateTracking.getPlatformDir ();
            }
        } else {
            
            /* comment out for xtesting
            FileObject searchForFO = FileUtil.toFileObject (configFile);
            for (File cluster : UpdateTracking.clusters (true)) {       
                cluster = FileUtil.normalizeFile(cluster);
                if (FileUtil.isParentOf (FileUtil.toFileObject (cluster), searchForFO)) {
                    res = cluster;
                    break;
                }*/
            
            for (File cluster : UpdateTracking.clusters (true)) {       
                cluster = FileUtil.normalizeFile (cluster);
                if (isParentOf (cluster, jarFile)) {
                    res = cluster;
                    break;
                }
            }
        }

        if (res == null || ! Utilities.canWriteInCluster (res)) {
            if (! useUserdirAsFallback && isGlobal) {
                ERR.log(Level.WARNING, "There is no write permission to write in target cluster " + res + " for " + update.getUpdateElement());
                throw new OperationException(OperationException.ERROR_TYPE.WRITE_PERMISSION, update.getCodeName());
            }
            // go to userdir if no writable cluster is known
            if (countOfWarnings++ < MAX_COUNT_OF_WARNINGS) {
                ERR.log(Level.WARNING, "There is no write permission to write in target cluster " + res + " for " + update.getUpdateElement());
            }
            if (countOfWarnings == MAX_COUNT_OF_WARNINGS) {
                ERR.log(Level.WARNING, "There is no write permission to write in target cluster " + res + " for more updates or plugins.");
            }
            res = UpdateTracking.getUserDir ();
        }
        ERR.log (Level.FINEST, "Install dir of " + installed + " is " + res);
        
        return res;
    }
    
    private static boolean isParentOf (File parent, File child) {
        File tmp = child.getParentFile ();
        while (tmp != null && ! parent.equals (tmp)) {
            tmp = tmp.getParentFile ();
        }
        return tmp != null;
    }
    
    static File getUserDir () {
        return UpdateTracking.getUserDir ();
    }
    
    static boolean needsRestart (boolean isUpdate, UpdateElementImpl update, File dest) {
        assert update.getInstallInfo () != null : "Each UpdateElement must know own InstallInfo but " + update;
        boolean isForcedRestart = update.getInstallInfo ().needsRestart () != null && update.getInstallInfo ().needsRestart ();
        boolean needsRestart = isForcedRestart || isUpdate;
        if (! needsRestart) {
            // handle installation into core or lib directory
            needsRestart = willInstallInSystem (dest);
        }
        return needsRestart;
    }

    private static boolean willInstallInSystem (File nbmFile) {
        boolean res = false;
        try {
            JarFile jf = new JarFile (nbmFile);
            try {
                for (JarEntry entry : Collections.list (jf.entries ())) {
                    String entryName = entry.getName ();
                    if (entryName.startsWith (NBM_CORE + "/") || entryName.startsWith (NBM_LIB + "/")) {
                        res = true;
                        break;
                    }
                }
            } finally {
                jf.close();
            }
        } catch (IOException ioe) {
            ERR.log (Level.INFO, ioe.getMessage (), ioe);
        }
        
        return res;
    }

    @Override
    public File locate(String relativePath, String codeNameBase, boolean localized) {
        // Rarely returns anything so don't bother optimizing.
        Set<File> files = locateAll(relativePath, codeNameBase, localized);
        return files.isEmpty() ? null : files.iterator().next();
    }

    public @Override Set<File> locateAll(String relativePath, String codeNameBase, boolean localized) {
        synchronized (InstallManager.class) {
            if (clusters.isEmpty()) {
                return Collections.<File>emptySet();
            }
        }
        // XXX #28729: use codeNameBase to search only in the appropriate places
        if (relativePath.length() == 0) {
            throw new IllegalArgumentException("Cannot look up \"\" in InstalledFileLocator.locate"); // NOI18N
        }
        if (relativePath.charAt(0) == '/') {
            throw new IllegalArgumentException("Paths passed to InstalledFileLocator.locate should not start with '/': " + relativePath); // NOI18N
        }
        int slashIdx = relativePath.lastIndexOf('/');
        if (slashIdx == relativePath.length() - 1) {
            throw new IllegalArgumentException("Paths passed to InstalledFileLocator.locate should not end in '/': " + relativePath); // NOI18N
        }
        
        String prefix, name;
        if (slashIdx != -1) {
            prefix = relativePath.substring(0, slashIdx + 1);
            name = relativePath.substring(slashIdx + 1);
            assert name.length() > 0;
        } else {
            prefix = "";
            name = relativePath;
        }
        if (localized) {
            int i = name.lastIndexOf('.');
            String baseName, ext;
            if (i == -1) {
                baseName = name;
                ext = "";
            } else {
                baseName = name.substring(0, i);
                ext = name.substring(i);
            }
            String[] suffixes = org.netbeans.Util.getLocalizingSuffixesFast();
            Set<File> files = new HashSet<File>();
            for (String suffixe : suffixes) {
                String locName = baseName + suffixe + ext;
                files.addAll(locateExactPath(prefix, locName));
            }
            return files;
        } else {
            return locateExactPath(prefix, name);
        }
        
    }

    /** Search all top dirs for a file. */
    private static Set<File> locateExactPath(String prefix, String name) {
        Set<File> files = new HashSet<File>();
        synchronized(InstallManager.class) {
            File[] dirs = clusters.toArray(new File[clusters.size()]);
            for (File dir : dirs) {
                File f = makeFile(dir, prefix, name);
                if (f.exists()) {                    
                    files.add(f);
                }
            }            
        }        
        return files;
    }
    
    private static File makeFile(File dir, String prefix, String name) {        
        return FileUtil.normalizeFile(new File(dir, prefix.replace('/', File.separatorChar) + name));
    }
    
    private static Boolean isGlobalInstallation() {
        String s = System.getProperty("plugin.manager.install.global"); // NOI18N
        
        if (Boolean.parseBoolean(s)) {
            return Boolean.TRUE;
        } else if (Boolean.FALSE.toString().equalsIgnoreCase(s)) {
            return Boolean.FALSE;
        } else {
            return null;
        }
    }

}
