package de.renew.util;


/**
 * Implement the typing rules for Java reference nets.
 * <p>
 * Assignment conversions are not implemented.
 * <p>
 * A special anonymous class serves as a representative of the untyped.
 * <p>
 * The null type is represented by the object null.
 */
public class Types {
    /**
     * The all-instances class logger entity for Types.
     */
    public final static org.apache.log4j.Logger logger =
        org.apache.log4j.Logger.getLogger(Types.class);

    /**
     * Create a new anonymous class that does not
     * differ from java.lang.Object except that it is
     * a different class, then create a single object
     * of that class and extract the class object from it.
     */
    public static final Class<?> UNTYPED = new Object() {}.getClass();
    /**
     * An empty string array is used if no package names should be displayed while converting a class to a string.
     *
     */
    public static final String[] NOPACKAGES = null;
    /**
     * A string array of all packages that need to be displayed while converting a class to a string.
     */
    public static final String[] ALLPACKAGES = { };

    // Add a test suite. Very rudimentary.
    private static int _assertionCount = 0;

    /**
     * This class is completely static. Do not instantiate me.
     */
    private Types() {}

    /**
     * Convert from primitive types to the associated classes.
     *
     * @param clazz the class to be associated if it does differ from java.lang.Object.
     * @return The extract class object from clazz or the associated classes if it does differ from java.lang.Object.
     */
    public static Class<?> objectify(Class<?> clazz) {
        if (clazz == Boolean.TYPE) {
            return Boolean.class;
        } else if (clazz == Character.TYPE) {
            return Character.class;
        } else if (clazz == Byte.TYPE) {
            return Byte.class;
        } else if (clazz == Short.TYPE) {
            return Short.class;
        } else if (clazz == Integer.TYPE) {
            return Integer.class;
        } else if (clazz == Long.TYPE) {
            return Long.class;
        } else if (clazz == Float.TYPE) {
            return Float.class;
        } else if (clazz == Double.TYPE) {
            return Double.class;
        }
        return clazz;
    }

    /**
     * Determine if the argument class is final.
     *
     * @param clazz the class object to be to find out if it is final.
     * @return True if the argument class includes the final modifier.
     */
    public static boolean isFinal(Class<?> clazz) {
        if (clazz == null || clazz == UNTYPED) {
            // Hmm, what do make of these two cases?
            // Nobody inherits from them, surely.
            return true;
        }
        return (clazz.getModifiers() & java.lang.reflect.Modifier.FINAL) != 0;
    }

    /**
     * Determine if a conversion is possible by an identity conversion.
     *
     * @param from The source class object to be conversion form.
     * @param to The target class object to be conversion to.
     * @return true if the identity conversion possible is.
     */
    public static boolean allowsIdentityConversion(Class<?> from, Class<?> to) {
        return to == from;
    }

    /**
     * Determine if a conversion is possible by a primitive
     * widening conversion.
     *
     * @param from The source class object to be conversion form.
     * @param to The target class object to be conversion to.
     * @return True if to is numerical integral and from is byte. False otherwise.
     */
    public static boolean allowsPrimitiveWidening(Class<?> from, Class<?> to) {
        // Exclude conversions to null or the untyped.
        // They should not occur.
        if (to == null || to == UNTYPED) {
            return false;
        }

        // Exclude identity conversions.
        if (to == from) {
            return false;
        }

        // Exclude the null type and the untyped.
        if (from == null || from == UNTYPED) {
            return false;
        }

        // Restrict attention to primitive types on both sides.
        if (!to.isPrimitive() || !from.isPrimitive()) {
            return false;
        }

        // Treat void first.
        if (to == Void.TYPE || from == Void.TYPE) {
            return false;
        }

        // Same goes for booleans.
        if (to == Boolean.TYPE || from == Boolean.TYPE) {
            return false;
        }

        // to and from must both be numeric or character types.
        if (to == Character.TYPE) {
            return false;
        }

        // to must be numeric.
        if (to == Double.TYPE) {
            return true;
        }
        if (from == Double.TYPE) {
            return false;
        }
        if (to == Float.TYPE) {
            return true;
        }
        if (from == Float.TYPE) {
            return false;
        }

        // to must be numeric integral, from must be integral.
        if (to == Long.TYPE) {
            return true;
        }
        if (from == Long.TYPE) {
            return false;
        }
        if (to == Integer.TYPE) {
            return true;
        }
        return from == Byte.TYPE;

        // The remaining conversions are not allowed.
    }

    /**
     * Determine if a conversion is possible by a reference
     * widening conversion.
     *
     * @param from The source class object to be conversion form.
     * @param to The target class object to be conversion to.
     * @return True if the class object to is compatible to be assigned to the instance of the class from. False otherwise.
     */
    public static boolean allowsReferenceWidening(Class<?> from, Class<?> to) {
        // Exclude conversions to null or the untype.
        // They should not occur.
        if (to == null || to == UNTYPED) {
            return false;
        }

        // Exclude identity conversions.
        if (to == from) {
            return false;
        }

        // Exclude the untyped.
        if (from == UNTYPED) {
            return false;
        }

        // Treat the null type.
        if (from == null) {
            return !to.isPrimitive();
        }

        // Restrict attention to reference types on both sides.
        if (to.isPrimitive() || from.isPrimitive()) {
            return false;
        }

        // The check is provided by the Java runtime library.
        return to.isAssignableFrom(from);
    }

    /**
     * Determine if a source and target type permit
     * a lossless conversion.
     *
     * @param from The source class object to be conversion form.
     * @param to The target class object to be conversion to.
     * @return True if the source class type can be converted losslessly into the
     * expected target type. False otherwise.
     */
    public static boolean allowsLosslessWidening(Class<?> from, Class<?> to) {
        // Widening conversions are the only candidates for lossless
        // conversions.
        if (!allowsWideningConversion(from, to)) {
            return false;
        }

        // Exclude the three bad casts that remain.
        return (from != Integer.TYPE || to != Float.TYPE) && (from != Long.TYPE || to != Float.TYPE)
            && (from != Long.TYPE || to != Double.TYPE);

        // Everything is ok.
    }

    /**
     * Determine if a formal and an actual parameter would match for
     * a method or constructor call.
     *
     * @param from The source class object to be conversion form.
     * @param to The target class object to be conversion to.
     * @return True if a reference, identity or primitive conversion is allowed. False otherwise.
     */
    public static boolean allowsWideningConversion(Class<?> from, Class<?> to) {
        return allowsIdentityConversion(from, to) || allowsPrimitiveWidening(from, to)
            || allowsReferenceWidening(from, to);
    }

    /**
     * Determine if formal and actual parameters would match for
     * a method or constructor call.
     *
     * @param from An array of source classes to be conversion form.
     * @param to An array of target classes object to be conversion to.
     * @return True if a reference, identity or primitive conversion for all element are allowed. False otherwise.
     */
    public static boolean allowsWideningConversion(Class<?>[] from, Class<?>[] to) {
        if (from.length != to.length) {
            return false;
        }
        for (int i = 0; i < from.length; i++) {
            if (!allowsWideningConversion(from[i], to[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Determine if a cast is permitted at compile time.
     *
     * @param from The source class to be cast form.
     * @param to The target class object to be cast to.
     * @return True if a reference, identity or primitive conversion from the source class to the target class are allowed
     *          and if the return types of the source and target classes agree.
     */
    public static boolean allowsCast(Class<?> from, Class<?> to) {
        // Exclude conversions to null or the untyped.
        // They should not occur.
        if (to == null || to == UNTYPED) {
            return false;
        }

        // Treat the null type.
        if (from == null) {
            return !to.isPrimitive();
        }

        // Treat the untype.
        if (from == UNTYPED) {
            return true;
        }

        // Reduce array types.
        while (from.isArray() && to.isArray()) {
            from = from.getComponentType();
            to = to.getComponentType();
        }

        // Consider the identity conversion.
        if (allowsIdentityConversion(from, to)) {
            return true;
        }

        // Treat primitive types;
        if (to.isPrimitive() || from.isPrimitive()) {
            if (!to.isPrimitive() || !from.isPrimitive()) {
                return false;
            }


            // Everything that is not a void or a boolean can be converted.
            // Note that we have already treated identity conversions.
            return to != Boolean.TYPE && from != Boolean.TYPE && to != Void.TYPE
                && from != Void.TYPE;
        }

        // Treat reference types.
        if (allowsReferenceWidening(from, to)) {
            return true;
        }
        if (allowsReferenceWidening(to, from)) {
            return true;
        }


        // If either from or to is final, there is nothing more
        // we can do. Since array classes are final, this should
        // also eliminate arrays.
        if (isFinal(from)) {
            return false;
        }
        if (isFinal(to)) {
            return false;
        }


        // If neither from nor to are interfaces, there is nothing more
        // we can do.
        if (!to.isInterface() && !from.isInterface()) {
            return false;
        }


        // Do any methods disagree on the return type?
        // 
        // (Actually this check is stronger than documented by the JLS,
        // which allows class types to be cast to reference types
        // even if the return types of some methods disagree. This is
        // not sensible, because such a cast must fail at runtime
        // in any case.)
        java.lang.reflect.Method[] methods = from.getMethods();
        for (java.lang.reflect.Method method : methods) {
            try {
                java.lang.reflect.Method otherMethod =
                    to.getMethod(method.getName(), method.getParameterTypes());
                if (otherMethod.getReturnType() != method.getReturnType()) {
                    return false;
                }
            } catch (Exception ignored) {
            }
        }
        return true;
    }

    /**
     * Convert object classes to primitive types.
     *
     * @param clazz The class object to be converted to primitive types.
     * @exception RuntimeException if the object class cannot be converted losslessly into the expected primitive type.
     * @return The converted primitive type.
     */
    public static Class<?> typify(Class<?> clazz) {
        if (clazz == Void.class) {
            return Void.TYPE;
        } else if (clazz == Boolean.class) {
            return Boolean.TYPE;
        } else if (clazz == Character.class) {
            return Character.TYPE;
        } else if (clazz == Byte.class) {
            return Byte.TYPE;
        } else if (clazz == Short.class) {
            return Short.TYPE;
        } else if (clazz == Integer.class) {
            return Integer.TYPE;
        } else if (clazz == Long.class) {
            return Long.TYPE;
        } else if (clazz == Float.class) {
            return Float.TYPE;
        } else if (clazz == Double.class) {
            return Double.TYPE;
        }
        throw new RuntimeException("Cannot make primitive type from " + clazz);
    }

    /**
     * Convert a class to a string using {@link #typeToString(Class, String[])} methode.
     *
     * @param clazz the class to be converted to a string
     * @return the type string
     */

    public static String typeToString(Class<?> clazz) {
        return typeToString(clazz, ALLPACKAGES);
    }

    /**
     * Convert a class to a string, while keeping track of well-known
     * packages that do not need to be printed.
     *
     * @param clazz the class to be converted to a string
     * @param packages the packages that do not need to be
     *   displayed as a string array, or an empty string array if
     *   all packages should be displayed, or null, if no package names
     *   should be displayed.
     * @return the type string
     */
    public static String typeToString(Class<?> clazz, String[] packages) {
        if (clazz == UNTYPED) {
            return "untyped";
        } else if (clazz == null) {
            return "null";
        }

        StringBuilder buf = new StringBuilder();
        while (clazz.isArray()) {
            buf.append("[]");
            clazz = clazz.getComponentType();
        }

        String baseName = clazz.getName();
        String pureName = getPureName(baseName);
        String peckage = null;
        if (packages != null) {
            peckage = getPackageName(baseName);
            for (int i = 0; peckage != null && i < packages.length; i++) {
                if (peckage.equals(packages[i])) {
                    peckage = null;
                    break;
                }
            }
        }

        StringBuilder result = new StringBuilder();
        if (peckage != null) {
            result.append(peckage).append('.');
        }
        return result.append(pureName).append(buf).toString();
    }

    /**
     * Get a string of package name of a class using {@link #typeToString(Class, String[])} and {@link #getPackageName(String)} methods.
     *
     * @param clazz The source class.
     * @return the package name as string
     */
    public static String getPackageName(Class<?> clazz) {
        return getPackageName(typeToString(clazz, ALLPACKAGES));
    }

    /**
     * Get a string of pure name of a class using {@link #typeToString(Class, String[])} and {@link #getPureName(String)} methods.
     *
     * @param clazz The source class.
     * @return the pure name as string
     */
    public static String getPureName(Class<?> clazz) {
        return getPureName(typeToString(clazz, ALLPACKAGES));
    }

    /**
     * Get a string of package name of a class. Used after convert it to string using {@link #typeToString(Class, String[])} method.
     *
     * @param className The source class.
     * @return the package name as string
     */
    public static String getPackageName(String className) {
        int lastDotPos = className.lastIndexOf(".");
        if (lastDotPos < 0) {
            return null;
        } else {
            return className.substring(0, lastDotPos);
        }
    }

    /**
     * Get a string of pure name of a class. Used after convert it to string using {@link #typeToString(Class, String[])} method.
     *
     * @param className The source class.
     * @return the pure name as string
     */
    public static String getPureName(String className) {
        int lastDotPos = className.lastIndexOf(".");
        if (lastDotPos < 0) {
            return className;
        } else {
            return className.substring(lastDotPos + 1);
        }
    }

    private static void assertTrue(boolean correct) {
        _assertionCount++;
        if (!correct) {
            logger.error("Assertion " + _assertionCount + " failed.");
            System.exit(-1);
        }
    }

    /**
     * Test the typing rules for Java reference nets .
     *
     * @param args A string array of arguments to pass to the main method
     */
    public static void main(String[] args) {
        assertTrue(!allowsCast(Integer.class, Integer.TYPE));
        assertTrue(allowsCast(null, Integer.class));
        assertTrue(!allowsCast(null, Integer.TYPE));
        assertTrue(allowsCast(Object.class, Integer.class));
        assertTrue(allowsCast(Object.class, Object[].class));
        assertTrue(allowsCast(Cloneable.class, Object[].class));
        assertTrue(!allowsCast(String.class, Number.class));
        assertTrue(!allowsCast(String.class, Number.class));
        assertTrue("int[]".equals(typeToString(int[].class, ALLPACKAGES)));
        assertTrue("int[]".equals(typeToString(int[].class, NOPACKAGES)));
        assertTrue("java.lang.Object[]".equals(typeToString(Object[].class, ALLPACKAGES)));
        assertTrue("Object[]".equals(typeToString(Object[].class, NOPACKAGES)));
        assertTrue(
            "java.lang.Integer".equals(typeToString(Integer.class, new String[] { "java.lung" })));
        assertTrue("java.lang.Integer".equals(typeToString(Integer.class)));
        assertTrue("Integer".equals(typeToString(Integer.class, new String[] { "java.lang" })));
    }
}