/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Original authors and others - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 450927
 *     Simon Scholz <simon.scholz@vogella.com> - Bug 487913, 488067
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.data;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Convenience class which uses java reflection to get/set property names from
 * the row bean. It looks for getter methods for reading and setter methods for
 * writing according to the Java conventions.
 *
 * @param <R>
 *            type of the row object/bean
 */
public class ReflectiveColumnPropertyAccessor<R> implements IColumnPropertyAccessor<R> {

    private static final Logger LOG = LoggerFactory.getLogger(ReflectiveColumnPropertyAccessor.class);

    private final List<String> propertyNames;

    private Map<Class<?>, Map<String, PropertyDescriptor>> propertyDescriptorMap = new HashMap<>();

    /**
     * @param propertyNames
     *            of the members of the row bean
     */
    public ReflectiveColumnPropertyAccessor(String... propertyNames) {
        this.propertyNames = Arrays.asList(propertyNames);
    }

    /**
     * @param propertyNames
     *            of the members of the row bean
     * @since 1.4
     */
    public ReflectiveColumnPropertyAccessor(List<String> propertyNames) {
        this.propertyNames = propertyNames;
    }

    @Override
    public int getColumnCount() {
        return this.propertyNames.size();
    }

    @Override
    public Object getDataValue(R rowObj, int columnIndex) {
        try {
            PropertyDescriptor propertyDesc = getPropertyDescriptor(rowObj, columnIndex);
            Method readMethod = propertyDesc.getReadMethod();
            return readMethod.invoke(rowObj);
        } catch (Exception e) {
            LOG.warn("Error on getting data value", e); //$NON-NLS-1$
            throw new IllegalStateException(e);
        }
    }

    @Override
    public void setDataValue(R rowObj, int columnIndex, Object newValue) {
        try {
            PropertyDescriptor propertyDesc = getPropertyDescriptor(rowObj, columnIndex);
            Method writeMethod = propertyDesc != null ? propertyDesc.getWriteMethod() : null;
            if (writeMethod == null) {
                throw new IllegalStateException(
                        "Setter method not found in backing bean for value at column index: " + columnIndex); //$NON-NLS-1$
            }
            writeMethod.invoke(rowObj, newValue);
        } catch (IllegalArgumentException ex) {
            LOG.error("Data type being set does not match the data type of the setter method in the backing bean", ex); //$NON-NLS-1$
        } catch (Exception e) {
            LOG.error("Error while setting data value", e); //$NON-NLS-1$
            throw new IllegalStateException("Error while setting data value"); //$NON-NLS-1$
        }
    }

    @Override
    public String getColumnProperty(int columnIndex) {
        return this.propertyNames.get(columnIndex);
    }

    @Override
    public int getColumnIndex(String propertyName) {
        return this.propertyNames.indexOf(propertyName);
    }

    private PropertyDescriptor getPropertyDescriptor(R rowObj, int columnIndex) throws IntrospectionException {
        String propertyName = this.propertyNames.get(columnIndex);
        return getPropertyDescriptor(rowObj, propertyName);
    }

    /**
     *
     * @param rowObj
     *            The Java Bean for which the {@link PropertyDescriptor} is
     *            requested.
     * @param propertyName
     *            The name of the property for which the
     *            {@link PropertyDescriptor} is requested.
     * @return The {@link PropertyDescriptor} that describes the property with
     *         the given name in the given Java Bean object that exports it via
     *         a pair of accessor methods.
     * @throws IntrospectionException
     *             if an exception occurs during introspection
     * @since 1.6
     */
    protected PropertyDescriptor getPropertyDescriptor(Object rowObj, String propertyName) throws IntrospectionException {
        synchronized (this.propertyDescriptorMap) {
            Map<String, PropertyDescriptor> descriptorMap = this.propertyDescriptorMap.get(rowObj.getClass());

            if (descriptorMap == null) {
                PropertyDescriptor[] propertyDescriptors =
                        Introspector.getBeanInfo(rowObj.getClass()).getPropertyDescriptors();

                Map<String, PropertyDescriptor> propertiesByAttribute = new HashMap<>();
                for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                    propertiesByAttribute.put(propertyDescriptor.getName(), propertyDescriptor);
                }
                descriptorMap = propertiesByAttribute;
                this.propertyDescriptorMap.put(rowObj.getClass(), propertiesByAttribute);
            }

            return descriptorMap.get(propertyName);
        }
    }

}
