/*
 * 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.fesod.sheet.util;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Map;
import org.apache.fesod.common.util.MemberUtils;
import org.apache.fesod.common.util.StringUtils;
import org.apache.fesod.shaded.cglib.beans.BeanMap;
import org.apache.fesod.sheet.metadata.NullObject;

public class FieldUtils {

    public static Class<?> nullObjectClass = NullObject.class;

    private static final int START_RESOLVE_FIELD_LENGTH = 2;

    public static Class<?> getFieldClass(Map dataMap, String fieldName, Object value) {
        if (dataMap instanceof BeanMap) {
            Class<?> fieldClass = ((BeanMap) dataMap).getPropertyType(fieldName);
            if (fieldClass != null) {
                return fieldClass;
            }
        }
        return getFieldClass(value);
    }

    public static Class<?> getFieldClass(Object value) {
        if (value != null) {
            return value.getClass();
        }
        return nullObjectClass;
    }

    /**
     * Parsing the name matching cglib。
     * <pre>
     *     null -&gt; null
     *     string1 -&gt; string1
     *     String2 -&gt; string2
     *     sTring3 -&gt; STring3
     *     STring4 -&gt; STring4
     *     STRING5 -&gt; STRING5
     *     STRing6 -&gt; STRing6
     * </pre>
     *
     * @param field field
     * @return field name.
     */
    public static String resolveCglibFieldName(Field field) {
        if (field == null) {
            return null;
        }
        String fieldName = field.getName();
        if (StringUtils.isBlank(fieldName) || fieldName.length() < START_RESOLVE_FIELD_LENGTH) {
            return fieldName;
        }
        char firstChar = fieldName.charAt(0);
        char secondChar = fieldName.charAt(1);
        if (Character.isUpperCase(firstChar) == Character.isUpperCase(secondChar)) {
            return fieldName;
        }
        if (Character.isUpperCase(firstChar)) {
            return buildFieldName(Character.toLowerCase(firstChar), fieldName);
        }
        return buildFieldName(Character.toUpperCase(firstChar), fieldName);
    }

    private static String buildFieldName(char firstChar, String fieldName) {
        return firstChar + fieldName.substring(1);
    }

    /**
     * Gets an accessible {@link Field} by name respecting scope. Superclasses/interfaces will be considered.
     *
     * @param cls       the {@link Class} to reflect, must not be {@code null}
     * @param fieldName the field name to obtain
     * @return the Field object
     * @throws IllegalArgumentException if the class is {@code null}, or the field name is blank or empty
     */
    public static Field getField(final Class<?> cls, final String fieldName) {
        final Field field = getField(cls, fieldName, false);
        MemberUtils.setAccessibleWorkaround(field);
        return field;
    }

    /**
     * Gets an accessible {@link Field} by name, breaking scope if requested. Superclasses/interfaces will be
     * considered.
     *
     * @param cls         the {@link Class} to reflect, must not be {@code null}
     * @param fieldName   the field name to obtain
     * @param forceAccess whether to break scope restrictions using the
     *                    {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)} method. {@code false} will
     *                    only
     *                    match {@code public} fields.
     * @return the Field object
     * @throws NullPointerException if the class is {@code null}
     * @throws IllegalArgumentException if the field name is blank or empty or is matched at multiple places
     * in the inheritance hierarchy
     */
    public static Field getField(final Class<?> cls, final String fieldName, final boolean forceAccess) {
        Validate.isTrue(cls != null, "The class must not be null");
        Validate.isTrue(StringUtils.isNotBlank(fieldName), "The field name must not be blank/empty");
        // FIXME is this workaround still needed? lang requires Java 6
        // Sun Java 1.3 has a bugged implementation of getField hence we write the
        // code ourselves

        // getField() will return the Field object with the declaring class
        // set correctly to the class that declares the field. Thus requesting the
        // field on a subclass will return the field from the superclass.
        //
        // priority order for lookup:
        // searchclass private/protected/package/public
        // superclass protected/package/public
        // private/different package blocks access to further superclasses
        // implementedinterface public

        // check up the superclass hierarchy
        for (Class<?> acls = cls; acls != null; acls = acls.getSuperclass()) {
            try {
                final Field field = acls.getDeclaredField(fieldName);
                // getDeclaredField checks for non-public scopes as well
                // and it returns accurate results
                if (!Modifier.isPublic(field.getModifiers())) {
                    if (forceAccess) {
                        field.setAccessible(true);
                    } else {
                        continue;
                    }
                }
                return field;
            } catch (final NoSuchFieldException ex) { // NOPMD
                // ignore
            }
        }
        // check the public interface case. This must be manually searched for
        // incase there is a public supersuperclass field hidden by a private/package
        // superclass field.
        Field match = null;
        for (final Class<?> class1 : ClassUtils.getAllInterfaces(cls)) {
            try {
                final Field test = class1.getField(fieldName);
                Validate.isTrue(
                        match == null,
                        "Reference to field %s is ambiguous relative to %s"
                                + "; a matching field exists on two or more implemented interfaces.",
                        fieldName,
                        cls);
                match = test;
            } catch (final NoSuchFieldException ex) { // NOPMD
                // ignore
            }
        }
        return match;
    }
}
