/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */
package org.openide.nodes;

import org.openide.ErrorManager;
import org.openide.util.NbBundle;

import java.beans.Beans;
import java.beans.PropertyEditor;

import java.lang.reflect.*;

import java.security.*;


/** Support class for <code>Node.Property</code>.
*
* @see Node.Property
* @author Jan Jancura, Jaroslav Tulach, Ian Formanek
* @version 0.21, Mar 24, 1998
*/
public abstract class PropertySupport extends Node.Property {
    /** flag whether the property is readable */
    private boolean canR;

    /** flag whether the property is writable */
    private boolean canW;

    /** Constructs a new support.
    * @param name        the name of the property
    * @param type        the class type of the property
    * @param displayName the display name of the property
    * @param canR        whether the property is readable
    * @param canW        whether the property is writable
    */
    public PropertySupport(
        String name, Class type, String displayName, String shortDescription, boolean canR, boolean canW
    ) {
        super(type);
        this.setName(name);
        setDisplayName(displayName);
        setShortDescription(shortDescription);
        this.canR = canR;
        this.canW = canW;
    }

    /* Can read the value of the property.
    * Returns the value passed into constructor.
    * @return <CODE>true</CODE> if the read of the value is supported
    */
    public boolean canRead() {
        return canR;
    }

    /* Can write the value of the property.
    * Returns the value passed into constructor.
    * @return <CODE>true</CODE> if the read of the value is supported
    */
    public boolean canWrite() {
        return canW;
    }

    /** Support for properties from Java Reflection. */
    public static class Reflection extends Node.Property {
        /** Instance of a bean. */
        protected Object instance;

        /** setter method */
        private Method setter;

        /** getter method */
        private Method getter;

        /** class of property editor */
        private Class propertyEditorClass;

        /** Create a support with method objects specified.
        * The methods must be public.
        * @param instance (Bean) object to work on
        * @param valueType type of the property
        * @param getter getter method, can be <code>null</code>
        * @param setter setter method, can be <code>null</code>
        * @throws IllegalArgumentException if the methods are not public
        */
        public Reflection(Object instance, Class valueType, Method getter, Method setter) {
            super(valueType);

            if ((getter != null) && !Modifier.isPublic(getter.getModifiers())) {
                throw new IllegalArgumentException("Cannot use a non-public getter " + getter); // NOI18N
            }

            if ((setter != null) && !Modifier.isPublic(setter.getModifiers())) {
                throw new IllegalArgumentException("Cannot use a non-public setter " + setter); // NOI18N
            }

            this.instance = instance;
            this.setter = setter;
            this.getter = getter;
        }

        /** Create a support with methods specified by name.
        * The instance class will be examined for the named methods.
        * But if the instance class is not public, the nearest public superclass
        * will be used instead, so that the getters and setters remain accessible.
        * @param instance (Bean) object to work on
        * @param valueType type of the property
        * @param getter name of getter method, can be <code>null</code>
        * @param setter name of setter method, can be <code>null</code>
        * @exception NoSuchMethodException if the getter or setter methods cannot be found
        */
        public Reflection(Object instance, Class valueType, String getter, String setter)
        throws NoSuchMethodException {
            this(
                instance, valueType,
                (
            // find the getter ()
            getter == null) ? null : findAccessibleClass(instance.getClass()).getMethod(getter, new Class[0]),
                (
            // find the setter (valueType)
            setter == null) ? null : findAccessibleClass(instance.getClass()).getMethod(
                    setter, new Class[] { valueType }
                )
            );
        }

        // [PENDING] should use Beans API in case there is overriding BeanInfo  --jglick

        /** Create a support based on the property name.
        * The getter and setter methods are constructed by capitalizing the first
        * letter in the name of propety and prefixing it with <code>get</code> and
        * <code>set</code>, respectively.
        *
        * @param instance object to work on
        * @param valueType type of the property
        * @param property name of property
        * @exception NoSuchMethodException if the getter or setter methods cannot be found
        */
        public Reflection(Object instance, Class valueType, String property)
        throws NoSuchMethodException {
            this(
                instance, valueType, findGetter(instance, valueType, property),
                findAccessibleClass(instance.getClass()).getMethod(
                    firstLetterToUpperCase(property, "set"), new Class[] { valueType }
                )
            );
        }

        /** Find the nearest superclass (or same class) that is public to this one. */
        private static Class findAccessibleClass(Class clazz) {
            if (Modifier.isPublic(clazz.getModifiers())) {
                return clazz;
            } else {
                Class sup = clazz.getSuperclass();

                if (sup == null) {
                    return Object.class; // handle interfaces
                }

                return findAccessibleClass(sup);
            }
        }

        /** Helper method to convert the first letter of a string to uppercase.
        * And prefix the string with some next string.
        */
        private static String firstLetterToUpperCase(String s, String pref) {
            switch (s.length()) {
            case 0:
                return pref;

            case 1:
                return pref + Character.toUpperCase(s.charAt(0));

            default:
                return pref + Character.toUpperCase(s.charAt(0)) + s.substring(1);
            }
        }

        // Finds the proper getter
        private static Method findGetter(Object instance, Class valueType, String property)
        throws NoSuchMethodException {
            NoSuchMethodException nsme;

            try {
                return findAccessibleClass(instance.getClass()).getMethod(
                    firstLetterToUpperCase(property, "get"), new Class[0]
                );
            } catch (NoSuchMethodException e) {
                if (valueType != boolean.class) {
                    throw e;
                } else {
                    nsme = e;
                }
            }

            // Is of type boolean and "get" getter does not exist
            try {
                return findAccessibleClass(instance.getClass()).getMethod(
                    firstLetterToUpperCase(property, "is"), new Class[0]
                );
            } catch (NoSuchMethodException e) {
                throw e;
            }
        }

        /* Can read the value of the property.
        * @return <CODE>true</CODE> if the read of the value is supported
        */
        public boolean canRead() {
            return getter != null;
        }

        /* Getter for the value.
        * @return the value of the property
        * @exception IllegalAccessException cannot access the called method
        * @exception IllegalArgumentException wrong argument
        * @exception InvocationTargetException an exception during invocation
        */
        public Object getValue() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            if (getter == null) {
                throw new IllegalAccessException();
            }

            Object valideInstance = Beans.getInstanceOf(instance, getter.getDeclaringClass());

            try {
                try {
                    return getter.invoke(valideInstance, new Object[0]);
                } catch (IllegalAccessException ex) {
                    try {
                        getter.setAccessible(true);

                        return getter.invoke(valideInstance, new Object[0]);
                    } finally {
                        getter.setAccessible(false);
                    }
                }
            } catch (IllegalArgumentException iae) {
                //Provide a better message for debugging
                StringBuffer sb = new StringBuffer("Attempted to invoke method ");
                sb.append(getter.getName());
                sb.append(" from class ");
                sb.append(getter.getDeclaringClass().getName());
                sb.append(" on an instance of ");
                sb.append(valideInstance.getClass().getName());
                sb.append(" Problem:");
                sb.append(iae.getMessage());

                IllegalArgumentException nue = new IllegalArgumentException(sb.toString());
                ErrorManager.getDefault().annotate(nue, iae);
                throw nue;
            }
        }

        /* Can write the value of the property.
        * @return <CODE>true</CODE> if the read of the value is supported
        */
        public boolean canWrite() {
            return setter != null;
        }

        /* Setter for the value.
        * @param val the value of the property
        * @exception IllegalAccessException cannot access the called method
        * @exception IllegalArgumentException wrong argument
        * @exception InvocationTargetException an exception during invocation
        */
        public void setValue(Object val)
        throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            if (setter == null) {
                throw new IllegalAccessException();
            }

            Object valideInstance = Beans.getInstanceOf(instance, setter.getDeclaringClass());

            try {
                setter.invoke(valideInstance, new Object[] { val });
            } catch (IllegalAccessException ex) {
                try {
                    setter.setAccessible(true);
                    setter.invoke(valideInstance, new Object[] { val });
                } finally {
                    setter.setAccessible(false);
                }
            }
        }

        /* Returns property editor for this property.
        * @return the property editor or <CODE>null</CODE> if there should not be
        *    any editor.
        */
        public PropertyEditor getPropertyEditor() {
            if (propertyEditorClass != null) {
                try {
                    return (PropertyEditor) propertyEditorClass.newInstance();
                } catch (InstantiationException ex) {
                    ErrorManager.getDefault().notify(ex);
                } catch (IllegalAccessException iex) {
                    ErrorManager.getDefault().notify(iex);
                }
            }

            return super.getPropertyEditor();
        }

        /** Set the property editor explicitly.
        * @param clazz class type of the property editor
        */
        public void setPropertyEditorClass(Class clazz) {
            propertyEditorClass = clazz;
        }
    }

    /** A simple read/write property.
    * Subclasses should implement
    * {@link #getValue} and {@link #setValue}.
    */
    public static abstract class ReadWrite extends PropertySupport {
        /** Construct a new support.
        * @param name        the name of the property
        * @param type        the class type of the property
        * @param displayName the display name of the property
        * @param shortDescription a short description of the property
        */
        public ReadWrite(String name, Class type, String displayName, String shortDescription) {
            super(name, type, displayName, shortDescription, true, true);
        }
    }

    /** A simple read-only property.
    * Subclasses should implement {@link #getValue}.
    */
    public static abstract class ReadOnly extends PropertySupport {
        /** Construct a new support.
        * @param name        the name of the property
        * @param type        the class type of the property
        * @param displayName the display name of the property
        * @param shortDescription a short description of the property
        */
        public ReadOnly(String name, Class type, String displayName, String shortDescription) {
            super(name, type, displayName, shortDescription, true, false);
        }

        /* Setter for the value.
        * @param val the value of the property
        * @exception IllegalAccessException cannot access the called method
        * @exception IllegalArgumentException wrong argument
        * @exception InvocationTargetException an exception during invocation
        */
        public void setValue(Object val)
        throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            throw new IllegalAccessException("Cannot write to ReadOnly property"); // NOI18N
        }
    }

    /** A simple write-only property.
    * Subclasses should implement {@link #setValue}.
    */
    public static abstract class WriteOnly extends PropertySupport {
        /** Construct a new support.
        * @param name        the name of the property
        * @param type        the class type of the property
        * @param displayName the display name of the property
        * @param shortDescription a short description of the property
        */
        public WriteOnly(String name, Class type, String displayName, String shortDescription) {
            super(name, type, displayName, shortDescription, false, true);
        }

        /* Getter for the value.
        * @return the value of the property
        * @exception IllegalAccessException cannot access the called method
        * @exception IllegalArgumentException wrong argument
        * @exception InvocationTargetException an exception during invocation
        */
        public Object getValue() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            throw new IllegalAccessException("Cannod read from WriteOnly property"); // NOI18N
        }
    }

    /** Support for the name property of a node. Delegates {@link #setValue} and {@link #getValue}
    * to {@link Node#setName} and {@link Node#getName}.
    * <p>(Final only for performance, can be unfinaled if desired).
    */
    public static final class Name extends PropertySupport {
        /** The node to which we delegate the work. */
        private final Node node;

        /** Create the name property for a node with the standard name and hint.
        * @param node the node
        */
        public Name(final Node node) {
            this(
                node, NbBundle.getBundle(PropertySupport.class).getString("CTL_StandardName"),
                NbBundle.getBundle(PropertySupport.class).getString("CTL_StandardHint")
            );
        }

        /** Create the name property for a node.
        * @param node the node
        * @param propName name of the "name" property
        * @param hint hint message for the "name" property
        */
        public Name(final Node node, final String propName, final String hint) {
            super(Node.PROP_NAME, String.class, propName, hint, true, node.canRename());
            this.node = node;
        }

        /* Getter for the value. Delegates to Node.getName().
        * @return the name
        */
        public Object getValue() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            return node.getName();
        }

        /* Setter for the value. Delegates to Node.setName().
        * @param val new name
        */
        public void setValue(Object val)
        throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            if (!(val instanceof String)) {
                throw new IllegalAccessException();
            }

            Object oldName = node.getName();
            node.setName((String) val);
            node.firePropertyChange(Node.PROP_NAME, oldName, val);
        }
    }
     // end of Name inner class
}
