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

import java.io.IOException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import org.netbeans.api.autoupdate.*;
import org.netbeans.api.autoupdate.InstallSupport.Installer;
import org.netbeans.api.autoupdate.InstallSupport.Validator;
import org.netbeans.api.autoupdate.OperationContainer.OperationInfo;
import org.netbeans.api.autoupdate.OperationSupport.Restarter;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.sendopts.CommandException;
import org.netbeans.spi.sendopts.Env;
import org.netbeans.spi.sendopts.Option;
import org.netbeans.spi.sendopts.OptionGroups;
import org.netbeans.spi.sendopts.OptionProcessor;
import org.openide.util.*;

import static org.netbeans.modules.autoupdate.cli.Bundle.*;
import org.netbeans.modules.progress.spi.Controller;
import org.netbeans.modules.progress.spi.InternalHandle;
import org.netbeans.modules.progress.spi.ProgressEvent;
import org.netbeans.modules.progress.spi.ProgressUIWorker;

/**
 *
 * @author Jaroslav Tulach, Jiri Rechtacek
 */
@org.openide.util.lookup.ServiceProvider(service=org.netbeans.spi.sendopts.OptionProcessor.class)
public class ModuleOptions extends OptionProcessor {
    private static final Logger LOG = Logger.getLogger(ModuleOptions.class.getName());
    
    private Option list;
    private Option install;
    private Option disable;
    private Option enable;
    private Option update;
    private Option refresh;
    private Option updateAll;
    private Option both;
    private Option extraUC;
    private Option directDisable;
    private final Collection<UpdateUnitProvider> ownUUP = new HashSet<> ();
    
    /** Creates a new instance of ModuleOptions */
    public ModuleOptions() {
    }

    @NbBundle.Messages({
        "MSG_UpdateModules=Updates all or specified modules",
        "MSG_UpdateAll=Updates all modules",
        "MSG_Refresh=Refresh all catalogs",
        "MSG_ExtraUC=Add a extra Update Center (URL)"
    })
    private Option init() {
        if (both != null) {
            return both;
        }

        String b = "org.netbeans.modules.autoupdate.cli.Bundle";
        list = Option.shortDescription(
            Option.withoutArgument(Option.NO_SHORT_NAME, "list"), b, "MSG_ListModules"); // NOI18N
        install = Option.shortDescription(
            Option.additionalArguments(Option.NO_SHORT_NAME, "install"), b, "MSG_InstallModules"); // NOI18N
        disable = Option.shortDescription(
            Option.additionalArguments(Option.NO_SHORT_NAME, "disable"), b, "MSG_DisableModules"); // NOI18N
        enable = Option.shortDescription(
            Option.additionalArguments(Option.NO_SHORT_NAME, "enable"), b, "MSG_EnableModules"); // NOI18N
        update = Option.shortDescription(
            Option.additionalArguments(Option.NO_SHORT_NAME, "update"), b, "MSG_UpdateModules"); // NOI18N
        refresh = Option.shortDescription(
            Option.withoutArgument(Option.NO_SHORT_NAME, "refresh"), b, "MSG_Refresh"); // NOI18N
        updateAll = Option.shortDescription(
            Option.withoutArgument(Option.NO_SHORT_NAME, "update-all"), b, "MSG_UpdateAll"); // NOI18N
        extraUC = Option.shortDescription(
            Option.requiredArgument(Option.NO_SHORT_NAME, "extra-uc"), b, "MSG_ExtraUC"); // NOI18N
        directDisable = Option.shortDescription(
            Option.additionalArguments(Option.NO_SHORT_NAME, "direct-disable"), b, "MSG_DirectDisableModule"); // NOI18N
        
        Option oper = OptionGroups.someOf(refresh, list, install, disable, enable, update, updateAll, extraUC, directDisable);
        Option modules = Option.withoutArgument(Option.NO_SHORT_NAME, "modules");
        both = OptionGroups.allOf(modules, oper);
        return both;
    }

    @Override
    public Set<Option> getOptions() {
        return Collections.singleton(init());
    }
    @NbBundle.Messages({
        "# {0} - update center name", 
        "MSG_RefreshingUpdateCenter=Refreshing {0}"
    })
    private void refresh(Env env) throws CommandException {
        for (UpdateUnitProvider p : UpdateUnitProviderFactory.getDefault().getUpdateUnitProviders(true)) {
            try {
                env.getOutputStream().println(MSG_RefreshingUpdateCenter(p.getDisplayName()));
                p.refresh(null, true);
            } catch (IOException ex) {
                throw (CommandException)new CommandException(31, ex.getMessage()).initCause(ex);
            }
        }
    }

    @NbBundle.Messages({
        "MSG_ListHeader_CodeName=Code Name",
        "MSG_ListHeader_Version=Version",
        "MSG_ListHeader_State=State"
    })
    private void listAllModules(PrintStream out) throws IOException {
        List<UpdateUnit> modules = UpdateManager.getDefault().getUpdateUnits();
        
        PrintTable table = new PrintTable(
            MSG_ListHeader_CodeName(), MSG_ListHeader_Version(), MSG_ListHeader_State()
        );
        table.setLimits(50, -1, -1);
        for (UpdateUnit uu : modules) {
            table.addRow(Status.toArray(uu));
        }
        StringBuilder sb = new StringBuilder();
        table.write(sb);
        out.print(sb.toString());
        out.flush();
    }

    private static <T extends Throwable> T initCause(T t, Throwable cause) {
        t.initCause(cause);
        return t;
    }

    @Override
    protected void process(Env env, Map<Option, String[]> optionValues) throws CommandException {
        try {
            try {
                if (optionValues.containsKey(extraUC)) {
                    extraUC(env, optionValues.get(extraUC));
                }
                if (optionValues.containsKey(refresh)) {
                    refresh(env);
                }

                if (optionValues.containsKey(list)) {
                    listAllModules(env.getOutputStream());
                }

                if (optionValues.containsKey(install)) {
                    install(env, optionValues.get(install));
                }

                if (optionValues.containsKey(disable)) {
                    changeModuleState(env, optionValues.get(disable), false, false);
                }

                if (optionValues.containsKey(enable)) {
                    changeModuleState(env, optionValues.get(enable), true, false);
                }

                if (optionValues.containsKey(directDisable)) {
                    changeModuleState(env, optionValues.get(directDisable), false, true);
                }
            } catch (InterruptedException | IOException | OperationException ex) {
                throw initCause(new CommandException(4), ex);
            }

            if (optionValues.containsKey(updateAll)) {
                updateAll(env);
            }
            if (optionValues.containsKey(update)) {
                updateModules(env, optionValues.get(update));
            }
        } finally {
            for (UpdateUnitProvider uuc : ownUUP) {
                UpdateUnitProviderFactory.getDefault().remove(uuc);
            }
        }
        
    }

    @NbBundle.Messages({
        "# {0} - name of the module",
        "MSG_FoundButNotInstalled=Found {0}, but not installed",
        "# {0} - requested patterns",
        "MSG_CannotFindAnyPattern=Cannot find any module matching {0}\n"
    })
    private void changeModuleState(Env env, String[] cnbs, boolean enable, boolean direct) throws IOException, CommandException, InterruptedException, OperationException {
        CodeNameMatcher cnm = CodeNameMatcher.create(env, cnbs);

        List<UpdateUnit> units = UpdateManager.getDefault().getUpdateUnits();
        OperationContainer<OperationSupport> operate = 
                enable ? OperationContainer.createForEnable()  :
                    (direct ? OperationContainer.createForDirectDisable() : OperationContainer.createForDisable());
        StringBuilder sb = new StringBuilder();
        boolean found = false;
        for (UpdateUnit updateUnit : units) {
            final String codeName = updateUnit.getCodeName();
            if (cnm.matches(codeName)) {
                final UpdateElement elem = updateUnit.getInstalled();
                if (elem != null) {
                    found = true;
                    if (elem.isEnabled() != enable) {
                        operate.add(updateUnit, elem);
                    }
                } else {
                    sb.append("\n").append(MSG_FoundButNotInstalled(codeName));
                }
            }
        }
        if (!found) {
            sb.insert(0, MSG_CannotFindAnyPattern(cnm));
            throw new CommandException(55, sb.toString());
        }
        OperationSupport support = operate.getSupport();
        if (support != null) {
            support.doOperation(null);
        }
    }

    @NbBundle.Messages({
        "MSG_UpdateNotFound=Updates not found.",
        "# {0} - pattern",
        "MSG_UpdateNoMatchPattern=Nothing to update. The pattern {0} has no match among available updates.",
        "# {0} - module name",
        "# {1} - installed version",
        "# {2} - available version",
        "MSG_Update=Will update {0}@{1} to version {2}",
        "# {0} - plugin name",
        "MSG_Download=Downloading {0}"
    })
    private void updateModules(final Env env, String... pattern) throws CommandException {
        if (! initialized()) {
            refresh(env);
        }
        CodeNameMatcher cnm = CodeNameMatcher.create(env, pattern);
        
        List<UpdateUnit> units = UpdateManager.getDefault().getUpdateUnits(UpdateManager.TYPE.MODULE);
        final Collection <String> firstClass = getFirstClassModules();
        boolean firstClassHasUpdates = false;
        OperationContainer<InstallSupport> operate = OperationContainer.createForUpdate();
        if (! firstClass.isEmpty() && pattern.length == 0) {
            for (UpdateUnit uu : units) {
                if (uu.getInstalled() == null) {
                    continue;
                }
                final List<UpdateElement> updates = uu.getAvailableUpdates();
                if (updates.isEmpty()) {
                    continue;
                }
                if (! firstClass.contains (uu.getCodeName ())) {
                    continue;
                }
                final UpdateElement ue = updates.get(0);
                env.getOutputStream().println(
                    Bundle.MSG_Update(uu.getCodeName(), uu.getInstalled().getSpecificationVersion(), ue.getSpecificationVersion()
                ));
                if (operate.canBeAdded(uu, ue)) {
                    LOG.log(Level.FINE, "  ... update {0} -> {1}", new Object[]{uu.getInstalled(), ue});
                    firstClassHasUpdates = true;
                    OperationInfo<InstallSupport> info = operate.add(ue);
                    if (info != null) {
                        Set<UpdateElement> requiredElements = info.getRequiredElements();
                        LOG.log(Level.FINE, "      ... add required elements: {0}", requiredElements);
                        operate.add(requiredElements);
                    }
                }
            }
        } 
        if (! firstClassHasUpdates) {
            for (UpdateUnit uu : units) {
                if (uu.getInstalled() == null) {
                    continue;
                }
                final List<UpdateElement> updates = uu.getAvailableUpdates();
                if (updates.isEmpty()) {
                    continue;
                }
                if (pattern.length > 0 && !cnm.matches(uu.getCodeName())) {
                    continue;
                }
                final UpdateElement ue = updates.get(0);
                env.getOutputStream().println(
                    Bundle.MSG_Update(uu.getCodeName(), uu.getInstalled().getSpecificationVersion(), ue.getSpecificationVersion()
                ));
                if (operate.canBeAdded(uu, ue)) {
                    LOG.log(Level.FINE, "  ... update {0} -> {1}", new Object[]{uu.getInstalled(), ue});
                    OperationInfo<InstallSupport> info = operate.add(ue);
                    if (info != null) {
                        Set<UpdateElement> requiredElements = info.getRequiredElements();
                        LOG.log(Level.FINE, "      ... add required elements: {0}", requiredElements);
                        operate.add(requiredElements);
                    }
                }
            }
        }
        final InstallSupport support = operate.getSupport();
        if (support == null) {
            env.getOutputStream().println(cnm.isEmpty() ? Bundle.MSG_UpdateNotFound() : Bundle.MSG_UpdateNoMatchPattern(cnm));
            env.getOutputStream().println("updates=0"); // NOI18N
            return;
        }
        env.getOutputStream().println("updates=" + operate.listAll().size()); // NOI18N
        ProgressHandle downloadHandle = new CLIInternalHandle("downloading-updates", env).createProgressHandle(); // NOI18N
        downloadHandle.setInitialDelay(0);
        try {
            final Validator res1 = support.doDownload(downloadHandle, null, false);

            Installer res2 = support.doValidate(res1, null);

            ProgressHandle installHandle = new CLIInternalHandle("installing-updates", env).createProgressHandle(); // NOI18N
            installHandle.setInitialDelay(0);
            Restarter res3 = support.doInstall(res2, installHandle);
            if (res3 != null) {
                support.doRestart(res3, null);
            }
        } catch (OperationException ex) {
            try {
                support.doCancel();
                throw (CommandException)new CommandException(33, ex.getMessage()).initCause(ex);
            } catch (OperationException ex1) {
                throw (CommandException)new CommandException(33, ex1.getMessage()).initCause(ex1);
            }
        }
    }

    @NbBundle.Messages({
        "# {0} - module name",
        "# {1} - module version",
        "MSG_Installing=Installing {0}@{1}",
        "# {0} - module name",
        "MSG_AlreadyPresent=Module {0} is already installed.",
    })
    private void install(final Env env, String... pattern) throws CommandException {
        if (! initialized()) {
            refresh(env);
        }

        CodeNameMatcher cnm = CodeNameMatcher.create(env, pattern);

        List<UpdateUnit> units = UpdateManager.getDefault().getUpdateUnits();
        OperationContainer<InstallSupport> operate = OperationContainer.createForInstall();
        boolean found = false;
        StringBuilder sb = new StringBuilder();
        
        
        for (UpdateUnit uu : units) {
            if (!cnm.matches(uu.getCodeName())) {
                continue;
            }
            found = true;
            if (uu.getInstalled() != null) {
                sb.append(MSG_AlreadyPresent(uu.getCodeName())).append("\n");
                continue;
            }
            if (uu.getAvailableUpdates().isEmpty()) {
                continue;
            }
            UpdateElement ue = uu.getAvailableUpdates().get(0);
            env.getOutputStream().println(
                    Bundle.MSG_Installing(uu.getCodeName(), ue.getSpecificationVersion()));
            operate.add(ue);
        }
        final InstallSupport support = operate.getSupport();
        if (support == null) {
            if (!found) {
                sb.insert(0, MSG_CannotFindAnyPattern(cnm));
                throw new CommandException(55, sb.toString());
            }
            env.getOutputStream().print(sb.toString());
            return;
        }
        try {
            env.getOutputStream().println("modules=" + operate.listAll().size()); // NOI18N
            ProgressHandle downloadHandle = new CLIInternalHandle("downloading-modules", env).createProgressHandle(); // NOI18N
            downloadHandle.setInitialDelay(0);
            final Validator res1 = support.doDownload(downloadHandle, null, false);

            Installer res2 = support.doValidate(res1, null);

            ProgressHandle installHandle = new CLIInternalHandle("installing-modules", env).createProgressHandle(); // NOI18N
            installHandle.setInitialDelay(0);
            Restarter res3 = support.doInstall(res2, installHandle);
            if (res3 != null) {
                support.doRestart(res3, null);
            }
        } catch (OperationException ex) {
            // a hack
            if (OperationException.ERROR_TYPE.INSTALL.equals(ex.getErrorType())) {
                // probably timeout of loading
                env.getErrorStream().println(ex.getLocalizedMessage());
                throw (CommandException) new CommandException(34, ex.getMessage()).initCause(ex);
            } else {
                try {
                    support.doCancel();
                    throw (CommandException) new CommandException(32, ex.getMessage()).initCause(ex);
                } catch (OperationException ex1) {
                    throw (CommandException) new CommandException(32, ex1.getMessage()).initCause(ex1);
                }
            }
        }
    }
    
    private void updateAll(Env env) throws CommandException {
        updateModules(env);
    }

    
    @NbBundle.Messages({
        "MSG_NoURL=None extra Update Center (URL) specified."
    })
    private void extraUC(Env env, String... urls) throws CommandException {
        List<URL> url2UC = new ArrayList<> (urls.length);
        for (String spec : urls) {
            try {
                url2UC.add(new URL(spec));
            } catch (MalformedURLException ex) {
                throw initCause(new CommandException(4), ex);
            }
        }
        for (URL url : url2UC) {
            ownUUP.add(UpdateUnitProviderFactory.getDefault().create(Long.toString(System.currentTimeMillis()), url.toExternalForm(), url));
        }
        refresh(env);        
    }

    private boolean initialized() {
        Preferences pref = NbPreferences.root ().node ("/org/netbeans/modules/autoupdate");
        long last = pref.getLong("lastCheckTime", -1);
        return last != -1;
    }    

    private static final String PLUGIN_MANAGER_FIRST_CLASS_MODULES = "plugin.manager.first.class.modules"; // NOI18N
    
    private Collection<String> getFirstClassModules() {
        Preferences p = NbPreferences.root().node("/org/netbeans/modules/autoupdate"); // NOI18N
        String names = p.get(PLUGIN_MANAGER_FIRST_CLASS_MODULES, "");
        Set<String> res = new HashSet<> ();
        StringTokenizer en = new StringTokenizer (names, ","); // NOI18N
        while (en.hasMoreTokens ()) {
            res.add (en.nextToken ().trim ());
        }
        return res;
    }
    
    private class CLIInternalHandle extends InternalHandle {
        public CLIInternalHandle(String displayName, Env env) {
            super(displayName, null, false);
            setController(new Controller(new CLIProgressUIWorker(env)));
        }
    }

    private class CLIProgressUIWorker implements ProgressUIWorker { 
        private final Env env;
        public CLIProgressUIWorker(Env env) {
            this.env = env;
        }

        @Override
        public void processProgressEvent(ProgressEvent event) {
            printEvent(event);
        }

        @Override
        public void processSelectedProgressEvent(ProgressEvent event) {
            printEvent(event);
        }

        private void printEvent(ProgressEvent event) {
            final String msg = event.getMessage();
            if (msg != null && msg.length() > 0) {
                env.getOutputStream().println(msg);
            }
        }
    }
}
