/*
 * 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 org.netbeans.modules.autoupdate.updateprovider.NativeComponentItem;
import org.netbeans.modules.autoupdate.updateprovider.FeatureItem;
import org.netbeans.modules.autoupdate.updateprovider.UpdateItemImpl;
import org.netbeans.modules.autoupdate.updateprovider.LocalizationItem;
import org.netbeans.modules.autoupdate.updateprovider.InstalledModuleItem;
import org.netbeans.modules.autoupdate.updateprovider.ModuleItem;
import org.netbeans.modules.autoupdate.updateprovider.InstalledModuleProvider;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
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.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.autoupdate.UpdateElement;
import org.netbeans.api.autoupdate.UpdateManager;
import org.netbeans.api.autoupdate.UpdateUnit;
import org.netbeans.api.autoupdate.UpdateUnitProvider;
import org.netbeans.modules.autoupdate.updateprovider.BackupUpdateProvider;
import org.netbeans.modules.autoupdate.updateprovider.InstalledUpdateProvider;
import org.netbeans.spi.autoupdate.UpdateItem;
import org.netbeans.spi.autoupdate.UpdateProvider;
import org.openide.modules.Dependency;
import org.openide.modules.ModuleInfo;
import org.openide.util.NbBundle;

/**
 *
 * @author Jiri Rechtacek
 */
public class UpdateUnitFactory {
    
    /** Creates a new instance of UpdateItemFactory */
    private UpdateUnitFactory () {}
    
    private static final UpdateUnitFactory INSTANCE = new UpdateUnitFactory ();
    private final Logger log = Logger.getLogger (this.getClass ().getName ());
    private static final DateFormat FMT = new SimpleDateFormat ("mm:ss:SS"); // NOI18N
    private static long runTime = -1;
    private Set<String> scheduledForRestartUE = null;
    private Set<String> scheduledForRestartUU = null;
    
    public static final String UNSORTED_CATEGORY = NbBundle.getMessage (UpdateUnitFactory.class, "UpdateUnitFactory_Unsorted_Category");
    public static final String LIBRARIES_CATEGORY = NbBundle.getMessage (UpdateUnitFactory.class, "UpdateUnitFactory_Libraries_Category");
    public static final String BRIDGES_CATEGORY = NbBundle.getMessage (UpdateUnitFactory.class, "UpdateUnitFactory_Bridges_Category");
    
    public static UpdateUnitFactory getDefault () {
        return INSTANCE;
    }
    
    public Map<String, UpdateUnit> getUpdateUnits () {
        //TODO: this call should be forced not to be called from AWT
        //assert !SwingUtilities.isEventDispatchThread();
        resetRunTime ("Measuring of UpdateUnitFactory.getUpdateUnits()"); // NOI18N
        List<UpdateUnitProvider> updates = UpdateUnitProviderImpl.getUpdateUnitProviders (true);
        
//        // loop installed modules
//        SortedSet<String> unmarked = new TreeSet<String> ();
//        SortedSet<String> markedTrue = new TreeSet<String> ();
//        SortedSet<String> markedFalse = new TreeSet<String> ();
//        for (ModuleInfo moduleInfo : InstalledModuleProvider.getInstalledModules ().values ()) {
//            Object f = moduleInfo.getAttribute (ATTR_VISIBLE);
//            if (f == null) {
//                unmarked.add (moduleInfo.getCodeName ());
//            } else if (Boolean.parseBoolean (f.toString ())) {
//                markedTrue.add (moduleInfo.getCodeName ());
//            } else {
//                markedFalse.add (moduleInfo.getCodeName ());
//            }
//        }
//        System.out.println("###### SIZE OF UNMARKED MODULES ? " + unmarked.size ());
//        System.out.println("###### SIZE OF VISIBLE MODULES ? " + markedTrue.size ());
//        System.out.println("###### SIZE OF HIDDEN MODULES ? " + markedFalse.size ());
        
        try {
            InstalledModuleProvider.getDefault().getUpdateItems();
        } catch (Exception x) {
            x.printStackTrace();
        }
        reportRunTime ("Get all installed modules.");
        
        // append installed units
        Map<String, UpdateUnit> mappedImpl = appendUpdateItems (
                new HashMap<String, UpdateUnit> (),
                InstalledModuleProvider.getDefault());
        reportRunTime ("Append installed units.");

        for (UpdateUnitProvider up : updates) {
            UpdateUnitProviderImpl impl = Trampoline.API.impl (up);
            
            // append units from provider
            mappedImpl = appendUpdateItems (mappedImpl, impl.getUpdateProvider ());
            reportRunTime ("AppendUpdateItems for " + impl.getUpdateProvider ().getDisplayName ());
        }

        return mappedImpl;
    }
    
    public Map<String, UpdateUnit> getUpdateUnits (UpdateProvider provider) {
        //TODO: this call should be forced not to be called from AWT
        //assert !SwingUtilities.isEventDispatchThread();
        resetRunTime ("Measuring UpdateUnitFactory.getUpdateUnits (" + provider.getDisplayName () + ")"); // NOI18N
        
        // append units from provider
        Map<String, UpdateUnit> temp = appendUpdateItems (new HashMap<String, UpdateUnit> (), provider);
        reportRunTime ("Get appendUpdateItems for " + provider.getDisplayName ());
        
        Map<String, UpdateUnit> retval = new HashMap<String, UpdateUnit>();
        for (UpdateUnit unit : temp.values ()) {
            retval.put (unit.getCodeName (), mergeInstalledUpdateUnit (unit));
        }
        reportRunTime ("Get filltering by " + provider.getDisplayName ());
        
        return temp;
    }
    
    Map<String, UpdateUnit> appendUpdateItems (Map<String, UpdateUnit> originalUnits, UpdateProvider provider) {
        assert originalUnits != null : "Map of original UnitImpl cannot be null";

        Map<String, UpdateItem> items;
        try {
            items = provider.getUpdateItems ();
        } catch (IOException ioe) {
            log.log (Level.INFO, "Cannot read UpdateItem from UpdateProvider " + provider, ioe);
            return originalUnits;
        }
        
        assert items != null : "UpdateProvider[" + provider.getName () + "] should return non-null items.";
        
        // append updates
        for (String simpleItemId : items.keySet ()) {
            
            UpdateElement updateEl = null;
            try {

                // create UpdateItemImpl
                UpdateItemImpl itemImpl = Trampoline.SPI.impl (items.get (simpleItemId));

                boolean isKitModule = false;
                if (itemImpl instanceof ModuleItem) {
                    ModuleInfo mi = ((ModuleItem) itemImpl).getModuleInfo ();
                    assert mi != null : "ModuleInfo must be found for " + itemImpl;
                    isKitModule = Utilities.isKitModule (mi);
                }
                if (itemImpl instanceof InstalledModuleItem) {
                    if (isKitModule) {
                        KitModuleUpdateElementImpl impl = new KitModuleUpdateElementImpl ((InstalledModuleItem) itemImpl, null);
                        updateEl = Trampoline.API.createUpdateElement (impl);
                    } else {
                        ModuleUpdateElementImpl impl = new ModuleUpdateElementImpl ((InstalledModuleItem) itemImpl, null);
                        updateEl = Trampoline.API.createUpdateElement (impl);
                    }
                } else if (itemImpl instanceof ModuleItem) {
                    if (isKitModule) {
                        KitModuleUpdateElementImpl impl = new KitModuleUpdateElementImpl ((ModuleItem) itemImpl, provider.getDisplayName ());
                        updateEl = Trampoline.API.createUpdateElement (impl);
                    } else {
                        ModuleUpdateElementImpl impl = new ModuleUpdateElementImpl ((ModuleItem) itemImpl, provider.getDisplayName ());
                        updateEl = Trampoline.API.createUpdateElement (impl);
                    }
                } else if (itemImpl instanceof LocalizationItem) {
                    updateEl = Trampoline.API.createUpdateElement (new LocalizationUpdateElementImpl ((LocalizationItem) itemImpl, provider.getDisplayName ()));
                } else if (itemImpl instanceof NativeComponentItem) {
                    updateEl = Trampoline.API.createUpdateElement (new NativeComponentUpdateElementImpl ((NativeComponentItem) itemImpl, provider.getDisplayName ()));
                } else if (itemImpl instanceof FeatureItem) {
                    FeatureUpdateElementImpl impl = new FeatureUpdateElementImpl.Agent (
                            (FeatureItem) itemImpl,
                            provider.getDisplayName (),
                            UpdateManager.TYPE.FEATURE);
                    updateEl = Trampoline.API.createUpdateElement (impl);
                } else {
                    assert false : "Unknown type of UpdateElement " + updateEl;
                }

            } catch (IllegalArgumentException iae) {
                log.log (Level.INFO, iae.getLocalizedMessage (), iae);
            }

            // add element to map
            if (updateEl != null) {
                addElement (originalUnits, updateEl, provider);
            }
        }
        
        return originalUnits;
    }
    
    private void addElement (Map<String, UpdateUnit> impls, UpdateElement element, UpdateProvider provider) {
        // find if corresponding element exists
        UpdateUnit unit = impls.get (element.getCodeName ());
        
        // XXX: it's should be moved in UI what should filter all elements w/ broken dependencies
        // #101515: Plugin Manager must filter updates by platform dependency
        boolean passed = false;
        UpdateElementImpl elImpl = Trampoline.API.impl (element);
        if (elImpl instanceof ModuleUpdateElementImpl && elImpl.getModuleInfos () != null && elImpl.getModuleInfos ().size() == 1) {
            for (Dependency d : elImpl.getModuleInfos ().get (0).getDependencies ()) {
                if (Dependency.TYPE_REQUIRES == d.getType ()) {
                    //log.log (Level.FINEST, "Dependency: NAME: " + d.getName () + ", TYPE: " + d.getType () + ": " + d.toString ());
                    if (d.getName ().startsWith ("org.openide.modules.os")) { // NOI18N
                        for (ModuleInfo info : InstalledModuleProvider.getInstalledModules ().values ()) {
                            if (Arrays.asList (info.getProvides ()).contains (d.getName ())) {
                                log.log (Level.FINEST, element + " which requires OS " + d + " succeed.");
                                passed = true;
                                break;
                            }
                        }
                        if (! passed) {
                            log.log (Level.FINE, element + " which requires OS " + d + " fails.");
                            return ;
                        }
                    }
                }
            }
        }
        
        UpdateUnitImpl unitImpl = null;
        
        if (unit == null) {
            switch (elImpl.getType ()) {
            case MODULE :
                unitImpl = new ModuleUpdateUnitImpl (element.getCodeName ());
                break;
            case KIT_MODULE :
                unitImpl = new KitModuleUpdateUnitImpl (element.getCodeName ());
                break;
            case STANDALONE_MODULE :
            case FEATURE :
                unitImpl = new FeatureUpdateUnitImpl (element.getCodeName (), elImpl.getType ());
                break;
            case CUSTOM_HANDLED_COMPONENT :
                unitImpl = new NativeComponentUpdateUnitImpl (element.getCodeName ());
                break;
            case LOCALIZATION :
                unitImpl = new LocalizationUpdateUnitImpl (element.getCodeName ());
                break;
            default:
                assert false : "Unsupported for type " + elImpl.getType ();
            }
            unit = Trampoline.API.createUpdateUnit (unitImpl);
            impls.put (unit.getCodeName (), unit);
        } else {
            unitImpl = Trampoline.API.impl (unit);
        }
        
        if (provider == InstalledUpdateProvider.getDefault()) {
            if (unitImpl.getInstalled () == null) {
                unitImpl.setInstalled (element);
            }
        } else if (provider instanceof BackupUpdateProvider) {
            unitImpl.setBackup (element);
        } else {
            // suppose common UpdateProvider
            unitImpl.addUpdate (element);
        }
        
        // set UpdateUnit into element
        elImpl.setUpdateUnit (unit);
        
    }
    
    private UpdateUnit mergeInstalledUpdateUnit (UpdateUnit uu) {
        UpdateUnit fromCache = UpdateManagerImpl.getInstance ().getUpdateUnit (uu.getCodeName ());
        if (fromCache != null && fromCache.getInstalled () != null) {
            UpdateUnitImpl impl = Trampoline.API.impl (uu);
            impl.setInstalled (fromCache.getInstalled ());
        }
        return uu;
    }
    
    private void resetRunTime (String msg) {
        if (log.isLoggable (Level.FINE)) {
            if (msg != null) {
                log.log (Level.FINE, "|=== " + msg + " ===|"); // NOI18N
            }
        runTime = System.currentTimeMillis ();
        }
    }
    
    private void reportRunTime (String msg) {
        if (log.isLoggable (Level.FINE)) {
            if (msg != null) {
                log.log (Level.FINE, msg + " === " + FMT.format (new Date (System.currentTimeMillis () - runTime))); // NOI18N
            }
            resetRunTime (null);
        }
    }
    
    public void scheduleForRestart (UpdateElement el) {
        if (scheduledForRestartUE == null) {
            scheduledForRestartUE = new HashSet<String> ();
            scheduledForRestartUU = new HashSet<String> ();
        }
        scheduledForRestartUE.add (el.getCodeName () + "_" + el.getSpecificationVersion ()); // NOI18N
        scheduledForRestartUU.add (el.getCodeName ());
    }
    
    public boolean isScheduledForRestart (UpdateElement el) {
        return scheduledForRestartUE != null && scheduledForRestartUE.contains (el.getCodeName () + "_" + el.getSpecificationVersion ()); // NOI18N
    }
    
    public boolean isScheduledForRestart (UpdateUnit u) {
        return scheduledForRestartUU != null && scheduledForRestartUU.contains (u.getCodeName ());
    }
    
}
