/*******************************************************************************
 * Copyright (c) 2015 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Eclipse Distribution License v1.0 which accompany this distribution.
 * The Eclipse Public License is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Jan S. Rellermeyer, IBM Research - initial API and implementation
 *******************************************************************************/

package org.osgi.impl.service.rest;

import java.io.StringWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;
import org.json.JSONObject;
import org.osgi.impl.service.rest.pojos.BundleExceptionPojo;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Reflector to create pojos from JSON Object representations and vice versa.
 * 
 * @author Jan S. Rellermeyer, IBM Research
 * @param <B> The pojo base class for which the reflector does the conversion.
 */
public class PojoReflector<B> {

	private static HashMap<Class<?>, PojoReflector<?>>	reflectorCache	= new HashMap<Class<?>, PojoReflector<?>>();

	private final Class<B>								clazz;

	private final HashMap<String, Method>				setterMethodTable;

	private final HashMap<String, Method>				getterMethodTable;

	private static final DocumentBuilderFactory			factory;

	private static final Map<Class<?>, String>			typeCache		= new HashMap<Class<?>, String>();

	private static final String							SCHEMA_LOCATION	= "http://www.osgi.org/xmlns/rest/v1.0.0 rest.xsd";

	private static final String							REST_NS			= "rest";

	static {
		final SchemaFactory sfact = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
		factory = DocumentBuilderFactory.newInstance();
		try {
			factory.setSchema(sfact.newSchema(new StreamSource(PojoReflector.class.getResourceAsStream("/rest.xsd"))));
			factory.setNamespaceAware(true);
			factory.setValidating(true);
		} catch (SAXException e) {
			e.printStackTrace();
		}

		typeCache.put(String.class, "String");
		typeCache.put(Long.class, "Long");
		typeCache.put(Double.class, "Double");
		typeCache.put(Float.class, "Float");
		typeCache.put(Integer.class, "Integer");
		typeCache.put(Byte.class, "Byte");
		typeCache.put(Character.class, "Character");
		typeCache.put(Boolean.class, "Boolean");
		typeCache.put(Short.class, "Short");
	}

	public static <T> PojoReflector<T> getReflector(final Class<T> clazz) {
		@SuppressWarnings("unchecked")
		PojoReflector<T> r = (PojoReflector<T>) reflectorCache.get(clazz);
		if (r == null) {
			r = new PojoReflector<T>(clazz);
			reflectorCache.put(clazz, r);
		}
		return r;
	}

	private PojoReflector(final Class<B> clazz) {
		this.clazz = clazz;
		final Field[] fields = clazz.getDeclaredFields();

		setterMethodTable = new HashMap<String, Method>(fields.length);
		getterMethodTable = new HashMap<String, Method>(fields.length);
		for (int i = 0; i < fields.length; i++) {
			final Field field = fields[i];
			final String fieldName = field.getName();
			try {
				final Method setter = clazz.getMethod(getSetterName(fieldName), field.getType());
				setterMethodTable.put(fieldName, setter);
				final Method getter = clazz.getMethod(getGetterName(fieldName));
				getterMethodTable.put(fieldName, getter);
			} catch (final NoSuchMethodException e) {
				e.printStackTrace();
			}
		}
	}

	private static String getSetterName(final String fieldName) {
		return "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
	}

	private static String getGetterName(final String fieldName) {
		return "get" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
	}

	public B beanFromJSONObject(final JSONObject obj) throws Exception {
		final String[] names = JSONObject.getNames(obj);
		final B instance = clazz.newInstance();
		for (int i = 0; i < names.length; i++) {
			final String key = names[i];
			final Method setter = setterMethodTable.get(key);
			if (setter == null) {
				// silently ignore, it's JSON after all
				continue;
			}
			final Object o = obj.get(key);
			// check for empty object from JS...
			if (!(o instanceof JSONObject)) {
				setter.invoke(instance, o);
			}
		}

		return instance;
	}

	public B beanFromXml(final Document doc) throws Exception {
		final B instance = clazz.newInstance();
		final Node rootNode = doc.getFirstChild();
		final NodeList elems = rootNode.getChildNodes();
		for (int i = 0; i < elems.getLength(); i++) {
			final String key = elems.item(i).getNodeName();
			final Method setter = setterMethodTable.get(key);
			if (setter == null) {
				System.err.println("Warning: unknown value " + key);
				continue;
			}
			final String value = elems.item(i).getTextContent();
			final Object o;
			final Class<?> type = setter.getParameterTypes()[0];
			if (int.class == type) {
				o = Integer.valueOf(value);
			} else if (long.class == type) {
				o = Long.valueOf(value);
			} else if (boolean.class == type) {
				o = Boolean.valueOf(value);
			} else {
				o = value;
			}

			setter.invoke(instance, o);
		}

		return instance;
	}

	public Document xmlFromBean(final B bean) throws Exception {
		final DocumentBuilder builder = factory.newDocumentBuilder();
		final Document doc = builder.newDocument();

		final Element rootNode = xmlFromBean(bean, doc);
		rootNode.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI,
				"xsi:schemaLocation", SCHEMA_LOCATION);
		doc.appendChild(rootNode);

		return doc;
	}

	public Element xmlFromBean(final Object bean, final Document doc) throws Exception {
		final Element rootNode = doc.createElementNS(REST_NS, bean.getClass().getAnnotation(RootNode.class).name());
		if (bean instanceof Collection) {
			String elemName = null;
			boolean complex = false;

			final ElementNode a = bean.getClass().getAnnotation(ElementNode.class);
			if (a != null) {
				elemName = a.name();
			}

			for (final Object o : (Collection<?>) bean) {
				if (elemName == null) {
					elemName = o.getClass().getAnnotation(RootNode.class).name();
					complex = true;
				}

				if (complex) {
					rootNode.appendChild(getReflector(o.getClass()).xmlFromBean(o, doc));
				} else {
					rootNode.appendChild(toXml(o, elemName, doc));
				}
			}
		} else if (bean instanceof BundleExceptionPojo) {
			final BundleExceptionPojo p = (BundleExceptionPojo) bean;

			final Element tc = doc.createElementNS(REST_NS, "typecode");
			tc.setTextContent(Integer.toString(p.getTypecode()));
			rootNode.appendChild(tc);
			final Element msg = doc.createElementNS(REST_NS, "message");
			msg.setTextContent(p.getMessage());
			rootNode.appendChild(msg);
		} else {
			for (final Map.Entry<String, Method> entry : getterMethodTable.entrySet()) {
				final String field = entry.getKey();
				final Object o = entry.getValue().invoke(bean);
				rootNode.appendChild(toXml(o, field, doc));
			}
		}

		return rootNode;
	}

	// for debugging only
	@SuppressWarnings("unused")
	private final String printDoc(final Document doc) throws Exception {
		TransformerFactory tf = TransformerFactory.newInstance();
		Transformer transformer = tf.newTransformer();
		transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
		StringWriter writer = new StringWriter();
		transformer.transform(new DOMSource(doc), new StreamResult(writer));
		return writer.getBuffer().toString();
	}

	private static Element toXml(final Object o, final String name, final Document doc) {
		final Element e = doc.createElementNS(REST_NS, name);

		if ("usingBundles".equals(name)) {
			for (final String bundle : (String[]) o) {
				e.appendChild(toXml(bundle, "bundle", doc));
			}
		} else if (o instanceof Map) {
			@SuppressWarnings("unchecked")
			final Map<Object, Object> map = (Map<Object, Object>) o;
			for (final Map.Entry<Object, Object> entry : map.entrySet()) {
				final Element elem = doc.createElementNS(REST_NS, "property");
				elem.setAttribute("name", entry.getKey().toString());
				final Object val = entry.getValue();
				if (val.getClass().isArray()) {
					final String type = getType(o.getClass().getComponentType());
					if (type != null) {
						e.setAttribute("type", type);
					}
					final int len = Array.getLength(val);
					final StringBuilder sb = new StringBuilder();
					for (int i = 0; i < len; i++) {
						sb.append(Array.get(val, i).toString());
						if (i < len) {
							sb.append('\n');
						}
					}
					elem.setTextContent(sb.toString());
				} else {
					final String type = getType(val.getClass());
					if (type != null) {
						elem.setAttribute("type", type);
					}
					elem.setAttribute("value", val.toString());
				}
				e.appendChild(elem);
			}
		} else {
			e.setTextContent(o.toString());
		}

		return e;
	}

	private static String getType(Class<? extends Object> cls) {
		return typeCache.get(cls);
	}

	public static Document mapToXml(final Map<String, String> map) throws Exception {
		final DocumentBuilder builder = factory.newDocumentBuilder();
		final Document doc = builder.newDocument();

		final Element rootNode = doc.createElementNS(REST_NS, "bundleHeader");
		doc.appendChild(rootNode);

		for (final String key : map.keySet()) {
			final Element entry = doc.createElementNS(REST_NS, "entry");
			entry.setAttribute("key", key);
			entry.setAttribute("value", map.get(key));
			rootNode.appendChild(entry);
		}

		return doc;
	}

	@Retention(RetentionPolicy.RUNTIME)
	public @interface RootNode {
		String name();
	}

	@Retention(RetentionPolicy.RUNTIME)
	public @interface ElementNode {
		String name();
	}

}