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

import org.netbeans.modules.css.visual.spi.Location;
import org.netbeans.modules.css.visual.spi.RuleHandle;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.dnd.DnDConstants;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.Action;
import javax.swing.JTree;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellRenderer;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.modules.css.lib.api.CssParserResult;
import org.netbeans.modules.css.model.api.Model;
import org.netbeans.modules.css.model.api.Rule;
import org.netbeans.modules.css.model.api.StyleSheet;
import org.netbeans.modules.css.visual.api.CssStylesTC;
import org.netbeans.modules.css.visual.api.RuleEditorController;
import org.netbeans.modules.css.visual.api.EditCSSRulesAction;
import org.netbeans.modules.parsing.api.ParserManager;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.web.common.api.WebUtils;
import org.openide.explorer.ExplorerManager;
import org.openide.explorer.ExplorerUtils;
import org.openide.explorer.view.BeanTreeView;
import org.openide.filesystems.FileObject;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.Lookup.Result;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.lookup.Lookups;
import org.openide.util.lookup.ProxyLookup;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;

/**
 *
 * @author marekfukala
 */
@NbBundle.Messages({})
public class DocumentViewPanel extends javax.swing.JPanel implements ExplorerManager.Provider {

    static RequestProcessor RP = new RequestProcessor();
    /**
     * Tree view showing the style sheet information.
     */
    private BeanTreeView treeView;
    /**
     * Explorer manager provided by this panel.
     */
    private ExplorerManager manager = new ExplorerManager();
    /**
     * Lookup of this panel.
     */
    private final PanelLookup lookup;
    private final Lookup cssStylesLookup;
    
    /**
     * A strong reference to the Lookup.Result must be kept! 
     * See {@link Result#addLookupListener(org.openide.util.LookupListener)}.
     */
    private final Result<FileObject> lookupFileObjectResult; 
    
    /**
     * Filter for the tree displayed in this panel.
     */
    private Filter filter = new Filter();
    private DocumentViewModel documentModel;
    private DocumentNode documentNode;
    private final EditCSSRulesAction createRuleAction;
    
    private final PropertyChangeListener RULE_EDITOR_CONTROLLER_LISTENER = new PropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent pce) {
            if (RuleEditorController.PropertyNames.RULE_SET.name().equals(pce.getPropertyName())) {
                final Rule rule = (Rule) pce.getNewValue();
                if (rule == null) {
                    setSelectedStyleSheet();
                    return ;
                }
                final Model model = rule.getModel();
                model.runReadTask(new Model.ModelTask() {
                    @Override
                    public void run(StyleSheet styleSheet) {
                        setSelectedRule(RuleHandle.createRuleHandle(rule));
                    }
                });
            }
        }
    };

    /**
     * Creates new form DocumentViewPanel
     */
    public DocumentViewPanel(Lookup cssStylesLookup) {
        this.cssStylesLookup = cssStylesLookup;

        createRuleAction = EditCSSRulesAction.getDefault();

        lookupFileObjectResult = cssStylesLookup.lookupResult(FileObject.class);
        lookupFileObjectResult.addLookupListener(new LookupListener() {
            @Override
            public void resultChanged(LookupEvent ev) {
                //current stylesheet changed
                try {
                    contextChanged();
                } catch(Throwable t) {
                    Exceptions.printStackTrace(t);
                }
            }
        });

        //listen on selected rule in the rule editor and set selected rule in the 
        //document view accordingly
        RuleEditorController controller = cssStylesLookup.lookup(RuleEditorController.class);
        controller.addRuleEditorListener(RULE_EDITOR_CONTROLLER_LISTENER);

        Lookup explorerLookup = ExplorerUtils.createLookup(getExplorerManager(), getActionMap());
        lookup = new PanelLookup(explorerLookup);        
        Result<Node> lookupResult = explorerLookup.lookupResult(Node.class);
        lookupResult.addLookupListener(new LookupListener() {
            @Override
            public void resultChanged(LookupEvent ev) {
                //selected node changed
                Node[] selectedNodes = manager.getSelectedNodes();
                Node selected = selectedNodes.length > 0 ? selectedNodes[0] : null;
                boolean empty = (selected == null);
                lookup.emptySelection(empty);
                if (!empty) {
                    RuleHandle ruleHandle = selected.getLookup().lookup(RuleHandle.class);
                    if (ruleHandle != null) {
                        if (!settingRule.get()) {
                            selectRuleInRuleEditor(ruleHandle);
                        }
                        CssStylesListenerSupport.fireRuleSelected(ruleHandle);
                    }
                    Location location = selected.getLookup().lookup(Location.class);
                    if (location != null) {
                        createRuleAction.setContext(location.getFile());
                    }

                }
            }
        });

        initComponents();

        initTreeView();

        //create toolbar
        CustomToolbar toolbar = new CustomToolbar();
        toolbar.addButton(filterToggleButton);
        toolbar.addLineSeparator();
        toolbar.addButton(createRuleToggleButton);

        northPanel.add(toolbar, BorderLayout.EAST);

        //add document listener to the filter text field 
        filterTextField.getDocument().addDocumentListener(new DocumentListener() {
            private void contentChanged() {
                filter.setPattern(filterTextField.getText());
            }

            @Override
            public void insertUpdate(DocumentEvent e) {
                contentChanged();
            }

            @Override
            public void removeUpdate(DocumentEvent e) {
                contentChanged();
            }

            @Override
            public void changedUpdate(DocumentEvent e) {
            }
        });

        setFilterVisible(true, false);
        filterToggleButton.setSelected(true);

        initializeNodes();

        contextChanged();
    }

    public Lookup getLookup() {
        return lookup;
    }
    
    /**
     * Select corresponding node in the document view tree upon change of the
     * rule editor's content.
     *
     * A. The RuleNode holds instances of Rule-s from the model instance which
     * was created as setContext(file) was called on the view panel. B. The
     * 'rule' argument here is from an up-to-date model.
     *
     */
    private void setSelectedRule(RuleHandle handle) {
        try {
            Node foundRuleNode = findLocation(manager.getRootContext(), handle);
            Node[] toSelect = foundRuleNode != null ? new Node[]{foundRuleNode} : new Node[0];
            manager.setSelectedNodes(toSelect);
        } catch (PropertyVetoException ex) {
            //no-op
        }
    }

    private void setSelectedStyleSheet() {
        try {
            Node styleSheetNode = findLocation(manager.getRootContext(), new Location(getContext()));
            //assert styleSheetNode != null;
            Node[] toSelect = styleSheetNode != null ? new Node[]{styleSheetNode} : new Node[0];
            manager.setSelectedNodes(toSelect);
        } catch (PropertyVetoException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

    private final AtomicReference<RuleHandle> ruleHandleToSelect = new AtomicReference<>();
    private final AtomicBoolean settingRule = new AtomicBoolean();

    /**
     * Select rule in rule editor upon user action in the document view.
     */
    private void selectRuleInRuleEditor(final RuleHandle handle) {
        ruleHandleToSelect.set(handle);
        RP.post(new Runnable() { 
            @Override
            public void run() {
                final RuleEditorController rec = cssStylesLookup.lookup(RuleEditorController.class);
                final AtomicReference<Rule> matched_rule_ref = new AtomicReference<>();

                FileObject file = handle.getFile();
                Source source = Source.create(file);
                try {
                    ParserManager.parse(Collections.singleton(source), new UserTask() {
                        @Override
                        public void run(ResultIterator resultIterator) throws Exception {
                            ResultIterator ri = WebUtils.getResultIterator(resultIterator, "text/css"); //NOI18N
                            if (ri != null) {
                                final CssParserResult result = (CssParserResult) ri.getParserResult();
                                final Model model = Model.getModel(result);
                                Rule rule = handle.getRule(model);
                                matched_rule_ref.set(rule);
                            }
                        }
                    });
                } catch (ParseException ex) {
                    Exceptions.printStackTrace(ex);
                }

                final Rule match = matched_rule_ref.get();
                if (match != null) {
                    EventQueue.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            if (ruleHandleToSelect.get() == handle) {
                                settingRule.set(true);
                                rec.setModel(match.getModel());
                                rec.setRule(match);
                                settingRule.set(false);
                            }
                        }
                    });
                }
            }
        });

    }

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

    private FileObject getContext() {
        return cssStylesLookup.lookup(FileObject.class);
    }

    /**
     * Called when the CssStylesPanel is activated for different file.
     */
    private void contextChanged() {
        final FileObject context = getContext();
        lookup.update(context);

        //update the action context
        createRuleAction.setContext(context);

        //dispose old model
        if (documentModel != null) {
            documentModel.dispose();
        }

        if (context == null) {
            documentModel = null;
        } else {
            documentModel = new DocumentViewModel(context);
        }

        updateTitle();

        RP.post(new Runnable() {
            @Override
            public void run() {
                documentNode.setModel(documentModel);
                setSelectedStyleSheet();
            }
        });

    }

    /** Determines whether the panel is active. */
    private boolean active;

    /**
     * Invoked when the panel is activated.
     */
    void activated() {
        this.active = true;
        updateTitle();
    }

    /**
     * Invoked when the panel is deactivated.
     */
    void deactivated() {
        this.active = false;
    }

    /**
     * Updates the title of the enclosing view.
     */
    void updateTitle() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                if (active) {
                    TopComponent tc = WindowManager.getDefault().findTopComponent("CssStylesTC"); // NOI18N
                    if(tc != null) {
                        FileObject fob = getContext();
                        if(fob != null) {
                            ((CssStylesTC)tc).setTitle(fob.getNameExt());
                        }
                    }
                }
            }
        });
    }
    

    /**
     * Initializes the tree view.
     */
    private void initTreeView() {
        treeView = new BeanTreeView() {
            {
                tree.setCellRenderer(createTreeCellRenderer(tree.getCellRenderer()));
            }

            @Override
            public void expandAll() {
                // The original expandAll() doesn't work for us as it doesn't
                // seem to wait for the calculation of sub-nodes.
                Node root = manager.getRootContext();
                expandAll(root);
                // The view attempts to scroll to the expanded node
                // and it does it with a delay. Hence, simple calls like
                // tree.scrollRowToVisible(0) have no effect (are overriden
                // later) => the dummy collapse and expansion attempts
                // to work around that and keep the root node visible.
                collapseNode(root);
                expandNode(root);
            }

            /**
             * Expands the whole sub-tree under the specified node.
             *
             * @param node root node of the sub-tree that should be expanded.
             */
            private void expandAll(Node node) {
                treeView.expandNode(node);
                for (Node subNode : node.getChildren().getNodes(true)) {
                    if (!subNode.isLeaf()) {
                        expandAll(subNode);
                    }
                }
            }
        };
        treeView.setAllowedDragActions(DnDConstants.ACTION_NONE);
        treeView.setAllowedDropActions(DnDConstants.ACTION_NONE);
        treeView.setRootVisible(false);
        add(treeView, BorderLayout.CENTER);
    }

    private void initializeNodes() {
        documentNode = new DocumentNode(documentModel, filter);
        Node root = new FakeRootNode<>(documentNode,
                new Action[]{});
        manager.setRootContext(root);
        treeView.expandAll();
    }

    /**
     * Finds a node that represents the specified location in a tree represented
     * by the given root node.
     *
     * @param root root of a tree to search.
     * @param rule rule to find.
     * @return node that represents the rule or {@code null}.
     */
    public static Node findLocation(Node root, Location location) {
        Location candidate = root.getLookup().lookup(Location.class);
        if (candidate != null && location.equals(candidate)) {
            return root;
        }
        for (Node node : root.getChildren().getNodes()) {
            Node result = findLocation(node, location);
            if (result != null) {
                return result;
            }
        }
        return null;
    }

    /**
     * Creates a cell renderer for the tree view.
     *
     * @param delegate delegating/original tree renderer.
     * @return call renderer for the tree view.
     */
    private TreeCellRenderer createTreeCellRenderer(final TreeCellRenderer delegate) {
        return new DefaultTreeCellRenderer() {
            @Override
            public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
                return delegate.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
            }
        };
    }

    private void setFilterVisible(boolean visible, boolean requestFocus) {
        northPanel.remove(filterTextField);
        if (visible) {
            //update the UI
            northPanel.add(filterTextField, BorderLayout.CENTER);
            //set the filter text to the node
            filter.setPattern(filterTextField.getText());

            if(requestFocus) {
                filterTextField.requestFocus();
            }
        } else {
            //just remove the filter text from the node, but keep it in the field
            //so next time it is opened it will contain the old value
            filter.setPattern(null);
        }
        northPanel.revalidate();
        northPanel.repaint();
    }

    static class PanelLookup extends ProxyLookup {
        private final Lookup base;
        private Project project;
        private boolean emptySelection = true;

        PanelLookup(Lookup base) {
            this.base = base;
        }
        
        void update(FileObject file) {
            project = (file == null) ? null : FileOwnerQuery.getOwner(file);
            update();
        }

        void emptySelection(boolean emptySelection) {
            if (this.emptySelection != emptySelection) {
                this.emptySelection = emptySelection;
                update();
            }
        }

        private void update() {
            if (emptySelection && (project != null)) {
                setLookups(Lookups.fixed(new AbstractNode(Children.LEAF, Lookups.fixed(project))));
            } else {
                setLookups(base);
            }
        }
    }

    /**
     * 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.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        createRuleToggleButton = new javax.swing.JToggleButton();
        filterToggleButton = new javax.swing.JToggleButton();
        filterTextField = new javax.swing.JTextField();
        northPanel = new javax.swing.JPanel();

        createRuleToggleButton.setAction(createRuleAction);
        createRuleToggleButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/netbeans/modules/css/visual/resources/newRule.png"))); // NOI18N
        org.openide.awt.Mnemonics.setLocalizedText(createRuleToggleButton, null);
        createRuleToggleButton.setToolTipText(org.openide.util.NbBundle.getMessage(DocumentViewPanel.class, "CreateRuleDialog.title")); // NOI18N
        createRuleToggleButton.setFocusable(false);
        createRuleToggleButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                createRuleToggleButtonActionPerformed(evt);
            }
        });

        filterToggleButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/netbeans/modules/css/visual/resources/find.png"))); // NOI18N
        org.openide.awt.Mnemonics.setLocalizedText(filterToggleButton, null);
        filterToggleButton.setToolTipText(org.openide.util.NbBundle.getMessage(DocumentViewPanel.class, "DocumentViewPanel.filterToggleButton.toolTipText")); // NOI18N
        filterToggleButton.setFocusable(false);
        filterToggleButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                filterToggleButtonActionPerformed(evt);
            }
        });

        filterTextField.setText(org.openide.util.NbBundle.getMessage(DocumentViewPanel.class, "DocumentViewPanel.filterTextField.text")); // NOI18N
        filterTextField.setToolTipText(org.openide.util.NbBundle.getMessage(DocumentViewPanel.class, "DocumentViewPanel.filterToggleButton.toolTipText")); // NOI18N

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

        northPanel.setLayout(new java.awt.BorderLayout());
        add(northPanel, java.awt.BorderLayout.NORTH);
    }// </editor-fold>//GEN-END:initComponents

    private void filterToggleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_filterToggleButtonActionPerformed
        setFilterVisible(filterToggleButton.isSelected(), true);
    }//GEN-LAST:event_filterToggleButtonActionPerformed

    private void createRuleToggleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_createRuleToggleButtonActionPerformed
        createRuleToggleButton.setSelected(false); //disable selected as it's a toggle button
    }//GEN-LAST:event_createRuleToggleButtonActionPerformed
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JToggleButton createRuleToggleButton;
    private javax.swing.JTextField filterTextField;
    private javax.swing.JToggleButton filterToggleButton;
    private javax.swing.JPanel northPanel;
    // End of variables declaration//GEN-END:variables
}
