/*

   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.batik.test.xml;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.List;
import java.util.ArrayList;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * This helper class can be used to build Java object from their
 * XML description.
 *
 * @author <a href="mailto:vhardy@apache.org">Vincent Hardy</a>
 * @version $Id: XMLReflect.java 1810083 2017-09-29 10:39:45Z ssteiner $
 */
public class XMLReflect implements XMLReflectConstants{
    /**
     * An error happened while trying to construct a test. No constructor
     * matching the list of arguments could be found
     * {0} : The test's class name
     * {1} : The list of argument types for which no constructor was found
     */
    public static final String NO_MATCHING_CONSTRUCTOR
        = "xml.XMLReflect.error.no.matching.constructor";

    /**
     * Implementation helper: builds a generic object
     */
    public static Object buildObject(Element element) throws Exception {

        Element classDefiningElement =
            getClassDefiningElement(element);

        String className
            = classDefiningElement.getAttribute(XR_CLASS_ATTRIBUTE);

        Class cl = Class.forName(className);
        Object[] argsArray = null;
        Class[]  argsClasses = null;

        NodeList children = element.getChildNodes();
        if(children != null && children.getLength() > 0){
            int n = children.getLength();
            List args = new ArrayList();
            for(int i=0; i<n; i++){
                Node child = children.item(i);
                if(child.getNodeType() == Node.ELEMENT_NODE){
                    Element childElement = (Element)child;
                    String tagName = childElement.getTagName().intern();
                    if(tagName == XR_ARG_TAG){
                        Object arg = buildArgument(childElement);
                        args.add(arg);
                    }
                }
            }

            if(args.size() > 0){
                argsArray = new Object[args.size()];
                args.toArray(argsArray);

                argsClasses = new Class[args.size()];

                for(int i=0; i<args.size(); i++){
                    argsClasses[i] = argsArray[i].getClass();
                }
            }
        }

        Constructor constructor
            = getDeclaredConstructor(cl, argsClasses);

        if (constructor == null) {
            String argsClassesStr = "null";
            if (argsClasses != null) {
                argsClassesStr = "";
                for (Class argsClass : argsClasses) {
                    argsClassesStr += argsClass.getName() + " / ";
                }
            }
            throw new Exception(Messages.formatMessage(NO_MATCHING_CONSTRUCTOR,
                                                       new Object[] { className,
                                                                      argsClassesStr }));
        }
        return configureObject(constructor.newInstance(argsArray),
                               element, classDefiningElement);
    }

    /**
     * Implementation helper: configures a generic object
     */
    public static Object configureObject(Object obj,
                                         Element element,
                                         Element classDefiningElement) throws Exception {
        // First, build a vector of elements from the child element
        // to the classDefiningElement so that we can go from the
        // top (classDefiningElement) to the child and apply properties
        // as we iterate
        List v = new ArrayList();
        v.add(element);
        while (element != classDefiningElement) {
            element = (Element) element.getParentNode();
            v.add(element);
        }

        int ne = v.size();
        for (int j=ne-1; j>=0; j--) {
            element = (Element)v.get(j);
            NodeList children = element.getChildNodes();
            if(children != null && children.getLength() > 0){
                int n = children.getLength();
                for(int i=0; i<n; i++){
                    Node child = children.item(i);
                    if(child.getNodeType() == Node.ELEMENT_NODE){
                        Element childElement = (Element)child;
                        String tagName = childElement.getTagName().intern();
                        if(tagName == XR_PROPERTY_TAG){
                            Object arg = buildArgument(childElement);
                            String propertyName
                                = childElement.getAttribute(XR_NAME_ATTRIBUTE);
                            setObjectProperty(obj, propertyName, arg);
                        }
                    }
                }

            }
        }

        return obj;
    }

    /**
     * Sets the property with given name on object to the input value
     */
    public static void setObjectProperty(Object obj,
                                         String propertyName,
                                         Object propertyValue)
        throws Exception {
        Class cl = obj.getClass();
        Method m = null;
        try {
            m = cl.getMethod("set" + propertyName,
                             new Class[]{propertyValue.getClass()});

        } catch (NoSuchMethodException e) {
            //
            // Check if the type was one of the primitive types, Double,
            // Float, Boolean or Integer
            //
            Class propertyClass = propertyValue.getClass();
            try {
                if (propertyClass == java.lang.Double.class) {
                    m = cl.getMethod("set" + propertyName,
                                     new Class[] {java.lang.Double.TYPE});
                } else if (propertyClass == java.lang.Float.class) {
                    m = cl.getMethod("set" + propertyName,
                                     new Class[] {java.lang.Float.TYPE});
                } else if (propertyClass == java.lang.Integer.class) {
                    m = cl.getMethod("set" + propertyName,
                                     new Class[] {java.lang.Integer.TYPE});
                } else if (propertyClass == java.lang.Boolean.class) {
                    m = cl.getMethod("set" + propertyName,
                                     new Class[] {java.lang.Boolean.TYPE});
                } else {
                    System.err.println("Could not find a set method for property : " + propertyName
                                       + " with value " + propertyValue + " and class " + propertyValue.getClass().getName());
                    throw e;
                }
            } catch (NoSuchMethodException nsme) {
                throw nsme;
            }
        }
        if(m != null){
            m.invoke(obj, new Object[]{propertyValue});
        }
    }


    /**
     * Returns a constructor that has can be used for the input class
     * types.
     */
    public static Constructor getDeclaredConstructor(Class cl,
                                                 Class[] argClasses){
        Constructor[] cs = cl.getDeclaredConstructors();
        for (Constructor c : cs) {
            Class[] reqArgClasses = c.getParameterTypes();
            if (reqArgClasses != null && reqArgClasses.length > 0) {
                if (reqArgClasses.length == argClasses.length) {
                    int j = 0;
                    for (; j < argClasses.length; j++) {
                        if (!reqArgClasses[j].isAssignableFrom(argClasses[j])) {
                            break;
                        }
                    }
                    if (j == argClasses.length) {
                        return c;
                    }
                }
            } else {
                if (argClasses == null || argClasses.length == 0) {
                    return c;
                }
            }
        }

        return null;
    }

    /**
     * Limitation: Arguments *must* have a String based
     * constructor. Or be an object that takes a set of string
     * based arguments.
     */
    public static Object buildArgument(Element element) throws Exception {
        if(!element.hasChildNodes()){
            Element classDefiningElement =
                getClassDefiningElement(element);

            String classAttr = classDefiningElement.getAttribute(XR_CLASS_ATTRIBUTE);

            // String based argument
            Class cl = Class.forName(classAttr);

            if(element.hasAttribute(XR_VALUE_ATTRIBUTE)){
                String value = element.getAttribute(XR_VALUE_ATTRIBUTE);


                Constructor constructor
                    = cl.getDeclaredConstructor(new Class[] { String.class });

                return constructor.newInstance(new Object[] {value});
            }
            else{
                // Default constructor
                return cl.getDeclaredConstructor().newInstance();
            }
        }
        else{
            return buildObject(element);
        }
    }

    /**
     * Gets the defining class element
     */
    public static Element getClassDefiningElement(Element element) {
        if(element != null){
            String classAttr = element.getAttribute(XR_CLASS_ATTRIBUTE);

            if(classAttr == null || "".equals(classAttr)){
                Node parent = element.getParentNode();
                if(parent != null && parent.getNodeType() == Node.ELEMENT_NODE){
                    return getClassDefiningElement((Element)parent);
                }
                else{
                    return null;
                }
            }

            return element;

        }

        return null;
    }
}
