/*
 * 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.javascript.karma.ui.logicalview;

import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import java.util.prefs.PreferenceChangeEvent;
import java.util.prefs.PreferenceChangeListener;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.event.ChangeListener;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.api.project.Project;
import org.netbeans.modules.javascript.karma.exec.KarmaServers;
import org.netbeans.modules.javascript.karma.exec.KarmaServersListener;
import org.netbeans.modules.javascript.karma.preferences.KarmaPreferences;
import org.netbeans.modules.javascript.karma.preferences.KarmaPreferencesValidator;
import org.netbeans.modules.javascript.karma.util.FileUtils;
import org.netbeans.modules.javascript.karma.util.KarmaUtils;
import org.netbeans.modules.web.clientproject.api.jstesting.JsTestingProviders;
import org.netbeans.modules.web.common.api.ValidationResult;
import org.netbeans.spi.project.ui.CustomizerProvider2;
import org.netbeans.spi.project.ui.support.NodeList;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.awt.DynamicMenuContent;
import org.openide.awt.Mnemonics;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.util.ChangeSupport;
import org.openide.util.ContextAwareAction;
import org.openide.util.HelpCtx;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.WeakListeners;
import org.openide.util.actions.CallableSystemAction;
import org.openide.util.actions.NodeAction;
import org.openide.util.actions.Presenter;
import org.openide.util.actions.SystemAction;
import org.openide.util.lookup.Lookups;

public class KarmaChildrenList implements NodeList<Node>, PreferenceChangeListener {

    static final Logger LOGGER = Logger.getLogger(KarmaChildrenList.class.getName());

    private final Project project;
    private final ChangeSupport changeSupport = new ChangeSupport(this);


    public KarmaChildrenList(Project project) {
        assert project != null;
        this.project = project;
    }

    @Override
    public List<Node> keys() {
        if (KarmaPreferences.isEnabled(project)) {
            return Collections.<Node>singletonList(KarmaNode.create(project));
        }
        return Collections.emptyList();
    }

    @Override
    public void addChangeListener(ChangeListener listener) {
        changeSupport.addChangeListener(listener);
    }

    @Override
    public void removeChangeListener(ChangeListener listener) {
        changeSupport.removeChangeListener(listener);
    }

    @Override
    public Node node(Node key) {
        return key;
    }

    @Override
    public void addNotify() {
        KarmaPreferences.addPreferenceChangeListener(project, WeakListeners.create(PreferenceChangeListener.class, this, KarmaPreferences.class));
    }

    @Override
    public void removeNotify() {
        // noop
    }

    @Override
    public void preferenceChange(PreferenceChangeEvent evt) {
        if (!KarmaPreferences.isDebug(project)) {
            // possibly close browser tab
            KarmaServers.getInstance().closeDebugUrl(project);
        }
        // possibly restart server
        if (KarmaServers.getInstance().isServerRunning(project)) {
            KarmaServers.getInstance().stopServer(project, false);
            if (KarmaPreferences.isEnabled(project)) {
                ValidationResult result = new KarmaPreferencesValidator()
                        .validate(project)
                        .getResult();
                if (result.isFaultless()) {
                    KarmaServers.getInstance().startServer(project);
                }
            }
        }
        changeSupport.fireChange();
    }

    //~ Inner classes

    private static final class KarmaNode extends AbstractNode implements KarmaServersListener {

        @StaticResource
        private static final String KARMA_ICON = "org/netbeans/modules/javascript/karma/ui/resources/karma.png"; // NOI18N
        @StaticResource
        private static final String WAITING_BADGE = "org/netbeans/modules/javascript/karma/ui/resources/waiting.png"; // NOI18N
        @StaticResource
        private static final String RUNNING_BADGE = "org/netbeans/modules/javascript/karma/ui/resources/running.png"; // NOI18N

        private final Project project;


        @NbBundle.Messages({
            "KarmaNode.displayName=Karma",
            "KarmaNode.description=Test Runner for JavaScript",
        })
        private KarmaNode(Project project) {
            super(Children.LEAF, Lookups.fixed(project));

            assert project != null;
            this.project = project;

            setName("Karma"); // NOI18N
            setDisplayName(Bundle.KarmaNode_displayName());
            setShortDescription(Bundle.KarmaNode_description());
            setIconBaseWithExtension(KARMA_ICON);
        }

        static KarmaNode create(Project project) {
            KarmaNode karmaNode = new KarmaNode(project);
            KarmaServers.getInstance().addKarmaServersListener(karmaNode);
            return karmaNode;
        }

        @Override
        public void destroy() throws IOException {
            KarmaServers.getInstance().removeKarmaServersListener(this);
            super.destroy();
        }

        @Override
        public Action[] getActions(boolean context) {
            return new Action[] {
                SystemAction.get(StartKarmaServerAction.class),
                SystemAction.get(StopKarmaServerAction.class),
                SystemAction.get(RestartKarmaServerAction.class),
                null,
                SystemAction.get(DebugKarmaServerAction.class),
                null,
                SystemAction.get(ActiveKarmaConfigAction.class),
                null,
                SystemAction.get(CustomizeKarmaAction.class),
            };
        }

        @Override
        public Image getIcon(int type) {
            return badgeIcon(super.getIcon(type));
        }

        @Override
        public Image getOpenedIcon(int type) {
            return badgeIcon(super.getOpenedIcon(type));
        }

        private Image badgeIcon(Image origImg) {
            Image badge = null;
            if (KarmaServers.getInstance().isServerStarting(project)) {
                badge = ImageUtilities.loadImage(WAITING_BADGE);
            } else if (KarmaServers.getInstance().isServerStarted(project)) {
                badge = ImageUtilities.loadImage(RUNNING_BADGE);
            }
            return badge != null ? ImageUtilities.mergeImages(origImg, badge, 15, 8) : origImg;
        }

        @Override
        public void serverStateChanged(Project project) {
            if (this.project.equals(project)) {
                fireIconChange();
                fireOpenedIconChange();
            }
        }

    }

    private static final class StartKarmaServerAction extends BaseNodeAction {

        public StartKarmaServerAction() {
        }

        @Override
        protected void performAction(Project project) {
            KarmaServers.getInstance().startServer(project);
        }

        @Override
        protected boolean enable(Project project) {
            return !KarmaServers.getInstance().isServerRunning(project);
        }

        @NbBundle.Messages("StartKarmaServerAction.name=Start")
        @Override
        public String getName() {
            return Bundle.StartKarmaServerAction_name();
        }

    }

    private static final class StopKarmaServerAction extends BaseNodeAction {

        public StopKarmaServerAction() {
        }

        @Override
        protected void performAction(Project project) {
            KarmaServers.getInstance().stopServer(project, false);
        }

        @Override
        protected boolean enable(Project project) {
            return KarmaServers.getInstance().isServerRunning(project);
        }

        @NbBundle.Messages("StopKarmaServerAction.name=Stop")
        @Override
        public String getName() {
            return Bundle.StopKarmaServerAction_name();
        }

    }

    private static final class RestartKarmaServerAction extends BaseNodeAction {

        public RestartKarmaServerAction() {
        }

        @Override
        protected void performAction(Project project) {
            KarmaServers.getInstance().restartServer(project);
        }

        @Override
        protected boolean enable(Project project) {
            return KarmaServers.getInstance().isServerRunning(project);
        }

        @NbBundle.Messages("RestartKarmaServerAction.name=Restart")
        @Override
        public String getName() {
            return Bundle.RestartKarmaServerAction_name();
        }

    }

    private static final class DebugKarmaServerAction extends CallableSystemAction implements ContextAwareAction {

        public DebugKarmaServerAction() {
        }

        @NbBundle.Messages("DebugKarmaServerAction.name=Debug")
        @Override
        public String getName() {
            return Bundle.DebugKarmaServerAction_name();
        }

        @Override
        public void performAction() {
            assert false;
        }

        @Override
        public HelpCtx getHelpCtx() {
            return HelpCtx.DEFAULT_HELP;
        }

        @NbBundle.Messages("DebugKarmaServerAction.browser.none=Confirm Karma debug properties first")
        @Override
        public Action createContextAwareInstance(Lookup actionContext) {

            final class DebugAction extends AbstractAction implements Presenter.Popup {

                private final Project project;


                private DebugAction(Lookup actionContext) {
                    project = actionContext.lookup(Project.class);
                    assert project != null : "Project expected in lookup: " + actionContext;
                    putValue(NAME, Bundle.DebugKarmaServerAction_name());
                }

                @Override
                public void actionPerformed(ActionEvent e) {
                    boolean originalDebug = KarmaPreferences.isDebug(project);
                    if (!originalDebug
                            && !KarmaPreferences.isDebugBrowserIdSet(project)) {
                        DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message(Bundle.DebugKarmaServerAction_browser_none()));
                        project.getLookup().lookup(CustomizerProvider2.class).showCustomizer(JsTestingProviders.CUSTOMIZER_IDENT, null);
                        return;
                    }
                    KarmaPreferences.setDebug(project, !originalDebug);
                }

                @Override
                public JMenuItem getPopupPresenter() {
                    JCheckBoxMenuItem debugMenuItem = new JCheckBoxMenuItem(this);
                    debugMenuItem.setSelected(KarmaPreferences.isDebug(project));
                    return debugMenuItem;
                }

            }
            return new DebugAction(actionContext);
        }

    }

    private static final class ActiveKarmaConfigAction extends CallableSystemAction implements ContextAwareAction {

        public ActiveKarmaConfigAction() {
        }

        @NbBundle.Messages("ActiveKarmaConfigAction.name=Set Configuration")
        @Override
        public String getName() {
            return Bundle.ActiveKarmaConfigAction_name();
        }

        @Override
        public void performAction() {
            assert false;
        }

        @Override
        public HelpCtx getHelpCtx() {
            return HelpCtx.DEFAULT_HELP;
        }

        @Override
        public Action createContextAwareInstance(final Lookup actionContext) {
            class ActiveConfigAction extends AbstractAction implements Presenter.Popup {
                @Override
                public void actionPerformed(ActionEvent e) {
                    assert false;
                }
                @Override
                public JMenuItem getPopupPresenter() {
                    return ConfigMenu.create(actionContext);
                }
            }
            return new ActiveConfigAction();
        }

    }

    private static final class ConfigMenu extends JMenu implements DynamicMenuContent, ActionListener {

        final Project project;


        private ConfigMenu(Lookup actionContext) {
            assert actionContext != null;
            project = actionContext.lookup(Project.class);
            assert project != null : "Project expected in lookup: " + actionContext;
        }

        public static ConfigMenu create(Lookup actionContext) {
            ConfigMenu configMenu = new ConfigMenu(actionContext);
            Mnemonics.setLocalizedText(configMenu, Bundle.ActiveKarmaConfigAction_name());
            return configMenu;
        }

        @Override
        public JComponent[] getMenuPresenters() {
            removeAll();
            // #238803
            File configDir = KarmaUtils.getKarmaConfigDir(project);
            List<File> configs = KarmaUtils.findKarmaConfigs(configDir);
            if (configs.isEmpty()) {
                configs = KarmaUtils.findJsFiles(configDir);
            }
            configs = FileUtils.sortFiles(configs);
            if (!configs.isEmpty()) {
                String activeConfig = KarmaPreferences.getConfig(project);
                for (final File config : configs) {
                    boolean selected = config.getAbsolutePath().equals(activeConfig);
                    JRadioButtonMenuItem configItem = new JRadioButtonMenuItem(config.getName(), selected);
                    configItem.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            KarmaPreferences.setConfig(project, config.getAbsolutePath());
                        }
                    });
                    add(configItem);
                }
            } else {
                setEnabled(false);
            }
            return new JComponent[] {this};
        }

        @Override
        public JComponent[] synchMenuPresenters(JComponent[] items) {
            // always rebuild submenu
            return getMenuPresenters();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            assert false;
        }

    }

    private static final class CustomizeKarmaAction extends BaseNodeAction {

        public CustomizeKarmaAction() {
        }

        @Override
        protected void performAction(Project project) {
            project.getLookup().lookup(CustomizerProvider2.class).showCustomizer(JsTestingProviders.CUSTOMIZER_IDENT, null);
        }

        @Override
        protected boolean enable(Project project) {
            return true;
        }

        @NbBundle.Messages("CustomizeKarmaAction.name=Properties")
        @Override
        public String getName() {
            return Bundle.CustomizeKarmaAction_name();
        }

    }

    private abstract static class BaseNodeAction extends NodeAction {

        protected abstract void performAction(Project project);

        protected abstract boolean enable(Project project);

        @Override
        protected final void performAction(Node[] activatedNodes) {
            Project project = getProject(activatedNodes);
            if (project == null) {
                LOGGER.fine("No project found -> no karma action performed");
                return;
            }
            performAction(project);
        }

        @Override
        protected final boolean enable(Node[] activatedNodes) {
            Project project = getProject(activatedNodes);
            if (project == null) {
                LOGGER.fine("No project found -> no karma action enabled");
                return false;
            }
            return enable(project);
        }

        @Override
        public HelpCtx getHelpCtx() {
            return null;
        }

        @Override
        protected boolean asynchronous() {
            return false;
        }

        @CheckForNull
        private Project getProject(Node[] activatedNodes) {
            if (activatedNodes.length != 1) {
                return null;
            }
            Node node = activatedNodes[0];
            return node.getLookup().lookup(Project.class);
        }

    }

}
