/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.modules.autoupdate;

import java.util.*;
import org.openide.DialogDisplayer;
import org.openide.ErrorManager;

import org.openide.util.NbBundle;
import org.openide.NotifyDescriptor;
import org.openide.modules.ModuleInfo;
import org.openide.modules.Dependency;
import org.openide.modules.SpecificationVersion;

/** Support class for checking dependencies between modules
 *
 * @author  phrebejk
 */
class DependencyChecker extends Object {

    /** DependencyChecker is a singleton */
    DependencyChecker() {        
    }
    
    private static final ErrorManager err = ErrorManager.getDefault ().getInstance ("org.netbeans.modules.autoupdate"); // NOI18N
    
    /** Gets collection of modules which have to be added to download list
     * if we have to add the toAddModule.
     */
    Collection modulesToAdd( ModuleUpdate toAdd, Enumeration selected, List group, StringBuffer dontAddModuleName ) {
        err.log ("DO modulesToAdd: " + toAdd.getCodeNameBase () + "[L10N? " + (toAdd instanceof L10NUpdate) + "], group: " + group + ", dontAddModuleName: " + dontAddModuleName);
        Collection result = new ArrayList();
        checkDependencies( toAdd.getRemoteModule(), result, selected, group, dontAddModuleName, toAdd.getLocalModule() != null );
        if (err.isLoggable (ErrorManager.INFORMATIONAL)) {
            String res = "";
            Iterator it = result.iterator ();
            while (it.hasNext ()) {
                ModuleUpdate mu = (ModuleUpdate) it.next ();
                res = res + mu.getCodeNameBase () + "[L10N? " + (mu instanceof L10NUpdate) + "], ";
                err.log ("modulesToAdd: " + toAdd.getCodeNameBase () + ", RETURNS: " + res);
            }
        }
        
        // add localization dependency
        err.log ("Do find localization for " + toAdd.getCodeNameBase () + " on locale " + Locale.getDefault ());
        checkFreeLocalizationDependency (toAdd, Locale.getDefault ().toString (), result, selected, group);

        return result;
    }

    /** Gets collection of modules which we have to remove from download list
     * if we have to remove the toRemove module.
     */
    Collection modulesToRemove( ModuleUpdate toRemove, ModuleUpdate toReplace ) {
        Collection result = new ArrayList();
        checkReverseDependencies( toRemove.getRemoteModule(), result, toReplace );
        return result;
    }

    /** Builds Collection with modules which should be added
     * into download list to satisfy module dependencies.
     */
    private boolean checkDependencies( ModuleInfo md, Collection result, Enumeration selected, List group, 
            StringBuffer dontAddModuleName, boolean isUpgrade ) {

        // Get all module dependencies
        Set depsS = md.getDependencies();
        Dependency[] deps = (Dependency[])depsS.toArray(new Dependency[depsS.size()]);
        // Array values say if the dependency is satisfied or not
        boolean[] satisfied = new boolean [ deps.length ];

        // All installed modules
        ModuleInfo[] installedModules = Updates.getInstalledModules();
        ModuleInfo[] installedPatches = Updates.getInstalledPatches();

        // For all dependencies
        for ( int j = 0; j < deps.length; j++ ) {
            
            // The module depends on other module
            if (deps[j].getType() == Dependency.TYPE_MODULE || deps[j].getType() == Dependency.TYPE_REQUIRES) {

                boolean ok = false;

                // Try to figure out if the dependency is satisfied by installed modules
                for (int i = 0; i < installedModules.length; i++) {
                    ok = checkModuleDependency ( deps[j], installedModules[i] );

                    if ( ok )
                        break;
                }

                if ( !ok ) {
                    // Try to figure out if the dependency is satisfied by installed patches
                    for (int i = 0; i < installedPatches.length; i++) {
                        ok = checkModuleDependency ( deps[j], installedPatches[i] );
                        if ( ok )
                            break;
                    }
                }
                
                // Dependency was not satisfied by other module let's try modules
                // available for download
                if ( !ok && selected != null ) {

                    while ( selected.hasMoreElements() ) {
                        ModuleUpdate mu = (ModuleUpdate)selected.nextElement();
                        if (mu instanceof L10NUpdate) {
                            continue;
                        }

                        ok = checkModuleDependency ( deps[j], mu.getRemoteModule() );

                        if ( ok ) {
                            if ( !result.contains( mu ) ) {
                                err.log ("  ADDED[SELECTED]: " + mu.getCodeNameBase () + "[L10N? " + (mu instanceof L10NUpdate) + "]: DEP: " + deps [j]);
                                result.add( mu );
                            }
                            break;
                        }
                    }
                }
                
                if ( !ok && group != null ) {

                    Iterator it = group.iterator();
                    while ( it.hasNext() ) {
                        ModuleUpdate mu = (ModuleUpdate)it.next();
                        if (mu instanceof L10NUpdate) {
                            continue;
                        }


                        ok = checkModuleDependency ( deps[j], mu.getRemoteModule() );
                        if ( ok ) {
                            if ( !result.contains( mu ) ) {
                                err.log ("  ADDED[GROUP]: " + mu.getCodeNameBase () + "[L10N? " + (mu instanceof L10NUpdate) + "]: DEP: " + deps [j]);
                                result.add( mu );
                            }
                            break;
                        }
                    }
                }
                
                if ( !ok ) {

                    Iterator it = Wizard.getAllModules().iterator();
                    while ( it.hasNext() ) {
                        ModuleUpdate mu = (ModuleUpdate)it.next();
                        if (mu instanceof L10NUpdate) {
                            continue;
                        }


                        ok = checkModuleDependency ( deps[j], mu.getRemoteModule() );

                        if ( ok ) {
                            if ( !result.contains( mu ) ) {
                                err.log ("  ADDED[ALL]: " + mu.getCodeNameBase () + "[L10N? " + (mu instanceof L10NUpdate) + "]: DEP: " + deps [j]);
                                result.add( mu );
                            }
                            break;
                        }
                    }
                }

                if ( !ok )
                    satisfied[j] = false;
                else
                    satisfied[j] = true;
            }
            // Module depends on specific version of IDE
            else if ( deps[j].getType() == Dependency.TYPE_IDE ) {
                // Try to figure out if the dependency is satisfied by installed ide
                if (  checkIdeDependency ( deps[j], IdeDescription.getIdeDescription() ) ) {
                    satisfied[j] = true;
                }
                else {
                    // Try to find suitable IDE between the modules
                    Iterator it = Wizard.getAllModules().iterator();
                    boolean ok = false;
                    while ( it.hasNext() ) {
                        ModuleUpdate mu = (ModuleUpdate)it.next();
                        if (mu instanceof L10NUpdate) {
                            continue;
                        }


                        ok = checkModuleDependency ( deps[j], mu.getRemoteModule() );
                        if ( ok ) {
                            if ( !result.contains( mu ) ) {
                                err.log ("  ADDED[GROUP]: " + mu.getCodeNameBase () + "[L10N? " + (mu instanceof L10NUpdate) + "]: DEP: " + deps [j]);
                                result.add( mu );
                            }
                            break;
                        }
                    }
                    satisfied[j] = ok;
                }
            }
            else // XXX what about Java/VM dependencies?!
                satisfied[j] = true;
        }
        
        StringBuffer sb = new StringBuffer( 280 );
        sb.append( getBundle( "MSG_NotSatisfied" ) + getBundle( "TXT_DependencyChecker_Module" ) + md.getCodeName() + " "); // NOI18N

        int notSatisfied = 0;

        // For all dependencies
        for ( int j = 0; j < deps.length; j++ ) {
            if ( !satisfied[j] ) {
                sb.append( deps[j] );
                notSatisfied++;
            }
        }
        
        StringBuffer sbbroken = null;
        if ( isUpgrade ) {
            // Checking, if upgrade of module doesn't break dependency of already
            // installed modules on it's certain implementation version
            List brokenlist = checkBrokenImplDependency( md, result );
            if ( brokenlist.size() > 0 ) {
                sbbroken = new StringBuffer( 280 );
                sbbroken.append( "MODULE :" + md.getCodeName() );        // NOI18N
                sbbroken.append( getBundle( "MSG_BadsList" ) );
                Iterator it = brokenlist.iterator();
                while ( it.hasNext() )
                    sbbroken.append( "\n" + ((ModuleInfo) it.next()).getCodeName() );  // NOI18N
                sbbroken.append( getBundle( "MSG_IncludeBadsAnyway" ) );
            }
        }
        
        if ( notSatisfied == 0 && sbbroken == null )
            return true;

        if ( notSatisfied > 0 ) {
            sb.append( getBundle( "MSG_IncludeAnyway" ) );
            NotifyDescriptor.Confirmation nd = new NotifyDescriptor.Confirmation(
                                              sb.toString(),
                                              NotifyDescriptor.YES_NO_OPTION,
                                              NotifyDescriptor.ERROR_MESSAGE );

            if ( ! DialogDisplayer.getDefault().notify( nd ).equals( NotifyDescriptor.YES_OPTION ) ) {
                dontAddModuleName.append( md.getDisplayName() );
                return false;
            }
        }
        if ( sbbroken != null ) {
            NotifyDescriptor.Confirmation nd = new NotifyDescriptor.Confirmation(
                                              sbbroken.toString(),
                                              NotifyDescriptor.YES_NO_OPTION,
                                              NotifyDescriptor.ERROR_MESSAGE );

            if ( ! DialogDisplayer.getDefault().notify( nd ).equals( NotifyDescriptor.YES_OPTION ) )
                dontAddModuleName.append( md.getDisplayName() );
        }

        return false;
    }


    /** Builds Collection with modules which should be removed
     * from download list to satisfy module dependencies.
     */
    boolean checkReverseDependencies( ModuleInfo module, Collection result, ModuleUpdate toReplace ) {

        //ArrayList dependentModules = new ArrayList();
        ModuleInfo[] installedModules = Updates.getInstalledModules();
        ModuleInfo[] installedPatches = Updates.getInstalledPatches();

        // All listed modules
        Iterator it = Wizard.getAllModules().iterator();
        while ( it.hasNext() ) {
            ModuleUpdate mu = (ModuleUpdate)it.next();


            //if ( !info.update() ) We have to check all modules
            //  continue;

            ModuleInfo md = mu.getRemoteModule();
            Set depsS = md.getDependencies();
            Dependency[] deps = (Dependency[])depsS.toArray(new Dependency[depsS.size()]);

            // All dependencies of module
            boolean moduleOk = true;
            for ( int j = 0; j < deps.length; j++ ) {



                if ( (deps[j].getType() == Dependency.TYPE_MODULE &&
                         (deps[j].getName() + "/").startsWith( module.getCodeNameBase() + "/" )) || // NOI18N
                     (deps[j].getType() == Dependency.TYPE_REQUIRES &&
                         Arrays.asList(module.getProvides()).contains(deps[j].getName())) ) {

                    boolean ok = false;

                    // Check if not satisfied by installed modules
                    for (int k = 0; k < installedModules.length; k++) {
                        ok = checkModuleDependency ( deps[j], installedModules[k] );
                        if ( ok )
                            break;
                    }
                    
                    // Check if not satisfied by replacing module
                    if ( !ok && toReplace != null )
                        ok = checkModuleDependency ( deps[j], toReplace.getRemoteModule() );

                    // Check if it is not stisfied by installed patches
                    if ( !ok )
                        for (int k = 0; k < installedPatches.length; k++) {
                            ok = checkModuleDependency ( deps[j], installedPatches[k] );
                            if ( ok )
                                break;
                        }


                    // The module has unsatisfied dependency we have to remove it
                    if ( !ok ) {
                        moduleOk = false;
                        break;
                    }
                }
                else if ( deps[j].getType() == Dependency.TYPE_IDE &&
                          // XXX how can this be true?!
                          deps[j].getName().equals( module.getCodeName() ) ) {

                    //Check if not satisfied by installed IDE
                    if ( !checkModuleDependency ( deps[j], IdeDescription.getIdeDescription() ) ) {
                        moduleOk = false;
                        break;
                    }
                }
            }

            if ( !moduleOk ) {
                if ( ! result.contains( mu ) ) {
                    result.add( mu );
                    //checkReverseDependencies( mu.getRemoteModule(), result );
                }
            }
        }

        return result.size() == 0;
    }


    /** Tests if the dependency on module is satisfied by the otherModule
     */
    static boolean checkModuleDependency ( Dependency dep,
                                    ModuleInfo otherModule ) {

        if (dep.getType() == Dependency.TYPE_REQUIRES) {
            return Arrays.asList(otherModule.getProvides()).contains(dep.getName());
        }

        String depName = dep.getName();

        if ( depName.equals (otherModule.getCodeName ())) {
            if ( dep.getComparison() == Dependency.COMPARE_ANY) {
                return true;
            }
            else if (dep.getComparison() == Dependency.COMPARE_SPEC) {
                    if (otherModule.getSpecificationVersion() == null)
                        return false;
                    else if (new SpecificationVersion(dep.getVersion()).compareTo(otherModule.getSpecificationVersion()) > 0)
                        return false;
                    else
                        return true;
            }
            else {
                // COMPARE_IMPL
                if (otherModule.getImplementationVersion () == null)
                    return false;
                else if (! otherModule.getImplementationVersion ().equals (dep.getVersion()))
                    return false;
                else
                    return true;
            }
        }

        int dash = depName.indexOf('-'); // NOI18N
        if (dash != -1) {
            // Ranged major release version, cf. #19714.
            int slash = depName.indexOf('/'); // NOI18N
            String cnb = depName.substring(0, slash);
            int relMin = Integer.parseInt(depName.substring(slash + 1, dash));
            int relMax = Integer.parseInt(depName.substring(dash + 1));
            if (cnb.equals(otherModule.getCodeNameBase()) &&
                    relMin <= otherModule.getCodeNameRelease() &&
                    relMax >= otherModule.getCodeNameRelease()) {
                if (dep.getComparison() == Dependency.COMPARE_ANY) {
                    return true;
                } else {
                    // COMPARE_SPEC; COMPARE_IMPL not allowed here
                    if (otherModule.getCodeNameRelease() > relMin) {
                        // Great, skip the spec version.
                        return true;
                    } else {
                        // As usual.
                        if (otherModule.getSpecificationVersion() == null)
                            return false;
                        else if (new SpecificationVersion(dep.getVersion()).compareTo(otherModule.getSpecificationVersion()) > 0)
                            return false;
                        else
                            return true;
                    }
                }
            }
        }

        return false;
    }


    /** Tests dependency on the IDE */
    boolean checkIdeDependency( Dependency dep,
                                ModuleInfo ide ) {

        String IDEName = ide.getCodeName();
        SpecificationVersion IDESpecVersion = ide.getSpecificationVersion();
        String IDEImplVersion = ide.getImplementationVersion();

        // Not equal names
        if ( !IDEName.equals ( dep.getName() ) )
            return false;
        //return ModuleDescription.getStringFormatted ("MSG_IDE_Name", name, IDEName); // NOI18N

            if ( dep.getComparison() == Dependency.COMPARE_SPEC ) {
                return new SpecificationVersion(dep.getVersion()).compareTo(IDESpecVersion) <= 0;
                // ? null : ModuleDescription.getStringFormatted ("MSG_IDE_Spec", version, IDESpecVersion); // NOI18N
            }
            else if ( dep.getComparison() == Dependency.COMPARE_IMPL ) {
                return dep.getVersion().equals (IDEImplVersion);
                // ? null : ModuleDescription.getStringFormatted ("MSG_IDE_Impl", version, IDEImplVersion); // NOI18N
            }
            else {
                // COMPARE_ANY
                throw new IllegalStateException("Cannot have COMPARE_ANY on IDE dependency"); // NOI18N
            }
    }
    
    private List checkBrokenImplDependency( ModuleInfo md, Collection result ) {
        ModuleInfo[] installedModules = Updates.getInstalledModules();
        
        List brokenlist = new ArrayList();
        Dependency dep, depR;
        for (int i = 0; i < installedModules.length; i++) {
            Iterator deps = installedModules[i].getDependencies().iterator();
            while ( deps.hasNext() ) {
                dep = (Dependency) deps.next();
                if ( dep.getName().equals (md.getCodeName ())
                        && dep.getComparison() == Dependency.COMPARE_IMPL ) {                          
                    if ( ! dep.getVersion().equals( md.getImplementationVersion() ) ) {
                        // Dependency ids broken, try to find if there is new version
                        // of module with proper implementation version
                        Iterator it = Wizard.getAllModules().iterator();
                        boolean found = false;
                        while ( it.hasNext() ) {
                            ModuleUpdate mu = (ModuleUpdate)it.next();
                            if ( mu.getRemoteModule().getCodeName().equals( installedModules[i].getCodeName() )
                                && mu.getRemoteModule().getSpecificationVersion().compareTo(
                                installedModules[i].getSpecificationVersion() ) > 0 )
                            {
                                boolean maybeOK = true;
                                Iterator depsR = mu.getRemoteModule().getDependencies().iterator();
                                while ( depsR.hasNext() ) {
                                    depR = (Dependency) depsR.next();
                                    if ( depR.getName().equals (md.getCodeName ())
                                            && depR.getComparison() == Dependency.COMPARE_IMPL
                                            && !depR.getVersion().equals( md.getImplementationVersion() ) )
                                    {
                                        maybeOK = false;
                                        break;
                                    }
                                }
                                if ( maybeOK ) {
                                    if ( !result.contains( mu ) ) {
                                        result.add( mu );
                                    }
                                    found = true;
                                    break;
                                }
                            }
                        }
                        if ( !found ) {
                            brokenlist.add( installedModules[i] );
                        }
                        break;
                    }
                    break;
                }
            }
        }

        return brokenlist;
    }
    
    static boolean checkPlatformDependency (ModuleInfo module) {
        Set/*<Dependency>*/ deps = module.getDependencies ();
        Iterator/*<Dependency>*/ it = deps.iterator ();
        //err.log ("do checkPlatformDependency of module " + module.getDisplayName ());
        boolean res = true;
        while (it.hasNext ()) {
            Dependency d = (Dependency) it.next ();
            if (Dependency.TYPE_REQUIRES == d.getType ()) {
                //err.log ("Dependency: NAME: " + d.getName () + ", TYPE: " + d.getType () + ": " + d.toString ());
                if (d.getName ().startsWith ("org.openide.modules.os")) { // NOI18N
                    res = false;
                    ModuleInfo[] installedModules = Updates.getInstalledModules ();
                    for (int i = 0; ! res && (i < installedModules.length); i++) {
                        res = checkModuleDependency (d, installedModules [i]);
                    }
                    err.log ("checkPlatformDependency on module " + module.getCodeNameBase () + " which requires " + d + " returns " + res);
                }
            }
        }
        //err.log ("done checkPlatformDependency. returns " + res);
        return res;
    }
    
    private static boolean checkFreeLocalizationDependency (ModuleUpdate baseModule, String locale, Collection result, Enumeration selected, List group) {
        boolean ok = false;
        
        if (baseModule instanceof L10NUpdate) {
            // can ignore; useless find a localization for L10NUpdate
            return false;
        }
        
        if (selected != null) {

            while ( selected.hasMoreElements() ) {
                ModuleUpdate mu = (ModuleUpdate) selected.nextElement ();
                if (mu instanceof L10NUpdate) {
                    L10NUpdate lu = (L10NUpdate) mu;

                    ok = locale.equals (lu.getLangcode ()) && lu.getCodeNameBase ().equals (baseModule.getCodeNameBase ());

                    if ( ok ) {
                        if ( !result.contains (lu) ) {
                            err.log ("  ADDED[SELECTED]: " + lu.getCodeNameBase () + "[" + lu.getLangcode () + "]");
                            result.add (lu);
                        }
                        break;
                    }
                }
            }
        }

        if ( !ok && group != null ) {
            Iterator it = group.iterator();
            while ( it.hasNext() ) {
                ModuleUpdate mu = (ModuleUpdate)it.next();
                if (mu instanceof L10NUpdate) {
                    L10NUpdate lu = (L10NUpdate) mu;

                    ok = locale.equals (lu.getLangcode ()) && lu.getCodeNameBase ().equals (baseModule.getCodeNameBase ());

                    if ( ok ) {
                        if ( !result.contains (lu) ) {
                            err.log ("  ADDED[GROUP]: " + lu.getCodeNameBase () + "[" + lu.getLangcode () + "]");
                            result.add (lu);
                        }
                        break;
                    }
                }
            }
        }

        if ( !ok ) {

            Iterator it = Wizard.getAllModules().iterator();
            while ( it.hasNext() ) {
                ModuleUpdate mu = (ModuleUpdate)it.next();
                if (mu instanceof L10NUpdate) {
                    L10NUpdate lu = (L10NUpdate) mu;

                    ok = locale.equals (lu.getLangcode ()) && lu.getCodeNameBase ().equals (baseModule.getCodeNameBase ());

                    if ( ok ) {
                        if ( !result.contains (lu) ) {
                            err.log ("  ADDED[ALL]: " + lu.getCodeNameBase () + "[" + lu.getLangcode () + "]");
                            result.add (lu);
                        }
                        break;
                    }
                }
            }
        }

        return ok;
    }
    
    private String getBundle( String key ) {
        return NbBundle.getMessage( DependencyChecker.class, key );
    }
    
    static Comparator getModuleDependencyComparator() {
        return new ModuleDependencyComparator();
    }
    
    static class ModuleDependencyComparator implements Comparator {
        
        public int compare(Object obj, Object obj1) {
             if ( ! ( ( obj instanceof ModuleUpdate ) && ( obj1 instanceof ModuleUpdate ) ) )
                return 0;
            ModuleInfo modA  = ((ModuleUpdate)obj).getRemoteModule();
            ModuleInfo modB  = ((ModuleUpdate)obj1).getRemoteModule();

            if ( moduleDependsOnModule( modA, modB ) )
                return 1;
            else if ( moduleDependsOnModule( modB, modA ) )
                return -1;
            else
                return 0;
        }
        
        // returns true if modA depends on modB
        private boolean moduleDependsOnModule(ModuleInfo modA, ModuleInfo modB) {
            Set depsA = modA.getDependencies();
            Iterator it = depsA.iterator();
            while ( it.hasNext() ) {
                Dependency dep = (Dependency)(it.next());
                if (dep.getType() == Dependency.TYPE_MODULE || dep.getType() == Dependency.TYPE_REQUIRES) {
                    if ( checkModuleDependency ( dep, modB ) )
                        return true;
                }
            }
            return false;
        }
        
    }
}
