/*
 * Copyright (c) 2008, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 *
 */
package com.sun.hotspot.igv.filterwindow;

import com.sun.hotspot.igv.data.ChangedEvent;
import com.sun.hotspot.igv.data.ChangedListener;
import com.sun.hotspot.igv.filter.CustomFilter;
import com.sun.hotspot.igv.filter.Filter;
import com.sun.hotspot.igv.filter.FilterChain;
import com.sun.hotspot.igv.filter.FilterSetting;
import com.sun.hotspot.igv.filterwindow.actions.*;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.util.*;
import javax.swing.JComboBox;
import javax.swing.UIManager;
import javax.swing.border.Border;
import org.openide.DialogDisplayer;
import org.openide.ErrorManager;
import org.openide.NotifyDescriptor;
import org.openide.awt.Toolbar;
import org.openide.awt.ToolbarPool;
import org.openide.explorer.ExplorerManager;
import org.openide.explorer.ExplorerUtils;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.util.*;
import org.openide.util.actions.SystemAction;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;

/**
 * 
 * @author Thomas Wuerthinger
 */
public final class FilterTopComponent extends TopComponent implements LookupListener, ExplorerManager.Provider {

    private static FilterTopComponent instance;
    public static final String FOLDER_ID = "Filters";
    public static final String AFTER_ID = "after";
    public static final String ENABLED_ID = "enabled";
    public static final String PREFERRED_ID = "FilterTopComponent";
    private CheckListView view;
    private ExplorerManager manager;
    private FilterChain filterChain;
    private FilterChain sequence;
    private Lookup.Result<FilterChain> result;
    private JComboBox comboBox;
    private List<FilterSetting> filterSettings;
    private FilterSetting customFilterSetting = new FilterSetting("-- Custom --");
    private ChangedEvent<FilterTopComponent> filterSettingsChangedEvent;
    private ActionListener comboBoxActionListener = new ActionListener() {

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

    public ChangedEvent<FilterTopComponent> getFilterSettingsChangedEvent() {
        return filterSettingsChangedEvent;
    }

    public FilterChain getSequence() {
        return sequence;
    }

    public void updateSelection() {
        Node[] nodes = this.getExplorerManager().getSelectedNodes();
        int[] arr = new int[nodes.length];
        for (int i = 0; i < nodes.length; i++) {
            int index = sequence.getFilters().indexOf(((FilterNode) nodes[i]).getFilter());
            arr[i] = index;
        }
        view.showSelection(arr);
    }

    private void comboBoxSelectionChanged() {

        Object o = comboBox.getSelectedItem();
        if (o == null) {
            return;
        }
        assert o instanceof FilterSetting;
        FilterSetting s = (FilterSetting) o;

        if (s != customFilterSetting) {
            FilterChain chain = getFilterChain();
            chain.getChangedEvent().beginAtomic();
            List<Filter> toRemove = new ArrayList<>();
            for (Filter f : chain.getFilters()) {
                if (!s.containsFilter(f)) {
                    toRemove.add(f);
                }
            }
            for (Filter f : toRemove) {
                chain.removeFilter(f);
            }

            for (Filter f : s.getFilters()) {
                if (!chain.containsFilter(f)) {
                    chain.addFilter(f);
                }
            }

            chain.getChangedEvent().endAtomic();
            filterSettingsChangedEvent.fire();
        } else {
            this.updateComboBoxSelection();
        }

        SystemAction.get(RemoveFilterSettingsAction.class).setEnabled(comboBox.getSelectedItem() != this.customFilterSetting);
        SystemAction.get(SaveFilterSettingsAction.class).setEnabled(comboBox.getSelectedItem() == this.customFilterSetting);
    }

    private void updateComboBox() {
        comboBox.removeAllItems();
        comboBox.addItem(customFilterSetting);
        for (FilterSetting s : filterSettings) {
            comboBox.addItem(s);
        }

        this.updateComboBoxSelection();
    }

    public void addFilterSetting() {
        NotifyDescriptor.InputLine l = new NotifyDescriptor.InputLine("Name of the new profile:", "Filter Profile");
        if (DialogDisplayer.getDefault().notify(l) == NotifyDescriptor.OK_OPTION) {
            String name = l.getInputText();

            FilterSetting toRemove = null;
            for (FilterSetting s : filterSettings) {
                if (s.getName().equals(name)) {
                    NotifyDescriptor.Confirmation conf = new NotifyDescriptor.Confirmation("Filter profile \"" + name + "\" already exists, do you want to replace it?", "Filter");
                    if (DialogDisplayer.getDefault().notify(conf) == NotifyDescriptor.YES_OPTION) {
                        toRemove = s;
                        break;
                    } else {
                        return;
                    }
                }
            }

            if (toRemove != null) {
                filterSettings.remove(toRemove);
            }
            FilterSetting setting = createFilterSetting(name);
            filterSettings.add(setting);

            // Sort alphabetically
            Collections.sort(filterSettings, new Comparator<FilterSetting>() {

                @Override
                public int compare(FilterSetting o1, FilterSetting o2) {
                    return o1.getName().compareTo(o2.getName());
                }
            });

            updateComboBox();
        }
    }

    public boolean canRemoveFilterSetting() {
        return comboBox.getSelectedItem() != customFilterSetting;
    }

    public void removeFilterSetting() {
        if (canRemoveFilterSetting()) {
            Object o = comboBox.getSelectedItem();
            assert o instanceof FilterSetting;
            FilterSetting f = (FilterSetting) o;
            assert f != customFilterSetting;
            assert filterSettings.contains(f);
            NotifyDescriptor.Confirmation l = new NotifyDescriptor.Confirmation("Do you really want to remove filter profile \"" + f + "\"?", "Filter Profile");
            if (DialogDisplayer.getDefault().notify(l) == NotifyDescriptor.YES_OPTION) {
                filterSettings.remove(f);
                updateComboBox();
            }
        }
    }

    private FilterSetting createFilterSetting(String name) {
        FilterSetting s = new FilterSetting(name);
        FilterChain chain = this.getFilterChain();
        for (Filter f : chain.getFilters()) {
            s.addFilter(f);
        }
        return s;
    }

    private void updateComboBoxSelection() {
        List<Filter> filters = this.getFilterChain().getFilters();
        boolean found = false;
        for (FilterSetting s : filterSettings) {
            if (s.getFilterCount() == filters.size()) {
                boolean ok = true;
                for (Filter f : filters) {
                    if (!s.containsFilter(f)) {
                        ok = false;
                    }
                }

                if (ok) {
                    if (comboBox.getSelectedItem() != s) {
                        comboBox.setSelectedItem(s);
                    }
                    found = true;
                    break;
                }
            }
        }

        if (!found && comboBox.getSelectedItem() != customFilterSetting) {
            comboBox.setSelectedItem(customFilterSetting);
        }
    }

    private class FilterChildren extends Children.Keys<Filter> implements ChangedListener<CheckNode> {

        private HashMap<Filter, Node> nodeHash = new HashMap<>();

        @Override
        protected Node[] createNodes(Filter filter) {
            if (nodeHash.containsKey(filter)) {
                return new Node[]{nodeHash.get(filter)};
            }

            FilterNode node = new FilterNode(filter);
            node.getSelectionChangedEvent().addListener(this);
            nodeHash.put(filter, node);
            return new Node[]{node};
        }

        public FilterChildren() {
            sequence.getChangedEvent().addListener(new ChangedListener<FilterChain>() {

                @Override
                public void changed(FilterChain source) {
                    addNotify();
                }
            });

            setBefore(false);
        }

        @Override
        protected void addNotify() {
            setKeys(sequence.getFilters());
            updateSelection();
        }

        @Override
        public void changed(CheckNode source) {
            FilterNode node = (FilterNode) source;
            Filter f = node.getFilter();
            FilterChain chain = getFilterChain();
            if (node.isSelected()) {
                if (!chain.containsFilter(f)) {
                    chain.addFilter(f);
                }
            } else {
                if (chain.containsFilter(f)) {
                    chain.removeFilter(f);
                }
            }
            view.revalidate();
            view.repaint();
            updateComboBoxSelection();
        }
    }

    public FilterChain getFilterChain() {
        return filterChain;
    }

    private FilterTopComponent() {
        filterSettingsChangedEvent = new ChangedEvent<>(this);
        initComponents();
        setName(NbBundle.getMessage(FilterTopComponent.class, "CTL_FilterTopComponent"));
        setToolTipText(NbBundle.getMessage(FilterTopComponent.class, "HINT_FilterTopComponent"));
        //        setIcon(Utilities.loadImage(ICON_PATH, true));

        sequence = new FilterChain();
        filterChain = new FilterChain();
        initFilters();
        manager = new ExplorerManager();
        manager.setRootContext(new AbstractNode(new FilterChildren()));
        associateLookup(ExplorerUtils.createLookup(manager, getActionMap()));
        view = new CheckListView();

        ToolbarPool.getDefault().setPreferredIconSize(16);
        Toolbar toolBar = new Toolbar();
        Border b = (Border) UIManager.get("Nb.Editor.Toolbar.border"); //NOI18N
        toolBar.setBorder(b);
        comboBox = new JComboBox();
        toolBar.add(comboBox);
        this.add(toolBar, BorderLayout.NORTH);
        toolBar.add(SaveFilterSettingsAction.get(SaveFilterSettingsAction.class));
        toolBar.add(RemoveFilterSettingsAction.get(RemoveFilterSettingsAction.class));
        toolBar.addSeparator();
        toolBar.add(NewFilterAction.get(NewFilterAction.class));
        toolBar.add(RemoveFilterAction.get(RemoveFilterAction.class).createContextAwareInstance(this.getLookup()));
        toolBar.add(MoveFilterUpAction.get(MoveFilterUpAction.class).createContextAwareInstance(this.getLookup()));
        toolBar.add(MoveFilterDownAction.get(MoveFilterDownAction.class).createContextAwareInstance(this.getLookup()));
        this.add(view, BorderLayout.CENTER);

        filterSettings = new ArrayList<>();
        updateComboBox();

        comboBox.addActionListener(comboBoxActionListener);
        setChain(filterChain);
    }

    public void newFilter() {
        CustomFilter cf = new CustomFilter("My custom filter", "");
        if (cf.openInEditor()) {
            sequence.addFilter(cf);
            FileObject fo = getFileObject(cf);
            FilterChangedListener listener = new FilterChangedListener(fo, cf);
            listener.changed(cf);
            cf.getChangedEvent().addListener(listener);
        }
    }

    public void removeFilter(Filter f) {
        com.sun.hotspot.igv.filter.CustomFilter cf = (com.sun.hotspot.igv.filter.CustomFilter) f;

        sequence.removeFilter(cf);
        try {
            getFileObject(cf).delete();
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }

    }

    private static class FilterChangedListener implements ChangedListener<Filter> {

        private FileObject fileObject;
        private CustomFilter filter;

        public FilterChangedListener(FileObject fo, CustomFilter cf) {
            fileObject = fo;
            filter = cf;
        }

        @Override
        public void changed(Filter source) {
            try {
                if (!fileObject.getName().equals(filter.getName())) {
                    FileLock lock = fileObject.lock();
                    fileObject.move(lock, fileObject.getParent(), filter.getName(), "");
                    lock.releaseLock();
                    FileObject newFileObject = fileObject.getParent().getFileObject(filter.getName());
                    fileObject = newFileObject;
                }

                FileLock lock = fileObject.lock();
                OutputStream os = fileObject.getOutputStream(lock);
                try (Writer w = new OutputStreamWriter(os)) {
                    String s = filter.getCode();
                    w.write(s);
                }
                lock.releaseLock();

            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
    }

    public void initFilters() {
        FileObject folder = FileUtil.getConfigRoot().getFileObject(FOLDER_ID);
        FileObject[] children = folder.getChildren();

        List<CustomFilter> customFilters = new ArrayList<>();
        HashMap<CustomFilter, String> afterMap = new HashMap<>();
        Set<CustomFilter> enabledSet = new HashSet<>();
        HashMap<String, CustomFilter> map = new HashMap<>();

        for (final FileObject fo : children) {
            InputStream is = null;

            String code = "";
            FileLock lock = null;
            try {
                lock = fo.lock();
                is = fo.getInputStream();
                BufferedReader r = new BufferedReader(new InputStreamReader(is));
                String s;
                StringBuilder sb = new StringBuilder();
                while ((s = r.readLine()) != null) {
                    sb.append(s);
                    sb.append("\n");
                }
                code = sb.toString();
            } catch (FileNotFoundException ex) {
                Exceptions.printStackTrace(ex);
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            } finally {
                try {
                    is.close();
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
                lock.releaseLock();
            }

            String displayName = fo.getName();


            final CustomFilter cf = new CustomFilter(displayName, code);
            map.put(displayName, cf);

            String after = (String) fo.getAttribute(AFTER_ID);
            afterMap.put(cf, after);

            Boolean enabled = (Boolean) fo.getAttribute(ENABLED_ID);
            if (enabled != null && (boolean) enabled) {
                enabledSet.add(cf);
            }

            cf.getChangedEvent().addListener(new FilterChangedListener(fo, cf));

            customFilters.add(cf);
        }

        for (int j = 0; j < customFilters.size(); j++) {
            for (int i = 0; i < customFilters.size(); i++) {
                List<CustomFilter> copiedList = new ArrayList<>(customFilters);
                for (CustomFilter cf : copiedList) {

                    String after = afterMap.get(cf);

                    if (map.containsKey(after)) {
                        CustomFilter afterCf = map.get(after);
                        int index = customFilters.indexOf(afterCf);
                        int currentIndex = customFilters.indexOf(cf);

                        if (currentIndex < index) {
                            customFilters.remove(currentIndex);
                            customFilters.add(index, cf);
                        }
                    }
                }
            }
        }

        for (CustomFilter cf : customFilters) {
            sequence.addFilter(cf);
            if (enabledSet.contains(cf)) {
                filterChain.addFilter(cf);
            }
        }
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:initComponents
    private void initComponents() {

        setLayout(new java.awt.BorderLayout());

    }// </editor-fold>//GEN-END:initComponents
    // Variables declaration - do not modify//GEN-BEGIN:variables
    // End of variables declaration//GEN-END:variables
    /**
     * Gets default instance. Do not use directly: reserved for *.settings files only,
     * i.e. deserialization routines; otherwise you could get a non-deserialized instance.
     * To obtain the singleton instance, use {@link findInstance}.
     */
    public static synchronized FilterTopComponent getDefault() {
        if (instance == null) {
            instance = new FilterTopComponent();
        }
        return instance;
    }

    /**
     * Obtain the FilterTopComponent instance. Never call {@link #getDefault} directly!
     */
    public static synchronized FilterTopComponent findInstance() {
        TopComponent win = WindowManager.getDefault().findTopComponent(PREFERRED_ID);
        if (win == null) {
            ErrorManager.getDefault().log(ErrorManager.WARNING, "Cannot find Filter component. It will not be located properly in the window system.");
            return getDefault();
        }
        if (win instanceof FilterTopComponent) {
            return (FilterTopComponent) win;
        }
        ErrorManager.getDefault().log(ErrorManager.WARNING, "There seem to be multiple components with the '" + PREFERRED_ID + "' ID. That is a potential source of errors and unexpected behavior.");
        return getDefault();
    }

    @Override
    public int getPersistenceType() {
        return TopComponent.PERSISTENCE_ALWAYS;
    }

    @Override
    protected String preferredID() {
        return PREFERRED_ID;
    }

    @Override
    public ExplorerManager getExplorerManager() {
        return manager;
    }

    @Override
    public void componentOpened() {
        Lookup.Template<FilterChain> tpl = new Lookup.Template<>(FilterChain.class);
        result = Utilities.actionsGlobalContext().lookup(tpl);
        result.addLookupListener(this);
    }

    @Override
    public void componentClosed() {
        result.removeLookupListener(this);
        result = null;
    }

    @Override
    public void resultChanged(LookupEvent lookupEvent) {
        setChain(Utilities.actionsGlobalContext().lookup(FilterChain.class));
    }

    public void setChain(FilterChain chain) {
        updateComboBoxSelection();
    }

    private FileObject getFileObject(CustomFilter cf) {
        FileObject fo = FileUtil.getConfigRoot().getFileObject(FOLDER_ID + "/" + cf.getName());
        if (fo == null) {
            try {
                fo = FileUtil.getConfigRoot().getFileObject(FOLDER_ID).createData(cf.getName());
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        return fo;
    }

    @Override
    public boolean requestFocus(boolean temporary) {
        view.requestFocus();
        return super.requestFocus(temporary);
    }

    @Override
    protected boolean requestFocusInWindow(boolean temporary) {
        view.requestFocus();
        return super.requestFocusInWindow(temporary);
    }

    @Override
    public void requestActive() {
        super.requestActive();
        view.requestFocus();
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        super.writeExternal(out);

        out.writeInt(filterSettings.size());
        for (FilterSetting f : filterSettings) {
            out.writeUTF(f.getName());

            out.writeInt(f.getFilterCount());
            for (Filter filter : f.getFilters()) {
                CustomFilter cf = (CustomFilter) filter;
                out.writeUTF(cf.getName());
            }
        }

        CustomFilter prev = null;
        for (Filter f : this.sequence.getFilters()) {
            CustomFilter cf = (CustomFilter) f;
            FileObject fo = getFileObject(cf);
            if (getFilterChain().containsFilter(cf)) {
                fo.setAttribute(ENABLED_ID, true);
            } else {
                fo.setAttribute(ENABLED_ID, false);
            }

            if (prev == null) {
                fo.setAttribute(AFTER_ID, null);
            } else {
                fo.setAttribute(AFTER_ID, prev.getName());
            }

            prev = cf;
        }
    }

    public CustomFilter findFilter(String name) {
        for (Filter f : sequence.getFilters()) {

            CustomFilter cf = (CustomFilter) f;
            if (cf.getName().equals(name)) {
                return cf;
            }
        }

        return null;
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        super.readExternal(in);

        int filterSettingsCount = in.readInt();
        for (int i = 0; i < filterSettingsCount; i++) {
            String name = in.readUTF();
            FilterSetting s = new FilterSetting(name);
            int filterCount = in.readInt();
            for (int j = 0; j < filterCount; j++) {
                String filterName = in.readUTF();
                CustomFilter filter = findFilter(filterName);
                if (filter != null) {
                    s.addFilter(filter);
                }
            }

            filterSettings.add(s);
        }
        updateComboBox();
    }

    final static class ResolvableHelper implements Serializable {

        private static final long serialVersionUID = 1L;

        public Object readResolve() {
            return FilterTopComponent.getDefault();
        }
    }
}
