/*
 * 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.apache.commons.beanutils2;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * An internally used helper class for storing introspection information about a bean
 * class.
 * </p>
 * <p>
 * This class is used by {@link PropertyUtilsBean}. When accessing bean properties via
 * reflection information about the properties available and their types and access
 * methods must be present. {@code PropertyUtilsBean} stores this information in a cache
 * so that it can be accessed quickly. The cache stores instances of this class.
 * </p>
 * <p>
 * This class mainly stores information about the properties of a bean class. Per default,
 * this is contained in {@code PropertyDescriptor} objects. Some additional information
 * required by the {@code BeanUtils} library is also stored here.
 * </p>
 *
 * @since 1.9.1
 */
class BeanIntrospectionData {
    /** An array with property descriptors for the managed bean class. */
    private final PropertyDescriptor[] descriptors;

    /** A map for remembering the write method names for properties. */
    private final Map<String, String> writeMethodNames;

    /**
     * Creates a new instance of {@code BeanIntrospectionData} and initializes its
     * completely.
     *
     * @param descs the array with the descriptors of the available properties
     */
    public BeanIntrospectionData(final PropertyDescriptor[] descs) {
        this(descs, setUpWriteMethodNames(descs));
    }

    /**
     * Creates a new instance of {@code BeanIntrospectionData} and allows setting the map
     * with write method names. This constructor is mainly used for testing purposes.
     *
     * @param descs the array with the descriptors of the available properties
     * @param writeMethNames the map with the names of write methods
     */
    BeanIntrospectionData(final PropertyDescriptor[] descs, final Map<String, String> writeMethNames) {
        descriptors = descs;
        writeMethodNames = writeMethNames;
    }

    /**
     * Returns the array with property descriptors.
     *
     * @return the property descriptors for the associated bean class
     */
    public PropertyDescriptor[] getDescriptors() {
        return descriptors;
    }

    /**
     * Returns the {@code PropertyDescriptor} for the property with the specified name. If
     * this property is unknown, result is <b>null</b>.
     *
     * @param name the name of the property in question
     * @return the {@code PropertyDescriptor} for this property or <b>null</b>
     */
    public PropertyDescriptor getDescriptor(final String name) {
        for (final PropertyDescriptor pd : getDescriptors()) {
            if (name.equals(pd.getName())) {
                return pd;
            }
        }
        return null;
    }

    /**
     * Returns the write method for the property determined by the given
     * {@code PropertyDescriptor}. This information is normally available in the
     * descriptor object itself. However, at least by the ORACLE implementation, the
     * method is stored as a {@code SoftReference}. If this reference has been freed by
     * the GC, it may be the case that the method cannot be obtained again. Then,
     * additional information stored in this object is necessary to obtain the method
     * again.
     *
     * @param beanCls the class of the affected bean
     * @param desc the {@code PropertyDescriptor} of the desired property
     * @return the write method for this property or <b>null</b> if there is none
     */
    public Method getWriteMethod(final Class<?> beanCls, final PropertyDescriptor desc) {
        Method method = desc.getWriteMethod();
        if (method == null) {
            final String methodName = writeMethodNames.get(desc.getName());
            if (methodName != null) {
                method = MethodUtils.getAccessibleMethod(beanCls, methodName,
                        desc.getPropertyType());
                if (method != null) {
                    try {
                        desc.setWriteMethod(method);
                    } catch (final IntrospectionException e) {
                        // ignore, in this case the method is not cached
                    }
                }
            }
        }

        return method;
    }

    /**
     * Initializes the map with the names of the write methods for the supported
     * properties. The method names - if defined - need to be stored separately because
     * they may get lost when the GC claims soft references used by the
     * {@code PropertyDescriptor} objects.
     *
     * @param descs the array with the descriptors of the available properties
     * @return the map with the names of write methods for properties
     */
    private static Map<String, String> setUpWriteMethodNames(final PropertyDescriptor[] descs) {
        final Map<String, String> methods = new HashMap<>();
        for (final PropertyDescriptor pd : descs) {
            final Method method = pd.getWriteMethod();
            if (method != null) {
                methods.put(pd.getName(), method.getName());
            }
        }
        return methods;
    }
}