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

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyEditorSupport;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import javax.imageio.ImageIO;
import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.border.EtchedBorder;
import javax.swing.border.TitledBorder;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.queries.SourceForBinaryQuery;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.explorer.ExplorerManager;
import org.openide.explorer.propertysheet.ExPropertyEditor;
import org.openide.explorer.propertysheet.PropertyEnv;
import org.openide.explorer.view.BeanTreeView;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.FilterNode;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;

/**
 * PropertyEditor for Icons. Depends on existing DataObject for images.
 * Images must be represented by some DataObject which returns itselv
 * as cookie, and has image file as a primary file. File extensions
 * for images is specified in isImage method.
 *
 * @author Jan Jancura
 */
final class BiIconEditor extends PropertyEditorSupport implements ExPropertyEditor {
    
    private static final String BEAN_ICONEDITOR_HELP = "beans.icon"; // NOI18N
    
    private FileObject sourceFileObject;
    private PropertyEnv env;
    
    /** Standard variable for localization. */
    static java.util.ResourceBundle bundle = org.openide.util.NbBundle.getBundle(
    BiIconEditor.class);
    
    public static boolean isImage(String s) {
        s = s.toLowerCase();
        return s.endsWith(".jpg") || s.endsWith(".gif") || // NOI18N
        s.endsWith(".jpeg") || s.endsWith(".jpe") || // NOI18N
        s.equals("jpg") || s.equals("gif") || // NOI18N
        s.equals("jpeg") || s.equals("jpe"); // NOI18N
    }
    
    // variables .................................................................................
    
    //private Icon icon;
    
    // init .......................................................................................
    
    public BiIconEditor( FileObject sourceFileObject ) {
        this.sourceFileObject = sourceFileObject;
    }
    
    // Special access methods......................................................................
    
    
    /** @return the name of image's source - depending on the type it can be a URL, file name or
     * resource path to the image on classpath */
    public String getSourceName() {
        if (getValue() instanceof BiImageIcon)
            return getValue().getName();
        else
            return null;
    }

    @Override
    public void setValue(Object value) {
        BiImageIcon old = getValue();
        if (old == value || old != null && old.equals(value)) {
            return;
        }
        if (env != null) {
            BiImageIcon newval = (BiImageIcon) value;
            env.setState(newval != null && (newval.url == null || newval.getIcon() == null) ? PropertyEnv.STATE_INVALID : PropertyEnv.STATE_VALID);
        }
        super.setValue(value);
    }

    @Override
    public BiImageIcon getValue() {
        return (BiImageIcon) super.getValue();
    }
    
    /**
     * @return The property value as a human editable string.
     * <p>   Returns null if the value can't be expressed as an editable string.
     * <p>   If a non-null value is returned, then the PropertyEditor should
     *       be prepared to parse that string back in setAsText().
     */
    @Override
    public String getAsText() {
        Object val = getValue();        
        return String.valueOf(textFromIcon((BiImageIcon) val));
    }
    
    /**
     * Set the property value by parsing a given String.  May raise
     * java.lang.IllegalArgumentException if either the String is
     * badly formatted or if this kind of property can't be expressed
     * as text.
     * @param text  The string to be parsed.
     */
    @Override
    public void setAsText(String string) throws IllegalArgumentException {
        try { 
            BiImageIcon iconFromText = iconFromText(string);
            if (iconFromText == null || iconFromText.url != null) {
                setValue(iconFromText);
            } else {
                String msg = NbBundle.getMessage(IconPanel.class, "CTL_Icon_not_exists", string);
                DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message(msg));
            }
        }
        catch ( IllegalArgumentException e ) {
            // User inserted incorrect path either report or
            // do nothing
            // For now choosing doing nothing
        }
    }

    /** translates icon object to text representation; null in case of undefined icon */
    String textFromIcon(BiImageIcon icon) {
        return icon == null
                ? null
                : icon.getName();
    }
    
    BiImageIcon iconFromText(String string) throws IllegalArgumentException {
        BiImageIcon ii;
        try {
            if (string.length() == 0 || string.equals("null")) { // NOI18N
                ii = null;
            }
            else {
                URL res = resolveIconPath(string, sourceFileObject);
                ii = new BiImageIcon(res, string);
            }
        } catch (IOException ex) {
            ii = new BiImageIcon(null, string);
        }
        return ii;
    }

    /**
     * translates resource path defined in {@link java.beans.BeanInfo}'s subclass
     * that complies with {@link Class#getResource(java.lang.String) Class.getResource} format
     * to format complying with {@link ClassPath#getResourceName(org.openide.filesystems.FileObject) ClassPath.getResourceName}
     * @param resourcePath absolute path or path relative to package of BeanInfo's subclass
     * @param beanInfo BeanInfo's subclass
     * @return path as URL
     * @throws FileStateInvalidException invalid FileObject
     * @throws FileNotFoundException resource cannot be found
     */
    private static URL resolveIconPath(String resourcePath, FileObject beanInfo)
            throws FileStateInvalidException, FileNotFoundException {
        ClassPath cp = ClassPath.getClassPath(beanInfo, ClassPath.SOURCE);
        String path = resourcePath.charAt(0) != '/'
                ? '/' + cp.getResourceName(beanInfo.getParent()) + '/' + resourcePath
                : resourcePath;
        FileObject res = cp.findResource(path);
        if (res != null && res.canRead() && res.isData()) {
            return res.toURL();
        } else {
            throw new FileNotFoundException(path);
        }
    }
    
    /**
     * @return  True if the class will honor the paintValue method.
     */
    @Override
    public boolean isPaintable() {
        return false;
    }
    
    /**
     * @return  True if the propertyEditor can provide a custom editor.
     */
    @Override
    public boolean supportsCustomEditor() {
        return true;
    }
    
    /**
     * A PropertyEditor may choose to make available a full custom Component
     * that edits its property value.  It is the responsibility of the
     * PropertyEditor to hook itself up to its editor Component itself and
     * to report property value changes by firing a PropertyChange event.
     * <P>
     * The higher-level code that calls getCustomEditor may either embed
     * the Component in some larger property sheet, or it may put it in
     * its own individual dialog, or ...
     *
     * @return A java.awt.Component that will allow a human to directly
     *      edit the current property value.  May be null if this is
     *      not supported.
     */
    @Override
    public java.awt.Component getCustomEditor() {
        return new IconPanel(this, env);
    }

    public void attachEnv(PropertyEnv env) {
        this.env = env;
        BiImageIcon val = getValue();
        if (val != null && (val.url == null || val.getIcon() == null)) {
            env.setState(PropertyEnv.STATE_INVALID);
        }
    }
    
    public static final class BiImageIcon {
        private String name;
        private URL url;
        private Icon icon;
        
        public BiImageIcon() {
        }
        
        BiImageIcon(URL url, String name) {
            this.url = url;
            this.name = name;
        }
        
        String getName() {
            return name;            
        }

        public Icon getIcon() {
            if (icon == null) {
                if (url == null) {
                    return icon;
                }
                try {
                    Image image = ImageIO.read(url);
                    if (image == null) {
                        return null;
                    }
                    icon = new ImageIcon(image);
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
            return icon;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final BiImageIcon other = (BiImageIcon) obj;
            if (this.name != other.name && (this.name == null || !this.name.equals(other.name))) {
                return false;
            }
            return true;
        }
        
    }
    
    private static final class IconPanel extends JPanel implements VetoableChangeListener {
        JRadioButton rbClasspath, rbNoPicture;
        JTextField tfName;
        JButton bSelect;
        JScrollPane spImage;
        private final PropertyEnv env;
        private BiImageIcon value;
        private BiIconEditor editor;
        
        IconPanel(BiIconEditor editor, PropertyEnv env) {
            this.env = env;
            this.editor = editor;
            
            // visual components .............................................
            
            JLabel lab;
            setLayout(new BorderLayout(6, 6));
            setBorder(new EmptyBorder(6, 6, 6, 6));
            getAccessibleContext().setAccessibleName(bundle.getString("ACS_IconPanelA11yName"));  // NOI18N
            getAccessibleContext().setAccessibleDescription(bundle.getString("ACS_IconPanelA11yDesc"));  // NOI18N
            JPanel p = new JPanel(new BorderLayout(3, 3));
            JPanel p1 = new JPanel(new BorderLayout());
            p1.setBorder(new TitledBorder(new EtchedBorder(), bundle.getString("CTL_ImageSourceType")));
            JPanel p2 = new JPanel();
            p2.setBorder(new EmptyBorder(0, 3, 0, 3));
            GridBagLayout l = new GridBagLayout();
            GridBagConstraints c = new GridBagConstraints();
            p2.setLayout(l);
            c.anchor = GridBagConstraints.WEST;
            
            p2.add(rbClasspath = new JRadioButton(bundle.getString("CTL_Classpath")));
            rbClasspath.setToolTipText(bundle.getString("ACS_ClasspathA11yDesc"));
            rbClasspath.setMnemonic(bundle.getString("CTL_Classpath_Mnemonic").charAt(0));
            c.gridwidth = 1;
            l.setConstraints(rbClasspath, c);
            
            p2.add(lab = new JLabel(bundle.getString("CTL_ClasspathExample")));
            lab.getAccessibleContext().setAccessibleDescription(bundle.getString("ACS_ClasspathExampleA11yDesc"));
            c.gridwidth = GridBagConstraints.REMAINDER;
            l.setConstraints(lab, c);
            
            p2.add(rbNoPicture = new JRadioButton(bundle.getString("CTL_NoPicture")));
            rbNoPicture.setToolTipText(bundle.getString("ACS_NoPictureA11yDesc"));
            rbNoPicture.setMnemonic(bundle.getString("CTL_NoPicture_Mnemonic").charAt(0));
            c.gridwidth = 1;
            l.setConstraints(rbNoPicture, c);
            
            p2.add(lab = new JLabel(bundle.getString("CTL_Null")));
            lab.getAccessibleContext().setAccessibleDescription(bundle.getString("ACS_NullA11yDesc"));
            c.gridwidth = GridBagConstraints.REMAINDER;
            l.setConstraints(lab, c);
            
            ButtonGroup bg = new ButtonGroup();
            bg.add(rbClasspath);
            bg.add(rbNoPicture);
            rbClasspath.setSelected(true);
            p1.add(p2, "West"); // NOI18N
            p.add(p1, "North"); // NOI18N
            p1 = new JPanel(new BorderLayout(6, 6));
            JLabel nameLabel = new JLabel(bundle.getString("CTL_ImageSourceName"));
            nameLabel.getAccessibleContext().setAccessibleDescription(bundle.getString("ACS_ImageSourceNameA11yDesc"));
            nameLabel.setDisplayedMnemonic(bundle.getString("CTL_ImageSourceName_Mnemonic").charAt(0));
            p1.add(nameLabel, "West"); // NOI18N
            p1.add(tfName = new JTextField(), "Center"); // NOI18N
            nameLabel.setLabelFor(tfName);
            tfName.getAccessibleContext().setAccessibleName(bundle.getString("ACS_ImageSourceNameTextFieldA11yName"));
            tfName.setToolTipText(bundle.getString("ACS_ImageSourceNameTextFieldA11yDesc"));
            p1.add(bSelect = new JButton("..."), "East"); // NOI18N
            bSelect.getAccessibleContext().setAccessibleName(bundle.getString("ACS_ImageSourceNameBrowseButtonA11yName"));
            bSelect.setToolTipText(bundle.getString("ACS_ImageSourceNameBrowseButtonA11yDesc"));
            bSelect.setEnabled(false);
            p.add(p1, "South"); // NOI18N
            add(p, "North"); // NOI18N
            spImage = new JScrollPane() {
                @Override
                public Dimension getPreferredSize() {
                    return new Dimension(60, 60);
                }
            };
            add(spImage, "Center"); // NOI18N
            
            // listeners .................................................
            
            tfName.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    setValue();
                }
            });
            rbClasspath.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    bSelect.setEnabled(true);
                    tfName.setEnabled(true);
                    setValue();
                }
            });
            rbNoPicture.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    bSelect.setEnabled(false);
                    tfName.setEnabled(false);
                    
                    setValue(null);
                    updateIcon();
                }
            });
            bSelect.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    if (rbClasspath.isSelected()) {
                        String name = selectResource();
                        if (name != null) {
                            tfName.setText("/" + name); // NOI18N
                            setValue();
                        }
                    }
                }
            });
            // initialization ......................................
 
            env.setState(PropertyEnv.STATE_NEEDS_VALIDATION);
            env.addVetoableChangeListener(this);
            setValue(editor.getValue());
            updateIcon();
            
            HelpCtx.setHelpIDString(this, BEAN_ICONEDITOR_HELP); 
            
            BiImageIcon i = getValue();
            if (i == null) {
                rbNoPicture.setSelected(true);
                bSelect.setEnabled(false);
                tfName.setEnabled(false);
                return;
            }
            
            rbClasspath.setSelected(true);
            bSelect.setEnabled(true);
            tfName.setText((i).getName());
        }
        
        void updateIcon() {
            BiImageIcon bii = getValue();
            Icon i = bii == null? null: bii.getIcon();
            spImage.setViewportView((i == null) ? new JLabel() : new JLabel(i));
            //      repaint();
            validate();
        }
        
        void setValue() {
            String val = tfName.getText();
            val.trim();
            if ("".equals(val)) { // NOI18N
                setValue(null);
                return;
            }
            
            try {
                setValue(editor.iconFromText(val));
            } catch (IllegalArgumentException ee) {
                // Reporting the exception is maybe too much let's do nothing
                // instead 
                // org.openide.ErrorManager.getDefault().notify(org.openide.ErrorManager.INFORMATIONAL, ee);
            }
            updateIcon();
        }
        
        private void setValue(BiImageIcon icon) {
            this.value = icon;
        }
        
        private BiImageIcon getValue() {
            return this.value;
        }
        
        private Object getPropertyValue(PropertyChangeEvent evt) throws PropertyVetoException {
            BiImageIcon ii = null;
            String s = tfName.getText().trim();
            if (rbClasspath.isSelected() && s.length() != 0) {
                try{
                    URL res = resolveIconPath(s, editor.sourceFileObject);
                    ii = new BiImageIcon(res, s);
                } catch (FileStateInvalidException ex) {
                    throw new PropertyVetoException(
                            NbBundle.getMessage(IconPanel.class, "CTL_Icon_not_exists", ex.getFileSystemName()), //NOI18N
                            evt);
                } catch (FileNotFoundException ex) {
                    throw new PropertyVetoException(
                            NbBundle.getMessage(IconPanel.class, "CTL_Icon_not_exists", ex.getMessage()), //NOI18N
                            evt);
                }
            }
            return ii;
        }

        public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
            if (PropertyEnv.PROP_STATE == evt.getPropertyName()) {
                BiImageIcon ii = (BiImageIcon) getPropertyValue(evt);
                editor.setValue(ii);
            }
        }
        
        private List<FileObject> getRoots(ClassPath cp) {
            List<FileObject> list = new ArrayList<FileObject>(cp.entries().size());
            for (ClassPath.Entry e : cp.entries()) {
                // try to map it to sources
                URL url = e.getURL();
                SourceForBinaryQuery.Result r = SourceForBinaryQuery.findSourceRoots(url);
                FileObject [] fos = r.getRoots();
                if (fos.length > 0) {
                    for (int i = 0 ; i < fos.length; i++) list.add(fos[i]);
                } else {
                    if (e.getRoot()!=null)
                        list.add(e.getRoot()); // add the class-path location directly
                }
            }
            
            return list;
        }
        
        private String rootDisplayName(FileObject fo) {
            return FileUtil.getFileDisplayName(fo);
        }
        
        /**
         * Obtains icon resource from the user.
         *
         * @returns name of the selected resource or <code>null</code>.
         */
        private String selectResource() {
            ClassPath executionClassPath = ClassPath.getClassPath(editor.sourceFileObject, ClassPath.EXECUTE);
            List<FileObject> roots = (executionClassPath == null)
                    ? Collections.<FileObject>emptyList()
                    : getRoots(executionClassPath);
            Node nodes[] = new Node[roots.size()];
            int selRoot = -1;
            try {
                ListIterator<FileObject> iter = roots.listIterator();
                while (iter.hasNext()) {
                    FileObject root = iter.next();
                    DataObject dob = DataObject.find(root);
                    final String displayName = rootDisplayName(root);
                    nodes[iter.previousIndex()] = new RootNode(dob.getNodeDelegate(), displayName);
                }
            } catch (DataObjectNotFoundException donfex) {
                Exceptions.printStackTrace(donfex);
                return null;
            }
            Children children = new Children.Array();
            children.add(nodes);
            final AbstractNode root = new AbstractNode(children);
            root.setIconBaseWithExtension("org/netbeans/modules/beans/resources/iconResourceRoot.gif"); // NOI18N
            root.setDisplayName(bundle.getString("CTL_ClassPathName")); // NOI18N
                            
            ResourceSelector selector = new ResourceSelector(root);
            DialogDescriptor dd = new DialogDescriptor(selector, bundle.getString("CTL_OpenDialogName")); // NOI18N
            Object res = DialogDisplayer.getDefault().notify(dd);
            nodes = (res == DialogDescriptor.OK_OPTION) ? selector.getNodes() : null;
            String name = null;
            if ((nodes != null) && (nodes.length == 1)) {
                DataObject dob = nodes[0].getCookie(DataObject.class);
                if (dob != null) {
                    FileObject fob = dob.getPrimaryFile();
                    if (fob != null) {                        
                        if (executionClassPath.contains(fob)) {
                            name = executionClassPath.getResourceName(fob);
                        } else {
                            ClassPath srcClassPath = ClassPath.getClassPath(fob, ClassPath.SOURCE);
                            name = srcClassPath.getResourceName(fob);
                        }
                    }
                }
            }
            return name;
        }
        
    } // end of IconPanel
    
    private static final class RootNode extends FilterNode {
        RootNode(Node node, String displayName) {
            super(node);
            if (displayName != null) {
                disableDelegation(DELEGATE_GET_DISPLAY_NAME | DELEGATE_SET_DISPLAY_NAME);
                setDisplayName(displayName);
            }
        }
    } // RootNode
    
    private static final class ResourceSelector extends JPanel implements ExplorerManager.Provider {
        /** Manages the tree. */
        private ExplorerManager manager = new ExplorerManager();
                
        public ResourceSelector(Node root) {
            setLayout(new BorderLayout(0, 5));
            setBorder(new EmptyBorder(12, 12, 0, 11));
            getAccessibleContext().setAccessibleDescription(bundle.getString("ACSD_ResourceSelector")); // NOI18N
            getAccessibleContext().setAccessibleName(bundle.getString("ACSN_ResourceSelector")); // NOI18N
            manager.setRootContext(root);
            
            BeanTreeView tree = new BeanTreeView();
            tree.setPopupAllowed(false);
            tree.setDefaultActionAllowed(false);
            // install proper border for tree
            tree.setBorder((Border)UIManager.get("Nb.ScrollPane.border")); // NOI18N
            tree.getAccessibleContext().setAccessibleName(bundle.getString("ACSN_ResourceSelectorView")); // NOI18N
            tree.getAccessibleContext().setAccessibleDescription(bundle.getString("ACSD_ResourceSelectorView")); // NOI18N
            add(tree, BorderLayout.CENTER);
        }
        
        /**
         * Gets preferred size. Overrides superclass method.
         * Height is adjusted to 1/2 screen.
         */
        @Override
        public Dimension getPreferredSize() {
            Dimension dim = super.getPreferredSize();
            dim.height = Math.max(dim.height, org.openide.util.Utilities.getUsableScreenBounds().height / 2);
            return dim;
        }
        
        /**
         * @return selected nodes
         */
        public Node[] getNodes() {
            return manager.getSelectedNodes();
        }
        
        public ExplorerManager getExplorerManager() {
            return manager;
        }
        
    } // ResourceSelector
    
}
