package eu.dnetlib.miscutils.jaxb;

import java.io.StringReader;
import java.io.StringWriter;
import java.lang.annotation.Annotation;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlRegistry;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.namespace.QName;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

/**
 * Common base class for implementing the Jaxb ObjectFactory pattern.
 *
 * @author marko
 *
 * @param <T>
 *            type of the roote element
 */
@XmlRegistry
public class JaxbFactory<T> {

	/**
	 * jaxb context.
	 */
	private JAXBContext context;

	/**
	 * class for the root element handled by this factory.
	 */
	private Class<T> clazz;

	/**
	 * Prevent instantiating with 0 arguments.
	 *
	 * @throws JAXBException
	 */
	protected JaxbFactory() throws JAXBException {
		// prevent instantiation
	}

	/**
	 * creates a new jaxb factory.
	 *
	 * This constructor is needed because spring constructor injection doesn't work well will varargs.
	 *
	 * @param clazz
	 * @throws JAXBException
	 */
	public JaxbFactory(final Class<? extends T> clazz) throws JAXBException {
		this(clazz, new Class<?>[] {});
	}

	/**
	 * creates a new jaxb factory.
	 *
	 * @param clazz
	 *            class for the root element
	 * @param classes
	 *            other classes
	 * @throws JAXBException
	 *             could happen
	 */
	@SuppressWarnings("unchecked")
	public JaxbFactory(final Class<? extends T> clazz, Class<?>... classes) throws JAXBException {
		Class<?>[] all = new Class<?>[classes.length + 1];
		for (int i = 0; i < classes.length; i++)
			all[i + 1] = classes[i];
		all[0] = clazz;

		this.clazz = (Class<T>) clazz;
		context = JAXBContext.newInstance(all);
	}

	/**
	 * Create a new T instance.
	 *
	 * @return new T instance
	 */
	public T newInstance() {
		try {
			return clazz.newInstance();
		} catch (InstantiationException e) {
			throw new IllegalStateException("jaxb bean not instantiable, e");
		} catch (IllegalAccessException e) {
			throw new IllegalStateException("jaxb bean not instantiable, e");
		}
	}

	/**
	 * Parses a given string and creates a java object.
	 *
	 * @param value
	 *            serialized representation
	 * @return java object
	 * @throws JAXBException
	 *             could happen
	 */
	public T parse(final String value) throws JAXBException {
		return parse(new StreamSource(new StringReader(value)));
	}

	/**
	 * Parses a given source and creates a java object.
	 *
	 * @param source
	 *            serialized representation
	 * @return java object
	 * @throws JAXBException
	 *             could happen
	 */
	@SuppressWarnings("unchecked")
	public T parse(final Source source) throws JAXBException {
		final Unmarshaller unmarshaller = context.createUnmarshaller();
		return (T) unmarshaller.unmarshal(source);
	}

	/**
	 * Serializes a java object to the xml representation.
	 *
	 * @param value
	 *            java object
	 * @return xml string
	 * @throws JAXBException
	 *             could happen
	 */
	public String serialize(final T value) throws JAXBException {
		final Marshaller marshaller = context.createMarshaller();
		final StringWriter buffer = new StringWriter();
		marshaller.marshal(createElement(value), buffer);
		return buffer.toString();
	}

	/**
	 * Serializes a java object to a xml sink.
	 *
	 * @param value
	 *            java object
	 * @param result
	 *            transform sink
	 * @throws JAXBException
	 *             could happen
	 */
	public void serialize(final T value, final Result result) throws JAXBException {
		final Marshaller marshaller = context.createMarshaller();
		marshaller.marshal(createElement(value), result);
	}

	/**
	 * creates a jax element for a given java object.
	 *
	 * @param value
	 *            java object
	 * @return jaxb element
	 */
	protected JAXBElement<T> createElement(final T value) {
		final XmlRootElement ann = findAnnotation(XmlRootElement.class, clazz);
		return new JAXBElement<T>(new QName(ann.namespace(), ann.name()), this.clazz, null, value);
	}

	/**
	 * recursively searches a given annotation.
	 *
	 * @param <T>
	 *            annotation type
	 * @param annotation
	 *            annotation to search
	 * @param clazz
	 *            root of the class hierarchy.
	 * @return annotation
	 */
	private <X extends Annotation> X findAnnotation(final Class<X> annotation, final Class<?> clazz) {
		if (clazz == null)
			return null;

		final X ann = clazz.getAnnotation(annotation);
		if (ann != null)
			return ann;

		for (Class<?> iface : clazz.getInterfaces()) {
			final X iann = findAnnotation(annotation, iface);
			if (iann != null)
				return iann;
		}

		final X parent = findAnnotation(annotation, clazz.getSuperclass());
		if (parent != null)
			return parent;

		return null;
	}

	public JAXBContext getContext() {
		return context;
	}

	public void setContext(final JAXBContext context) {
		this.context = context;
	}

	public Class<? extends T> getClazz() {
		return clazz;
	}

	public void setClazz(final Class<T> clazz) {
		this.clazz = clazz;
	}

}
