// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.json;

import jodd.introspector.ClassDescriptor;
import jodd.introspector.ClassIntrospector;
import jodd.introspector.PropertyDescriptor;
import jodd.introspector.Setter;
import jodd.typeconverter.TypeConverterManager;
import jodd.util.ClassLoaderUtil;
import jodd.util.ClassUtil;

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Map to bean converter.
 * Used when parsing with class metadata enabled.
 */
public class MapToBean {

	protected boolean declared = true;
	protected final JsonParserBase jsonParser;
	protected final String classMetadataName;

	public MapToBean(JsonParserBase jsonParser, String classMetadataName) {
		this.jsonParser = jsonParser;
		this.classMetadataName = classMetadataName;
	}

	/**
	 * Converts map to target type.
	 */
	public Object map2bean(Map map, Class targetType) {
		Object target = null;

		// create targets type
		String className = (String) map.get(classMetadataName);

		if (className == null) {
			if (targetType == null) {
				// nothing to do, no information about target type found
				target = map;
			}
		}
		else {
			try {
				targetType = ClassLoaderUtil.loadClass(className);
			} catch (ClassNotFoundException cnfex) {
				throw new JsonException(cnfex);
			}
		}

		if (target == null) {
			target = jsonParser.newObjectInstance(targetType);
		}

		ClassDescriptor cd = ClassIntrospector.lookup(target.getClass());

		boolean targetIsMap = target instanceof Map;

		for (Object key : map.keySet()) {
			String keyName = key.toString();

			if (classMetadataName != null) {
				if (keyName.equals(classMetadataName)) {
					continue;
				}
			}

			PropertyDescriptor pd = cd.getPropertyDescriptor(keyName, declared);

			if (!targetIsMap && pd == null) {
				// target property does not exist, continue
				continue;
			}

			// value is one of JSON basic types, like Number, Map, List...
			Object value = map.get(key);

			Class propertyType = pd == null ? null : pd.getType();
			Class componentType = pd == null ? null : pd.resolveComponentType(true);

			if (value != null) {
				if (value instanceof List) {
					if (componentType != null && componentType != String.class) {
						value = generifyList((List) value, componentType);
					}
				}
				else if (value instanceof Map) {
					// if the value we want to inject is a Map...
					if (!ClassUtil.isTypeOf(propertyType, Map.class)) {
						// ... and if target is NOT a map
						value = map2bean((Map) value, propertyType);
					}
					else {
						// target is also a Map, but we might need to generify it
						Class keyType = pd == null ? null : pd.resolveKeyType(true);

						if (keyType != String.class || componentType != String.class) {
							// generify
							value = generifyMap((Map) value, keyType, componentType);
						}
					}
				}
			}

			if (targetIsMap) {
				((Map)target).put(keyName, value);
			}
			else {
				try {
					setValue(target, pd, value);
				} catch (Exception ignore) {
					ignore.printStackTrace();
				}
			}
		}

		return target;
	}

	/**
	 * Converts type of all list elements to match the component type.
	 */
	private Object generifyList(List list, Class componentType) {
		for (int i = 0; i < list.size(); i++) {
			Object element = list.get(i);

			if (element != null) {
				if (element instanceof Map) {
					Object bean = map2bean((Map) element, componentType);
					list.set(i, bean);
				} else {
					Object value = convert(element, componentType);
					list.set(i, value);
				}
			}
		}

		return list;
	}

	/**
	 * Sets the property value.
	 */
	private void setValue(Object target, PropertyDescriptor pd, Object value) throws InvocationTargetException, IllegalAccessException {
		Class propertyType;

		Setter setter = pd.getSetter(true);
		if (setter != null) {
			if (value != null) {
				propertyType = setter.getSetterRawType();
				value = jsonParser.convertType(value, propertyType);
			}
			setter.invokeSetter(target, value);
		}
	}

	/**
	 * Change map elements to match key and value types.
	 */
	protected <K,V> Map<K, V> generifyMap(Map<Object, Object> map, Class<K> keyType, Class<V> valueType) {

		if (keyType == String.class) {
			// only value type is changed, we can make value replacements
			for (Map.Entry<Object, Object> entry : map.entrySet()) {
				Object value = entry.getValue();
				Object newValue = convert(value, valueType);

				if (value != newValue) {
					entry.setValue(newValue);
				}
			}
			return (Map<K, V>) map;
		}

		// key is changed too, we need a new map
		Map<K, V> newMap = new HashMap<>(map.size());

		for (Map.Entry<Object, Object> entry : map.entrySet()) {
			Object key = entry.getKey();
			Object newKey = convert(key, keyType);

			Object value = entry.getValue();
			Object newValue = convert(value, valueType);

			newMap.put((K)newKey, (V)newValue);
		}

		return newMap;
	}

	protected Object convert(Object value, Class targetType) {
		Class valueClass = value.getClass();

		if (valueClass == targetType) {
			return value;
		}

		if (value instanceof Map) {
			if (targetType == Map.class) {
				return value;
			}

			return map2bean((Map) value, targetType);
		}

		try {
			return TypeConverterManager.convertType(value, targetType);
		}
		catch (Exception ex) {
			throw new JsonException("Type conversion failed", ex);
		}
	}

}