/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */

package net.ranides.assira.reflection.util;

import lombok.experimental.UtilityClass;

import net.ranides.assira.io.StringInput;
import net.ranides.assira.io.StringOutput;
import net.ranides.assira.reflection.BeanModel;
import net.ranides.assira.reflection.BeanProperty;
import net.ranides.assira.reflection.IClass;

import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;

/**
 * Helper method for JavaBeans
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public final class BeanUtils {

    /**
     * Converts XML into JavaBean. It uses XMLDecoder.
     *
     * @param xml xml
     * @return new instance of JavaBean
     */
    public static Object fromXML(String xml) {
        XMLDecoder dec = new XMLDecoder(new StringInput(xml));
        Object object = dec.readObject();
        dec.close();
        return object;
    }

    /**
     * Converts JavaBean into XML. It uses XMLEncoder.
     *
     * @param object object
     * @return xml
     */
    public static String toXML(Object object) {
        StringOutput output = new StringOutput();
        XMLEncoder encoder = new XMLEncoder(output);
        encoder.writeObject(object);
        encoder.close();
        return output.toString();
    }

    /**
     * It creates deep copy of JavaBean by serialization to and from XML format.
     *
     * @param object object
     * @return new deep copy of an object
     */
    public static Object copy(Object object) {
        return fromXML( toXML(object) );
    }

    /**
     * It creates BeanMapper which converts JavaBeans of source type into JavaBean of target type.
     * Please read {@link #map} description to know more about conversion.
     *
     * @param target type
     * @param source type
     * @param <T> type
     * @param <S> type
     * @return mapper
     */
    public static <T,S> BeanMapper<T,S> mapper(IClass<T> target, IClass<S> source) {
        return new BeanMapper<>(target, source);
    }

    /**
     * It copies properties from source and puts them into target.
     * It tries to copy only properties if following conditions are met:
     *  - properties have identical name
     *  - properties have equivalent types
     *  - property is readable from source
     *  - property is writable in target
     *
     * If some property does not meet any of requirements, it is silently ignored.
     * For example, method does not throw exception if it detects properties with the same name but different type.
     *
     * This method does not create new instance of target object. It modifies the passed one.
     * Modified target object is returned as a result.
     *
     * @param target object
     * @param source object
     * @param <T> type of target
     * @return target
     */
    public static <T> T map(T target, Object source) {
        match(BeanModel.typefor(target), BeanModel.typefor(source), (pt, ps)-> { pt.set(target, ps.get(source)); });
        return target;
    }

    private static void match(BeanModel mt, BeanModel ms, BiConsumer<BeanProperty, BeanProperty> matcher) {
        for (BeanProperty ps : ms.properties().values()) {
            if(ps.isReadable()) {
                BeanProperty pt = mt.properties().get(ps.name());
                if(pt!=null && pt.isWritable() && pt.type().isEquivalent(ps.type())) {
                    matcher.accept(pt, ps);
                }
            }
        }
    }

    /**
     * BeanMapper can be used to copy properties from one JavaBean to another one.
     * Please read description of method {@link BeanUtils#map(Object, Object)} to know more details about
     * copy strategy.
     *
     * BeanMapper is preferred to "map" method when you have to map a lot of objects of known types.
     * In this case BeanMapper has better performance, because it creates list of matched properties only once
     * and reuses it.
     *
     * @param <T> target type
     * @param <S> source type
     */
    public static class BeanMapper<T,S> {

        private final IClass<T> type;
        private final List<BeanPropertyMapper> matched = new ArrayList<>();

        private BeanMapper(IClass<T> target, IClass<S> source) {
            type = target;
            match(BeanModel.typeinfo(target), BeanModel.typeinfo(source), (pt,ps) -> {
                matched.add(new BeanPropertyMapper(pt, ps));
            });
        }

        /**
         * Copies properties from source into target.
         *
         * @param target target
         * @param source source
         * @return target
         */
        public T map(T target, S source) {
            for(BeanPropertyMapper bpm : matched) {
                bpm.copy(target, source);
            }
            return target;
        }

        /**
         * Creates new instance of target object and copies into it properties from source.
         *
         * @param source source
         * @return new instance of object
         */
        public T map(S source) {
            return map(type.construct(), source);
        }
    }

    private static class BeanPropertyMapper {

        private final BeanProperty tp;

        private final BeanProperty sp;

        public BeanPropertyMapper(BeanProperty target, BeanProperty source) {
            this.tp = target;
            this.sp = source;
        }

        public void copy(Object target, Object source) {
            tp.set(target, sp.get(source));
        }
    }

}
