/**
 * Copyright (c) 2008, http://www.snakeyaml.org
 *
 * Licensed 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.yaml.snakeyaml.constructor;

import java.beans.IntrospectionException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;

import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.error.YAMLException;
import org.yaml.snakeyaml.introspector.Property;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeId;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.SequenceNode;
import org.yaml.snakeyaml.nodes.Tag;

/**
 * Construct a custom Java instance.
 */
public class Constructor extends SafeConstructor {
    private final Map<Tag, Class<? extends Object>> typeTags;
    protected final Map<Class<? extends Object>, TypeDescription> typeDefinitions;

    public Constructor() {
        this(Object.class);
    }

    /**
     * Create Constructor for the specified class as the root.
     * 
     * @param theRoot
     *            - the class (usually JavaBean) to be constructed
     */
    public Constructor(Class<? extends Object> theRoot) {
        this(new TypeDescription(checkRoot(theRoot)));
    }

    /**
     * Ugly Java way to check the argument in the constructor
     */
    private static Class<? extends Object> checkRoot(Class<? extends Object> theRoot) {
        if (theRoot == null) {
            throw new NullPointerException("Root class must be provided.");
        } else
            return theRoot;
    }

    public Constructor(TypeDescription theRoot) {
        if (theRoot == null) {
            throw new NullPointerException("Root type must be provided.");
        }
        this.yamlConstructors.put(null, new ConstructYamlObject());
        if (!Object.class.equals(theRoot.getType())) {
            rootTag = new Tag(theRoot.getType());
        }
        typeTags = new HashMap<Tag, Class<? extends Object>>();
        typeDefinitions = new HashMap<Class<? extends Object>, TypeDescription>();
        yamlClassConstructors.put(NodeId.scalar, new ConstructScalar());
        yamlClassConstructors.put(NodeId.mapping, new ConstructMapping());
        yamlClassConstructors.put(NodeId.sequence, new ConstructSequence());
        addTypeDescription(theRoot);
    }

    /**
     * Create Constructor for a class which does not have to be in the classpath
     * or for a definition from a Spring ApplicationContext.
     * 
     * @param theRoot
     *            fully qualified class name of the root class (usually
     *            JavaBean)
     * @throws ClassNotFoundException if cannot be loaded by the classloader
     */
    public Constructor(String theRoot) throws ClassNotFoundException {
        this(Class.forName(check(theRoot)));
    }

    private static final String check(String s) {
        if (s == null) {
            throw new NullPointerException("Root type must be provided.");
        }
        if (s.trim().length() == 0) {
            throw new YAMLException("Root type must be provided.");
        }
        return s;
    }

    /**
     * Make YAML aware how to parse a custom Class. If there is no root Class
     * assigned in constructor then the 'root' property of this definition is
     * respected.
     * 
     * @param definition
     *            to be added to the Constructor
     * @return the previous value associated with <tt>definition</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>definition</tt>.
     */
    public TypeDescription addTypeDescription(TypeDescription definition) {
        if (definition == null) {
            throw new NullPointerException("TypeDescription is required.");
        }
        Tag tag = definition.getTag();
        typeTags.put(tag, definition.getType());
        return typeDefinitions.put(definition.getType(), definition);
    }

    /**
     * Construct mapping instance (Map, JavaBean) when the runtime class is
     * known.
     */
    protected class ConstructMapping implements Construct {

        /**
         * Construct JavaBean. If type safe collections are used please look at
         * <code>TypeDescription</code>.
         * 
         * @param node
         *            node where the keys are property names (they can only be
         *            <code>String</code>s) and values are objects to be created
         * @return constructed JavaBean
         */
        public Object construct(Node node) {
            MappingNode mnode = (MappingNode) node;
            if (Properties.class.isAssignableFrom(node.getType())) {
                Properties properties = new Properties();
                if (!node.isTwoStepsConstruction()) {
                    constructMapping2ndStep(mnode, properties);
                } else {
                    throw new YAMLException("Properties must not be recursive.");
                }
                return properties;
            } else if (SortedMap.class.isAssignableFrom(node.getType())) {
                SortedMap<Object, Object> map = new TreeMap<Object, Object>();
                if (!node.isTwoStepsConstruction()) {
                    constructMapping2ndStep(mnode, map);
                }
                return map;
            } else if (Map.class.isAssignableFrom(node.getType())) {
                if (node.isTwoStepsConstruction()) {
                    return createDefaultMap();
                } else {
                    return constructMapping(mnode);
                }
            } else if (SortedSet.class.isAssignableFrom(node.getType())) {
                SortedSet<Object> set = new TreeSet<Object>();
                // XXX why this is not used ?
                // if (!node.isTwoStepsConstruction()) {
                constructSet2ndStep(mnode, set);
                // }
                return set;
            } else if (Collection.class.isAssignableFrom(node.getType())) {
                if (node.isTwoStepsConstruction()) {
                    return createDefaultSet();
                } else {
                    return constructSet(mnode);
                }
            } else {
                if (node.isTwoStepsConstruction()) {
                    return createEmptyJavaBean(mnode);
                } else {
                    return constructJavaBean2ndStep(mnode, createEmptyJavaBean(mnode));
                }
            }
        }

        @SuppressWarnings("unchecked")
        public void construct2ndStep(Node node, Object object) {
            if (Map.class.isAssignableFrom(node.getType())) {
                constructMapping2ndStep((MappingNode) node, (Map<Object, Object>) object);
            } else if (Set.class.isAssignableFrom(node.getType())) {
                constructSet2ndStep((MappingNode) node, (Set<Object>) object);
            } else {
                constructJavaBean2ndStep((MappingNode) node, object);
            }
        }

        protected Object createEmptyJavaBean(MappingNode node) {
            try {
                /**
                 * Using only default constructor. Everything else will be
                 * initialized on 2nd step. If we do here some partial
                 * initialization, how do we then track what need to be done on
                 * 2nd step? I think it is better to get only object here (to
                 * have it as reference for recursion) and do all other thing on
                 * 2nd step.
                 */
                java.lang.reflect.Constructor<?> c = node.getType().getDeclaredConstructor();
                c.setAccessible(true);
                return c.newInstance();
            } catch (Exception e) {
                throw new YAMLException(e);
            }
        }

        protected Object constructJavaBean2ndStep(MappingNode node, Object object) {
            flattenMapping(node);
            Class<? extends Object> beanType = node.getType();
            List<NodeTuple> nodeValue = node.getValue();
            for (NodeTuple tuple : nodeValue) {
                ScalarNode keyNode;
                if (tuple.getKeyNode() instanceof ScalarNode) {
                    // key must be scalar
                    keyNode = (ScalarNode) tuple.getKeyNode();
                } else {
                    throw new YAMLException("Keys must be scalars but found: " + tuple.getKeyNode());
                }
                Node valueNode = tuple.getValueNode();
                // keys can only be Strings
                keyNode.setType(String.class);
                String key = (String) constructObject(keyNode);
                try {
                    Property property = getProperty(beanType, key);
                    valueNode.setType(property.getType());
                    TypeDescription memberDescription = typeDefinitions.get(beanType);
                    boolean typeDetected = false;
                    if (memberDescription != null) {
                        switch (valueNode.getNodeId()) {
                        case sequence:
                            SequenceNode snode = (SequenceNode) valueNode;
                            Class<? extends Object> memberType = memberDescription
                                    .getListPropertyType(key);
                            if (memberType != null) {
                                snode.setListType(memberType);
                                typeDetected = true;
                            } else if (property.getType().isArray()) {
                                snode.setListType(property.getType().getComponentType());
                                typeDetected = true;
                            }
                            break;
                        case mapping:
                            MappingNode mnode = (MappingNode) valueNode;
                            Class<? extends Object> keyType = memberDescription.getMapKeyType(key);
                            if (keyType != null) {
                                mnode.setTypes(keyType, memberDescription.getMapValueType(key));
                                typeDetected = true;
                            }
                            break;
                        default: // scalar
                        }
                    }
                    if (!typeDetected && valueNode.getNodeId() != NodeId.scalar) {
                        // only if there is no explicit TypeDescription
                        Class<?>[] arguments = property.getActualTypeArguments();
                        if (arguments != null && arguments.length > 0) {
                            // type safe (generic) collection may contain the
                            // proper class
                            if (valueNode.getNodeId() == NodeId.sequence) {
                                Class<?> t = arguments[0];
                                SequenceNode snode = (SequenceNode) valueNode;
                                snode.setListType(t);
                            } else if (valueNode.getTag().equals(Tag.SET)) {
                                Class<?> t = arguments[0];
                                MappingNode mnode = (MappingNode) valueNode;
                                mnode.setOnlyKeyType(t);
                                mnode.setUseClassConstructor(true);
                            } else if (property.getType().isAssignableFrom(Map.class)) {
                                Class<?> ketType = arguments[0];
                                Class<?> valueType = arguments[1];
                                MappingNode mnode = (MappingNode) valueNode;
                                mnode.setTypes(ketType, valueType);
                                mnode.setUseClassConstructor(true);
                            } else {
                                // the type for collection entries cannot be
                                // detected
                            }
                        }
                    }

                    Object value = constructObject(valueNode);
                    // Correct when the property expects float but double was
                    // constructed
                    if (property.getType() == Float.TYPE || property.getType() == Float.class) {
                        if (value instanceof Double) {
                            value = ((Double) value).floatValue();
                        }
                    }
                    // Correct when the property a String but the value is binary
                    if (property.getType() == String.class && Tag.BINARY.equals(valueNode.getTag()) && value instanceof byte[]) {
                        value = new String((byte[])value);
                    }

                    property.set(object, value);
                } catch (Exception e) {
                    throw new ConstructorException("Cannot create property=" + key
                            + " for JavaBean=" + object, node.getStartMark(), e.getMessage(),
                            valueNode.getStartMark(), e);
                }
            }
            return object;
        }

        protected Property getProperty(Class<? extends Object> type, String name)
                throws IntrospectionException {
            return getPropertyUtils().getProperty(type, name);
        }
    }

    /**
     * Construct an instance when the runtime class is not known but a global
     * tag with a class name is defined. It delegates the construction to the
     * appropriate constructor based on the node kind (scalar, sequence,
     * mapping)
     */
    protected class ConstructYamlObject implements Construct {

        private Construct getConstructor(Node node) {
            Class<?> cl = getClassForNode(node);
            node.setType(cl);
            // call the constructor as if the runtime class is defined
            Construct constructor = yamlClassConstructors.get(node.getNodeId());
            return constructor;
        }

        public Object construct(Node node) {
            Object result = null;
            try {
                result = getConstructor(node).construct(node);
            } catch (ConstructorException e) {
                throw e;
            } catch (Exception e) {
                throw new ConstructorException(null, null, "Can't construct a java object for "
                        + node.getTag() + "; exception=" + e.getMessage(), node.getStartMark(), e);
            }
            return result;
        }

        public void construct2ndStep(Node node, Object object) {
            try {
                getConstructor(node).construct2ndStep(node, object);
            } catch (Exception e) {
                throw new ConstructorException(null, null,
                        "Can't construct a second step for a java object for " + node.getTag()
                                + "; exception=" + e.getMessage(), node.getStartMark(), e);
            }
        }
    }

    /**
     * Construct scalar instance when the runtime class is known. Recursive
     * structures are not supported.
     */
    protected class ConstructScalar extends AbstractConstruct {
        public Object construct(Node nnode) {
            ScalarNode node = (ScalarNode) nnode;
            Class<?> type = node.getType();
            Object result;
            if (type.isPrimitive() || type == String.class || Number.class.isAssignableFrom(type)
                    || type == Boolean.class || Date.class.isAssignableFrom(type)
                    || type == Character.class || type == BigInteger.class
                    || type == BigDecimal.class || Enum.class.isAssignableFrom(type)
                    || Tag.BINARY.equals(node.getTag()) || Calendar.class.isAssignableFrom(type) || type == UUID.class) {
                // standard classes created directly
                result = constructStandardJavaInstance(type, node);
            } else {
                // there must be only 1 constructor with 1 argument
                java.lang.reflect.Constructor<?>[] javaConstructors = type
                        .getDeclaredConstructors();
                int oneArgCount = 0;
                java.lang.reflect.Constructor<?> javaConstructor = null;
                for (java.lang.reflect.Constructor<?> c : javaConstructors) {
                    if (c.getParameterTypes().length == 1) {
                        oneArgCount++;
                        javaConstructor = c;
                    }
                }
                Object argument;
                if (javaConstructor == null) {
                    throw new YAMLException("No single argument constructor found for " + type);
                } else if (oneArgCount == 1) {
                    argument = constructStandardJavaInstance(
                            javaConstructor.getParameterTypes()[0], node);
                } else {
                    // TODO it should be possible to use implicit types instead
                    // of forcing String. Resolver must be available here to
                    // obtain the implicit tag. Then we can set the tag and call
                    // callConstructor(node) to create the argument instance.
                    // On the other hand it may be safer to require a custom
                    // constructor to avoid guessing the argument class
                    argument = constructScalar(node);
                    try {
                        javaConstructor = type.getDeclaredConstructor(String.class);
                    } catch (Exception e) {
                        throw new YAMLException("Can't construct a java object for scalar "
                                + node.getTag() + "; No String constructor found. Exception="
                                + e.getMessage(), e);
                    }
                }
                try {
                    javaConstructor.setAccessible(true);
                    result = javaConstructor.newInstance(argument);
                } catch (Exception e) {
                    throw new ConstructorException(null, null,
                            "Can't construct a java object for scalar " + node.getTag()
                                    + "; exception=" + e.getMessage(), node.getStartMark(), e);
                }
            }
            return result;
        }

        @SuppressWarnings("unchecked")
        private Object constructStandardJavaInstance(@SuppressWarnings("rawtypes")
        Class type, ScalarNode node) {
            Object result;
            if (type == String.class) {
                Construct stringConstructor = yamlConstructors.get(Tag.STR);
                result = stringConstructor.construct(node);
            } else if (type == Boolean.class || type == Boolean.TYPE) {
                Construct boolConstructor = yamlConstructors.get(Tag.BOOL);
                result = boolConstructor.construct(node);
            } else if (type == Character.class || type == Character.TYPE) {
                Construct charConstructor = yamlConstructors.get(Tag.STR);
                String ch = (String) charConstructor.construct(node);
                if (ch.length() == 0) {
                    result = null;
                } else if (ch.length() != 1) {
                    throw new YAMLException("Invalid node Character: '" + ch + "'; length: "
                            + ch.length());
                } else {
                    result = Character.valueOf(ch.charAt(0));
                }
            } else if (Date.class.isAssignableFrom(type)) {
                Construct dateConstructor = yamlConstructors.get(Tag.TIMESTAMP);
                Date date = (Date) dateConstructor.construct(node);
                if (type == Date.class) {
                    result = date;
                } else {
                    try {
                        java.lang.reflect.Constructor<?> constr = type.getConstructor(long.class);
                        result = constr.newInstance(date.getTime());
                    } catch (RuntimeException e) {
                        throw e;
                    } catch (Exception e) {
                        throw new YAMLException("Cannot construct: '" + type + "'");
                    }
                }
            } else if (type == Float.class || type == Double.class || type == Float.TYPE
                    || type == Double.TYPE || type == BigDecimal.class) {
                if (type == BigDecimal.class) {
                    result = new BigDecimal(node.getValue());
                } else {
                    Construct doubleConstructor = yamlConstructors.get(Tag.FLOAT);
                    result = doubleConstructor.construct(node);
                    if (type == Float.class || type == Float.TYPE) {
                        result = new Float((Double) result);
                    }
                }
            } else if (type == Byte.class || type == Short.class || type == Integer.class
                    || type == Long.class || type == BigInteger.class || type == Byte.TYPE
                    || type == Short.TYPE || type == Integer.TYPE || type == Long.TYPE) {
                Construct intConstructor = yamlConstructors.get(Tag.INT);
                result = intConstructor.construct(node);
                if (type == Byte.class || type == Byte.TYPE) {
                    result = Byte.valueOf(result.toString());
                } else if (type == Short.class || type == Short.TYPE) {
                    result = Short.valueOf(result.toString());
                } else if (type == Integer.class || type == Integer.TYPE) {
                    result = Integer.parseInt(result.toString());
                } else if (type == Long.class || type == Long.TYPE) {
                    result = Long.valueOf(result.toString());
                } else {
                    // only BigInteger left
                    result = new BigInteger(result.toString());
                }
            } else if (Enum.class.isAssignableFrom(type)) {
                String enumValueName = node.getValue();
                try {
                    result = Enum.valueOf(type, enumValueName);
                } catch (Exception ex) {
                    throw new YAMLException("Unable to find enum value '" + enumValueName
                            + "' for enum class: " + type.getName());
                }
            } else if (Calendar.class.isAssignableFrom(type)) {
                ConstructYamlTimestamp contr = new ConstructYamlTimestamp();
                contr.construct(node);
                result = contr.getCalendar();
            } else if (Number.class.isAssignableFrom(type)) {
                ConstructYamlNumber contr = new ConstructYamlNumber();
                result = contr.construct(node);
            }  else if (UUID.class == type) {
                result = UUID.fromString(node.getValue());
            } else {
                if (yamlConstructors.containsKey(node.getTag())) {
                    result = yamlConstructors.get(node.getTag()).construct(node);
                } else {
                    throw new YAMLException("Unsupported class: " + type);
                }
            }
            return result;
        }
    }

    /**
     * Construct sequence (List, Array, or immutable object) when the runtime
     * class is known.
     */
    protected class ConstructSequence implements Construct {
        @SuppressWarnings("unchecked")
        public Object construct(Node node) {
            SequenceNode snode = (SequenceNode) node;
            if (Set.class.isAssignableFrom(node.getType())) {
                if (node.isTwoStepsConstruction()) {
                    throw new YAMLException("Set cannot be recursive.");
                } else {
                    return constructSet(snode);
                }
            } else if (Collection.class.isAssignableFrom(node.getType())) {
                if (node.isTwoStepsConstruction()) {
                    return createDefaultList(snode.getValue().size());
                } else {
                    return constructSequence(snode);
                }
            } else if (node.getType().isArray()) {
                if (node.isTwoStepsConstruction()) {
                    return createArray(node.getType(), snode.getValue().size());
                } else {
                    return constructArray(snode);
                }
            } else {
                // create immutable object
                List<java.lang.reflect.Constructor<?>> possibleConstructors = new ArrayList<java.lang.reflect.Constructor<?>>(
                        snode.getValue().size());
                for (java.lang.reflect.Constructor<?> constructor : node
                        .getType().getDeclaredConstructors()) {
                    if (snode.getValue()
                            .size() == constructor.getParameterTypes().length) {
                        possibleConstructors.add(constructor);
                    }
                }
                if (!possibleConstructors.isEmpty()) {
                    if (possibleConstructors.size() == 1) {
                        Object[] argumentList = new Object[snode.getValue().size()];
                        java.lang.reflect.Constructor<?> c = possibleConstructors.get(0);
                        int index = 0;
                        for (Node argumentNode : snode.getValue()) {
                            Class<?> type = c.getParameterTypes()[index];
                            // set runtime classes for arguments
                            argumentNode.setType(type);
                            argumentList[index++] = constructObject(argumentNode);
                        }

                        try {
                            c.setAccessible(true);
                            return c.newInstance(argumentList);
                        } catch (Exception e) {
                            throw new YAMLException(e);
                        }
                    }

                    // use BaseConstructor
                    List<Object> argumentList = (List<Object>) constructSequence(snode);
                    Class<?>[] parameterTypes = new Class[argumentList.size()];
                    int index = 0;
                    for (Object parameter : argumentList) {
                        parameterTypes[index] = parameter.getClass();
                        index++;
                    }

                    for (java.lang.reflect.Constructor<?> c : possibleConstructors) {
                        Class<?>[] argTypes = c.getParameterTypes();
                        boolean foundConstructor = true;
                        for (int i = 0; i < argTypes.length; i++) {
                            if (!wrapIfPrimitive(argTypes[i]).isAssignableFrom(parameterTypes[i])) {
                                foundConstructor = false;
                                break;
                            }
                        }
                        if (foundConstructor) {
                            try {
                                c.setAccessible(true);
                                return c.newInstance(argumentList.toArray());
                            } catch (Exception e) {
                                throw new YAMLException(e);
                            }
                        }
                    }
                }
                throw new YAMLException("No suitable constructor with "
                        + String.valueOf(snode.getValue().size()) + " arguments found for "
                        + node.getType());

            }
        }

        private final Class<? extends Object> wrapIfPrimitive(Class<?> clazz) {
            if (!clazz.isPrimitive()) {
                return clazz;
            }
            if (clazz == Integer.TYPE) {
                return Integer.class;
            }
            if (clazz == Float.TYPE) {
                return Float.class;
            }
            if (clazz == Double.TYPE) {
                return Double.class;
            }
            if (clazz == Boolean.TYPE) {
                return Boolean.class;
            }
            if (clazz == Long.TYPE) {
                return Long.class;
            }
            if (clazz == Character.TYPE) {
                return Character.class;
            }
            if (clazz == Short.TYPE) {
                return Short.class;
            }
            if (clazz == Byte.TYPE) {
                return Byte.class;
            }
            throw new YAMLException("Unexpected primitive " + clazz);
        }

        @SuppressWarnings("unchecked")
        public void construct2ndStep(Node node, Object object) {
            SequenceNode snode = (SequenceNode) node;
            if (List.class.isAssignableFrom(node.getType())) {
                List<Object> list = (List<Object>) object;
                constructSequenceStep2(snode, list);
            } else if (node.getType().isArray()) {
                constructArrayStep2(snode, object);
            } else {
                throw new YAMLException("Immutable objects cannot be recursive.");
            }
        }
    }

    protected Class<?> getClassForNode(Node node) {
        Class<? extends Object> classForTag = typeTags.get(node.getTag());
        if (classForTag == null) {
            String name = node.getTag().getClassName();
            Class<?> cl;
            try {
                cl = getClassForName(name);
            } catch (ClassNotFoundException e) {
                throw new YAMLException("Class not found: " + name);
            }
            typeTags.put(node.getTag(), cl);
            return cl;
        } else {
            return classForTag;
        }
    }

    protected Class<?> getClassForName(String name) throws ClassNotFoundException {
        try {
            return Class.forName(name, true, Thread.currentThread().getContextClassLoader());
        } catch (ClassNotFoundException e) {
            return Class.forName(name);
        }
    }
}
