/*
 * 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.openjpa.meta;

import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.PrivilegedActionException;
import java.util.ArrayList;
import java.util.List;

import org.apache.openjpa.enhance.PCRegistry;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.OpenJPAException;
import org.apache.openjpa.util.UserException;

/**
 * Abstract implementation provides a set of generic utilities for detecting
 * persistence meta-data of Field/Member. Also provides bean-style properties
 * such as access style or identity type to be used by default when such
 * information is not derivable from available meta-data.
 *
 * @author Abe White
 * @author Pinaki Poddar
 */
public abstract class AbstractMetaDataDefaults
    implements MetaDataDefaults {

    private static final Localizer _loc = Localizer.forPackage
        (AbstractMetaDataDefaults.class);

    private int _access = AccessCode.FIELD;
    private int _identity = ClassMetaData.ID_UNKNOWN;
    private boolean _ignore = true;
    private boolean _interface = true;
    private boolean _pcRegistry = true;
    private int _callback = CALLBACK_RETHROW;
    private boolean _unwrapped = false;

    /**
     * Whether to attempt to use the information from registered classes
     * to populate metadata defaults. Defaults to true.
     */
    public boolean getUsePCRegistry() {
        return _pcRegistry;
    }

    /**
     * Whether to attempt to use the information from registered classes
     * to populate metadata defaults. Defaults to true.
     */
    public void setUsePCRegistry(boolean pcRegistry) {
        _pcRegistry = pcRegistry;
    }

    /**
     * The default access type for base classes with ACCESS_UNKNOWN.
     * ACCESS_FIELD by default.
     */
    @Override
    public int getDefaultAccessType() {
        return _access;
    }

    /**
     * The default access type for base classes with ACCESS_UNKNOWN.
     * ACCESS_FIELD by default.
     */
    public void setDefaultAccessType(int access) {
        _access = access;
    }

    /**
     * The default identity type for unmapped classes without primary
     * key fields. ID_UNKNOWN by default.
     */
    @Override
    public int getDefaultIdentityType() {
        return _identity;
    }

    /**
     * The default identity type for unmapped classes without primary
     * key fields. ID_UNKNOWN by default.
     */
    public void setDefaultIdentityType(int identity) {
        _identity = identity;
    }

    @Override
    public int getCallbackMode() {
        return _callback;
    }

    public void setCallbackMode(int mode) {
        _callback = mode;
    }

    public void setCallbackMode(int mode, boolean on) {
        if (on)
            _callback |= mode;
        else
            _callback &= ~mode;
    }

    @Override
    public boolean getCallbacksBeforeListeners(int type) {
        return false;
    }

    @Override
    public boolean isDeclaredInterfacePersistent() {
        return _interface;
    }

    public void setDeclaredInterfacePersistent(boolean pers) {
        _interface = pers;
    }

    @Override
    public boolean isDataStoreObjectIdFieldUnwrapped() {
        return _unwrapped;
    }

    public void setDataStoreObjectIdFieldUnwrapped(boolean unwrapped) {
        _unwrapped = unwrapped;
    }

    public boolean getIgnoreNonPersistent() {
        return _ignore;
    }

    @Override
    public void setIgnoreNonPersistent(boolean ignore) {
        _ignore = ignore;
    }

    @Override
    public void populate(ClassMetaData meta, int access) {
        populate(meta, access, false);
    }

    @Override
    public void populate(ClassMetaData meta, int access, boolean ignoreTransient) {
        if (meta.getDescribedType() == Object.class)
            return;
        meta.setAccessType(access);

        Log log = meta.getRepository().getLog();
        if (log.isTraceEnabled())
            log.trace(_loc.get("gen-meta", meta));
        if (!_pcRegistry || !populateFromPCRegistry(meta)) {
            if (log.isTraceEnabled())
                log.trace(_loc.get("meta-reflect"));
            populateFromReflection(meta, ignoreTransient);
        }
    }

    /**
     * Populate the given metadata using the {@link PCRegistry}.
     */
    private boolean populateFromPCRegistry(ClassMetaData meta) {
        Class<?> cls = meta.getDescribedType();
        if (!PCRegistry.isRegistered(cls))
            return false;
        try {
            String[] fieldNames = PCRegistry.getFieldNames(cls);
            Class<?>[] fieldTypes = PCRegistry.getFieldTypes(cls);
            Member member;
            FieldMetaData fmd;
            for (int i = 0; i < fieldNames.length; i ++) {
                String property = fieldNames[i];
                member = getMemberByProperty(meta, property, AccessCode.UNKNOWN, true);
                if (member == null) { // transient or indeterminable access
                    continue;
                }
                fmd = meta.addDeclaredField(property, fieldTypes[i]);
                fmd.backingMember(member);
                populate(fmd);
            }
            return true;
        } catch (OpenJPAException ke) {
            throw ke;
        } catch (Exception e) {
            if (e instanceof PrivilegedActionException)
                e = ((PrivilegedActionException) e).getException();
            throw new UserException(e);
        }
    }

    protected abstract List<Member> getPersistentMembers(ClassMetaData meta, boolean ignoreTransient);
    /**
     * Generate the given meta-data using reflection.
     * Adds FieldMetaData for each persistent state.
     * Delegate to concrete implementation to determine the persistent
     * members.
     */
    private void populateFromReflection(ClassMetaData meta, boolean ignoreTransient) {
        List<Member> members = getPersistentMembers(meta, ignoreTransient);
        boolean iface = meta.getDescribedType().isInterface();
        // If access is mixed or if the default is currently unknown,
        // process all fields, otherwise only process members of the class
        // level default access type.

        String name;
        boolean def;
        FieldMetaData fmd;
        for (Member member : members) {
            name = getFieldName(member);
            if (name == null || isReservedFieldName(name))
                continue;

            def = isDefaultPersistent(meta, member, name, ignoreTransient);
            if (!def && _ignore)
                continue;

            // passed the tests; persistent type -- we construct with
            // Object.class because setting backing member will set proper
            // type anyway
            fmd = meta.addDeclaredField(name, Object.class);
            fmd.backingMember(member);
            if (!def) {
                fmd.setExplicit(true);
                fmd.setManagement(FieldMetaData.MANAGE_NONE);
            }
            populate(fmd);
        }
    }

    protected void populate(FieldMetaData fmd) {

    }

    /**
     * Return the list of fields in <code>meta</code> that use field access,
     * or <code>null</code> if a list of fields is unobtainable. An empty list
     * should be returned if the list of fields is obtainable, but there
     * happens to be no field access in <code>meta</code>.
     *
     * This is used for error reporting purposes only, so need not be efficient.
     *
     * This implementation returns <code>null</code>.
     */
    protected List<String> getFieldAccessNames(ClassMetaData meta) {
        return null;
    }

    /**
     * Return the list of methods in <code>meta</code> that use property access,
     * or <code>null</code> if a list of methods is unobtainable. An empty list
     * should be returned if the list of methods is obtainable, but there
     * happens to be no property access in <code>meta</code>.
     *
     * This is used for error reporting purposes only, so need not be efficient.
     *
     * This implementation returns <code>null</code>.
     */
    protected List<String> getPropertyAccessNames(ClassMetaData meta) {
        return null;
    }

    /**
     * Return the field name for the given member. This will only be invoked
     * on members of the right type (field vs. method). Return null if the
     * member cannot be managed. Default behavior: For fields, returns the
     * field name. For getter methods, returns the minus "get" or "is" with
     * the next letter lower-cased. For other methods, returns null.
     */
    public static String getFieldName(Member member) {
        if (member instanceof Field) {
            return member.getName();
        }
        if (!(member instanceof Method)) {
            return null;
        }
        Method method = (Method) member;
        String name = method.getName();
        if (isNormalGetter(method)) {
            name = name.substring("get".length());
        }
        else if (isBooleanGetter(method)) {
            name = name.substring("is".length());
        }
        else {
            return null;
        }

        if (name.length() == 1) {
            return name.toLowerCase();
        }
        return Character.toLowerCase(name.charAt(0)) + name.substring(1);
    }

    /**
     * Returns true if the given field name is reserved for unmanaged fields.
     */
    protected boolean isReservedFieldName(String name) {
        // names used by enhancers
        return name.startsWith("openjpa") || name.startsWith("jdo");
    }

    /**
     * Return true if the given member is persistent by default. This will
     * only be invoked on members of the right type (field vs. method).
     * Returns false if member is static or final by default.
     *
     * @param name the field name from {@link #getFieldName}
     */
    protected abstract boolean isDefaultPersistent(ClassMetaData meta,
        Member member, String name, boolean ignoreTransient);

    /**
     * Gets the backing member of the given field. If the field has not been
     * assigned a backing member then get either the instance field or the
     * getter method depending upon the access style of the defining class.
     * <br>
     * Defining class is used instead of declaring class because this method
     * may be invoked during parsing phase when declaring metadata may not be
     * available.
     */
    @Override
    public Member getBackingMember(FieldMetaData fmd) {
        if (fmd == null)
            return null;
        if (fmd.getBackingMember() != null)
            return fmd.getBackingMember();
        return getMemberByProperty(fmd.getDeclaringMetaData(), fmd.getName(),
            fmd.getAccessType(), true);
    }

    @Override
    public Class<?> getUnimplementedExceptionType() {
        return UnsupportedOperationException.class;
    }

    /**
     * Helper method; returns true if the given class appears to be
     * user-defined.
     */
    protected static boolean isUserDefined(Class<?> cls) {
        return cls != null
                && !cls.getName().startsWith("java.")
                && !cls.getName().startsWith ("javax.")
                && !cls.getName().startsWith ("jakarta.");
    }

    /**
     * Affirms if the given method matches the following signature
     * <code> public T getXXX() </code>
     * where T is any non-void type.
     */
    public static boolean isNormalGetter(Method method) {
        String methodName = method.getName();
        return startsWith(methodName, "get")
            && method.getParameterTypes().length == 0
            && method.getReturnType() != void.class;
    }

    /**
     * Affirms if the given method matches the following signature
     * <code> public boolean isXXX() </code>
     * <code> public Boolean isXXX() </code>
     */
    public static boolean isBooleanGetter(Method method) {
        String methodName = method.getName();
        return startsWith(methodName, "is")
            && method.getParameterTypes().length == 0
            && isBoolean(method.getReturnType());
    }

    /**
     * Affirms if the given method signature matches bean-style getter method
     * signature.<br>
     * <code> public T getXXX()</code> where T is any non-void type.<br>
     * or<br>
     * <code> public T isXXX()</code> where T is boolean or Boolean.<br>
     */
    public static boolean isGetter(Method method, boolean includePrivate) {
        if (method == null)
            return false;
        int mods = method.getModifiers();
        if (!(Modifier.isPublic(mods)
              || Modifier.isProtected(mods)
              || (Modifier.isPrivate(mods) && includePrivate))
         || Modifier.isNative(mods)
         || Modifier.isStatic(mods))
            return false;
        return isNormalGetter(method) || isBooleanGetter(method);
    }

    /**
     * Affirms if the given full string starts with the given head.
     */
    public static boolean startsWith(String full, String head) {
        return full != null && head != null && full.startsWith(head)
            && full.length() > head.length();
    }

    public static boolean isBoolean(Class<?> cls) {
        return cls == boolean.class || cls == Boolean.class;
    }

    public static List<String> toNames(List<? extends Member> members) {
        List<String> result = new ArrayList<>();
        for (Member m : members)
            result.add(m.getName());
        return result;
    }

}
