/*
 * 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
 *
 *      https://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.jexl3.internal.introspection;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;

import org.apache.commons.jexl3.JexlException;

/**
 * Specialized executor to set a property in an object.
 *
 * @since 2.0
 */
public class PropertySetExecutor extends AbstractExecutor.Set {

    /** Index of the first character of the "set{p,P}roperty". */
    private static final int SET_START_INDEX = 3;

    /**
     * Discovers a PropertySetExecutor.
     * <p>The method to be found should be named "set{P,p}property".</p>
     *
     * @param is       the introspector
     * @param clazz    the class to find the get method from
     * @param property the property name to find
     * @param value      the value to assign to the property
     * @return the executor if found, null otherwise
     */
    public static PropertySetExecutor discover(final Introspector is,
                                               final Class<?> clazz,
                                               final String property,
                                               final Object value) {
        if (property == null || property.isEmpty()) {
            return null;
        }
        final java.lang.reflect.Method method = discoverSet(is, clazz, property, value);
        return method != null ? new PropertySetExecutor(clazz, method, property, value) : null;
    }

    /**
     * Discovers the method for a {@link org.apache.commons.jexl3.introspection.JexlPropertySet}.
     * <p>The method to be found should be named "set{P,p}property".
     * As a special case, any empty array will try to find a valid array-setting non-ambiguous method.
     *
     * @param is       the introspector
     * @param clazz    the class to find the get method from
     * @param property the name of the property to set
     * @param arg      the value to assign to the property
     * @return the method if found, null otherwise
     */
    private static java.lang.reflect.Method discoverSet(final Introspector is,
                                                        final Class<?> clazz,
                                                        final String property,
                                                        final Object arg) {
        // first, we introspect for the set<identifier> setter method
        final Object[] params = {arg};
        final StringBuilder sb = new StringBuilder("set");
        sb.append(property);
        // uppercase nth char
        final char c = sb.charAt(SET_START_INDEX);
        sb.setCharAt(SET_START_INDEX, Character.toUpperCase(c));
        java.lang.reflect.Method method = is.getMethod(clazz, sb.toString(), params);
        // lowercase nth char
        if (method == null) {
            sb.setCharAt(SET_START_INDEX, Character.toLowerCase(c));
            method = is.getMethod(clazz, sb.toString(), params);
            // uppercase nth char, try array
            if (method == null && isEmptyArray(arg)) {
                sb.setCharAt(SET_START_INDEX, Character.toUpperCase(c));
                method = lookupSetEmptyArray(is, clazz, sb.toString());
                // lowercase nth char
                if (method == null) {
                    sb.setCharAt(SET_START_INDEX, Character.toLowerCase(c));
                    method = lookupSetEmptyArray(is, clazz, sb.toString());
                }
            }
        }
        return method;
    }

    /**
     * Checks whether an argument is an empty array.
     *
     * @param arg the argument
     * @return true if {@code arg} is an empty array
     */
    private static boolean isEmptyArray(final Object arg) {
        return arg != null && arg.getClass().isArray() && Array.getLength(arg) == 0;
    }

    /**
     * Finds an empty array property setter method by {@code methodName}.
     * <p>This checks only one method with that name accepts an array as sole parameter.
     *
     * @param is       the introspector
     * @param clazz    the class to find the get method from
     * @param methodName    the method name to find
     * @return         the sole method that accepts an array as parameter
     */
    private static java.lang.reflect.Method lookupSetEmptyArray(final Introspector is, final Class<?> clazz, final String methodName) {
        java.lang.reflect.Method candidate = null;
        final java.lang.reflect.Method[] methods = is.getMethods(clazz, methodName);
        if (methods != null) {
            for (final java.lang.reflect.Method method : methods) {
                final Class<?>[] paramTypes = method.getParameterTypes();
                if (paramTypes.length == 1 && paramTypes[0].isArray()) {
                    if (candidate != null) {
                        // because the setter method is overloaded for different parameter type,
                        // return null here to report the ambiguity.
                        return null;
                    }
                    candidate = method;
                }
            }
        }
        return candidate;
    }

    /** The property. */
    protected final String property;

    /** The property value class. */
    protected final Class<?> valueClass;

    /**
     * Creates an instance.
     *
     * @param clazz  the class the set method applies to
     * @param method the method called through this executor
     * @param key    the key to use as 1st argument to the set method
     * @param value    the value
     */
    protected PropertySetExecutor(final Class<?> clazz,
                                  final java.lang.reflect.Method method,
                                  final String key,
                                  final Object value) {
        super(clazz, method);
        property = key;
        valueClass = classOf(value);
    }

    @Override
    public Object getTargetProperty() {
        return property;
    }

    @Override
    public Object invoke(final Object o, final Object argument) throws IllegalAccessException, InvocationTargetException {
        Object arg = argument;
        if (method != null) {
            // handle the empty array case
            if (isEmptyArray(arg)) {
                // if array is empty but its component type is different from the method first parameter component type,
                // replace argument with a new empty array instance (of the method first parameter component type)
                final Class<?> componentType = method.getParameterTypes()[0].getComponentType();
                if (componentType != null && !componentType.equals(arg.getClass().getComponentType())) {
                    arg = Array.newInstance(componentType, 0);
                }
            }
            method.invoke(o, arg);
        }
        return arg;
    }

    @Override
    public Object tryInvoke(final Object o, final Object identifier, final Object value) {
        if (o != null && method != null
            // ensure method name matches the property name
            && property.equals(castString(identifier))
            // object class should be same as executor's method declaring class
            && objectClass.equals(o.getClass())
            // argument class should be eq
            && valueClass.equals(classOf(value))) {
            try {
                return invoke(o, value);
            } catch (IllegalAccessException | IllegalArgumentException illegal) {
                return TRY_FAILED; // fail
            } catch (final InvocationTargetException invoke) {
                throw JexlException.tryFailed(invoke); // throw
            }
        }
        return TRY_FAILED;
    }
}
