/*
 * 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.maven.plugin.coreit;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

/**
 * Assists in serializing primitives and beans into properties for later inspection/verification.
 *
 * @author Benjamin Bentmann
 *
 */
class PropertyUtil {

    private static final Object[] NO_ARGS = {};

    private static final Class[] NO_PARAMS = {};

    /**
     * Serializes the specified object into the given properties, using the provided key. The object may be a scalar
     * value like a string or some array/collection/map or a bean.
     *
     * @param props The properties to serialize into, must not be <code>null</code>.
     * @param key   The key to use for serialization of the object data, must not be <code>null</code>.
     * @param obj   The object to serialize, may be <code>null</code>.
     */
    public static void store(Properties props, String key, Object obj) {
        store(props, key, obj, new HashSet());
    }

    /**
     * Serializes the specified object into the given properties, using the provided key. The object may be a scalar
     * value like a string or some array/collection/map or a bean.
     *
     * @param props   The properties to serialize into, must not be <code>null</code>.
     * @param key     The key to use for serialization of the object data, must not be <code>null</code>.
     * @param obj     The object to serialize, may be <code>null</code>.
     * @param visited The set/stack of already visited objects, used to detect back references in the object graph, must
     *                not be <code>null</code>.
     */
    private static void store(Properties props, String key, Object obj, Collection visited) {
        if (obj != null && !visited.contains(obj)) {
            visited.add(obj);
            if ((obj instanceof String)
                    || (obj instanceof Number)
                    || (obj instanceof Boolean)
                    || (obj instanceof File)
                    || (obj instanceof Path)) {
                props.put(key, obj.toString());
            } else if (obj instanceof Collection) {
                Collection coll = (Collection) obj;
                props.put(key, Integer.toString(coll.size()));
                int index = 0;
                for (Iterator it = coll.iterator(); it.hasNext(); index++) {
                    Object elem = it.next();
                    store(props, key + "." + index, elem, visited);
                }
            } else if (obj instanceof Map) {
                Map map = (Map) obj;
                props.put(key, Integer.toString(map.size()));
                @SuppressWarnings("checkstyle:UnusedLocalVariable")
                int index = 0;
                for (Iterator it = map.entrySet().iterator(); it.hasNext(); index++) {
                    Map.Entry entry = (Map.Entry) it.next();
                    store(props, key + "." + entry.getKey(), entry.getValue(), visited);
                }
            } else if (obj.getClass().isArray()) {
                int length = Array.getLength(obj);
                props.put(key, Integer.toString(length));
                for (int index = 0; index < length; index++) {
                    Object elem = Array.get(obj, index);
                    store(props, key + "." + index, elem, visited);
                }
            } else if (obj.getClass().getName().endsWith("Xpp3Dom")) {
                Class type = obj.getClass();
                try {
                    Method getValue = type.getMethod("getValue", NO_PARAMS);
                    String value = (String) getValue.invoke(obj, NO_ARGS);

                    if (value != null) {
                        props.put(key + ".value", value);
                    }

                    Method getName = type.getMethod("getName", NO_PARAMS);

                    Method getChildren = type.getMethod("getChildren", NO_PARAMS);
                    Object[] children = (Object[]) getChildren.invoke(obj, NO_ARGS);

                    props.put(key + ".children", Integer.toString(children.length));

                    Map indices = new HashMap();
                    for (Object child : children) {
                        String name = (String) getName.invoke(child, NO_ARGS);

                        Integer index = (Integer) indices.get(name);
                        if (index == null) {
                            index = 0;
                        }

                        store(props, key + ".children." + name + "." + index, child, visited);

                        indices.put(name, index + 1);
                    }
                } catch (Exception e) {
                    // can't happen
                }
            } else {
                Class type = obj.getClass();
                Method[] methods = type.getMethods();
                for (Method method : methods) {
                    if (Modifier.isStatic(method.getModifiers())
                            || method.getParameterTypes().length > 0
                            || !method.getName().matches("(get|is)\\p{Lu}.*")
                            || method.getName().endsWith("AsMap")
                            || Class.class.isAssignableFrom(method.getReturnType())
                            || Object.class.equals(method.getReturnType())) {
                        continue;
                    }

                    try {
                        Object value = method.invoke(obj, NO_ARGS);
                        store(props, key + "." + getPropertyName(method.getName()), value, visited);
                    } catch (Exception e) {
                        // just ignore
                    }
                }
            }
            visited.remove(obj);
        }
    }

    /**
     * Derives the bean property name from the specified method for its getter.
     *
     * @param methodName The method name of the property's getter, must not be <code>null</code>.
     * @return The property name, never <code>null</code>.
     */
    static String getPropertyName(String methodName) {
        String propertyName = methodName;
        if (methodName.startsWith("get") && methodName.length() > 3) {
            propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
        } else if (methodName.startsWith("is") && methodName.length() > 2) {
            propertyName = Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
        }
        return propertyName;
    }

    /**
     * Writes the specified properties to the given file.
     *
     * @param props The properties to write, must not be <code>null</code>.
     * @param file  The output file for the properties, must not be <code>null</code>.
     * @throws IOException If the properties could not be written to the file.
     */
    public static void write(Properties props, File file) throws IOException {
        OutputStream out = null;
        try {
            file.getParentFile().mkdirs();
            out = new FileOutputStream(file);
            props.store(out, "MAVEN-CORE-IT-LOG");
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    // just ignore
                }
            }
        }
    }
}
