/*
 * 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.tomcat.util.xreflection;


import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.tomcat.util.IntrospectionUtils;

public final class ObjectReflectionPropertyInspector {

    public static void main(String... args) throws Exception {
        if (args.length == 0) {
            System.err.println("Usage:\n\t"+
                "org.apache.tomcat.util.xreflection.ObjectReflectionPropertyInspector" +
                " <destination directory>"
            );
            System.exit(1);
        }

        File outputDir = new File(args[0]);
        if (!outputDir.exists() || !outputDir.isDirectory()) {
            System.err.println("Invalid output directory: "+ outputDir.getAbsolutePath());
            System.exit(1);
        }


        Set<SetPropertyClass> baseClasses = getKnownClasses()
            .stream()
            .map(ObjectReflectionPropertyInspector::processClass)
            .collect(Collectors.toCollection(LinkedHashSet::new));
        generateCode(
            baseClasses,
            "org.apache.tomcat.util",
            outputDir,
            "XReflectionIntrospectionUtils"
        );
    }

    private static Set<Class<?>> getKnownClasses() throws ClassNotFoundException {
        return
            Collections.unmodifiableSet(new LinkedHashSet<>(
                    Arrays.asList(
                        Class.forName("org.apache.catalina.authenticator.jaspic.SimpleAuthConfigProvider"),
                        Class.forName("org.apache.catalina.authenticator.jaspic.PersistentProviderRegistrations$Property"),
                        Class.forName("org.apache.catalina.authenticator.jaspic.PersistentProviderRegistrations$Provider"),
                        Class.forName("org.apache.catalina.connector.Connector"),
                        Class.forName("org.apache.catalina.core.ContainerBase"),
                        Class.forName("org.apache.catalina.core.StandardContext"),
                        Class.forName("org.apache.catalina.core.StandardEngine"),
                        Class.forName("org.apache.catalina.core.StandardHost"),
                        Class.forName("org.apache.catalina.core.StandardServer"),
                        Class.forName("org.apache.catalina.core.StandardService"),
                        Class.forName("org.apache.catalina.filters.AddDefaultCharsetFilter"),
                        Class.forName("org.apache.catalina.filters.RestCsrfPreventionFilter"),
                        Class.forName("org.apache.catalina.loader.ParallelWebappClassLoader"),
                        Class.forName("org.apache.catalina.loader.WebappClassLoaderBase"),
                        Class.forName("org.apache.catalina.realm.UserDatabaseRealm"),
                        Class.forName("org.apache.catalina.valves.AccessLogValve"),
                        Class.forName("org.apache.coyote.AbstractProtocol"),
                        Class.forName("org.apache.coyote.ajp.AbstractAjpProtocol"),
                        Class.forName("org.apache.coyote.ajp.AjpNio2Protocol"),
                        Class.forName("org.apache.coyote.ajp.AjpNioProtocol"),
                        Class.forName("org.apache.coyote.http11.AbstractHttp11Protocol"),
                        Class.forName("org.apache.coyote.http11.Http11Nio2Protocol"),
                        Class.forName("org.apache.coyote.http11.Http11NioProtocol"),
                        Class.forName("org.apache.tomcat.util.descriptor.web.ContextResource"),
                        Class.forName("org.apache.tomcat.util.descriptor.web.ResourceBase"),
                        Class.forName("org.apache.tomcat.util.modeler.AttributeInfo"),
                        Class.forName("org.apache.tomcat.util.modeler.FeatureInfo"),
                        Class.forName("org.apache.tomcat.util.modeler.ManagedBean"),
                        Class.forName("org.apache.tomcat.util.modeler.OperationInfo"),
                        Class.forName("org.apache.tomcat.util.modeler.ParameterInfo"),
                        Class.forName("org.apache.tomcat.util.net.AbstractEndpoint"),
                        Class.forName("org.apache.tomcat.util.net.AbstractNetworkChannelEndpoint"),
                        Class.forName("org.apache.tomcat.util.net.Nio2Endpoint"),
                        Class.forName("org.apache.tomcat.util.net.NioEndpoint"),
                        Class.forName("org.apache.tomcat.util.net.SocketProperties")
                    )
                )
            );
    }

    //types of properties that IntrospectionUtils.setProperty supports
    private static final Set<Class<?>> ALLOWED_TYPES = Collections.unmodifiableSet(new LinkedHashSet<>(
        Arrays.asList(
            Boolean.TYPE,
            Integer.TYPE,
            Long.TYPE,
            String.class,
            InetAddress.class
        )
    ));
    private static final Map<Class<?>, SetPropertyClass> classes = new LinkedHashMap<>();

    public static void generateCode(Set<SetPropertyClass> baseClasses, String packageName, File location, String className) throws Exception {
        String packageDirectory = packageName.replace('.','/');
        File sourceFileLocation = new File(location, packageDirectory);
        ReflectionLessCodeGenerator.generateCode(sourceFileLocation, className, packageName, baseClasses);
    }


    private static boolean isAllowedField(Field field) {
        return ALLOWED_TYPES.contains(field.getType()) && !Modifier.isFinal(field.getModifiers());
    }

    private static boolean isAllowedSetMethod(Method method) {
        return method.getName().startsWith("set") &&
            method.getParameterTypes().length == 1 &&
            ALLOWED_TYPES.contains(method.getParameterTypes()[0]) &&
            !Modifier.isPrivate(method.getModifiers());
    }

    private static boolean isAllowedGetMethod(Method method) {
        return (method.getName().startsWith("get") || method.getName().startsWith("is")) &&
            method.getParameterTypes().length == 0 &&
            ALLOWED_TYPES.contains(method.getReturnType()) &&
            !Modifier.isPrivate(method.getModifiers());
    }


    private static SetPropertyClass getOrCreateSetPropertyClass(Class<?> clazz) {
        boolean base = (clazz.getSuperclass() == null || clazz.getSuperclass() == Object.class);
        SetPropertyClass spc = classes.get(clazz);
        if (spc == null) {
            spc = new SetPropertyClass(clazz, base ? null : getOrCreateSetPropertyClass(clazz.getSuperclass()));
            classes.put(clazz, spc);
        }
        return spc;
    }

    static Method findGetter(Class<?> declaringClass, String propertyName) {
        for (String getterName : Arrays.asList("get" + IntrospectionUtils.capitalize(propertyName), "is" + propertyName)) {
            try {
                Method method = declaringClass.getMethod(getterName);
                if (!Modifier.isPrivate(method.getModifiers())) {
                    return method;
                }
            } catch (NoSuchMethodException e) {
                // Ignore
            }
        }
        try {
            Method method = declaringClass.getMethod("getProperty", String.class, String.class);
            if (!Modifier.isPrivate(method.getModifiers())) {
                return method;
            }
        } catch (NoSuchMethodException e) {
            // Ignore
        }

        return null;
    }

    static Method findSetter(Class<?> declaringClass, String propertyName, Class<?> propertyType) {
        try {
            Method method = declaringClass.getMethod("set" + IntrospectionUtils.capitalize(propertyName), propertyType);
            if (!Modifier.isPrivate(method.getModifiers())) {
                return method;
            }
        } catch (NoSuchMethodException e) {
            // Ignore
        }
        try {
            Method method = declaringClass.getMethod("setProperty", String.class, String.class);
            if (!Modifier.isPrivate(method.getModifiers())) {
                return method;
            }
        } catch (NoSuchMethodException e) {
            // Ignore
        }
        return null;
    }

    static String decapitalize(String name) {
        if (name == null || name.isEmpty()) {
            return name;
        }
        if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
            Character.isUpperCase(name.charAt(0))) {
            return name;
        }
        char[] chars = name.toCharArray();
        chars[0] = Character.toLowerCase(chars[0]);
        return new String(chars);
    }


    static SetPropertyClass processClass(Class<?> clazz) {
        SetPropertyClass spc = getOrCreateSetPropertyClass(clazz);
        final Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            if (isAllowedSetMethod(method)) {
                String propertyName = decapitalize(method.getName().substring(3));
                Class<?> propertyType = method.getParameterTypes()[0];
                Method getter = findGetter(clazz, propertyName);
                Method setter = findSetter(clazz, propertyName, propertyType);
                ReflectionProperty property = new ReflectionProperty(
                    spc.getClazz().getName(),
                    propertyName,
                    propertyType,
                    setter,
                    getter
                );
                spc.addProperty(property);
            } else if (isAllowedGetMethod(method)) {
                boolean startsWithIs = method.getName().startsWith("is");
                String propertyName = decapitalize(method.getName().substring(startsWithIs ? 2 : 3));
                Class<?> propertyType = method.getReturnType();
                Method getter = findGetter(clazz, propertyName);
                Method setter = findSetter(clazz, propertyName, propertyType);
                ReflectionProperty property = new ReflectionProperty(
                    spc.getClazz().getName(),
                    propertyName,
                    propertyType,
                    setter,
                    getter
                );
                spc.addProperty(property);
            }
        }

        final Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (isAllowedField(field)) {
                Method getter = findGetter(
                    field.getDeclaringClass(),
                    IntrospectionUtils.capitalize(field.getName())
                );
                Method setter = findSetter(
                    field.getDeclaringClass(),
                    IntrospectionUtils.capitalize(field.getName()),
                    field.getType()
                );
                ReflectionProperty property = new ReflectionProperty(
                    spc.getClazz().getName(),
                    field.getName(),
                    field.getType(),
                    setter,
                    getter
                );
                spc.addProperty(property);
            }
        }

        if (!spc.isBaseClass()) {
            SetPropertyClass parent = getOrCreateSetPropertyClass(spc.getClazz().getSuperclass());
            parent.addSubClass(spc);
            return processClass(parent.getClazz());
        } else {
            return spc;
        }
    }
}
