package usda.weru.weps;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.apache.log4j.Logger;
import usda.weru.util.PropertyStackContext;
import usda.weru.util.RelativeFileContext;

/**
 *
 * @author Joseph Levin <joelevin@weru.ksu.edu>
 */
public class RunFileDataBeanBridge {

    private static final Logger LOGGER = Logger.getLogger(RunFileDataBeanBridge.class);
    private final RunFileData c_data;
    private final RunFileBean c_bean;
    private final static Map<String, String> DATA_TO_BEAN;
    private final static Map<String, String> BEAN_TO_DATA;
    private final static RunFileBeanPersistenceHandler<String> PERSISTENCE;

    static {
        PERSISTENCE = new RunFileBeanStringPersistence();

        DATA_TO_BEAN = new HashMap<String, String>();
        BEAN_TO_DATA = new HashMap<String, String>();

        // loop over all the fields in the RunFileData class looking for constants that are annotated with a BeanProperty
        for (Field field : RunFileData.class.getFields()) {
            try {
                if (field.isAnnotationPresent(BeanProperty.class)) {
                    BeanProperty annotation = field.getAnnotation(BeanProperty.class);
                    String beanProperty = annotation.value();
                    String dataProperty = field.get(null).toString();

                    DATA_TO_BEAN.put(dataProperty, beanProperty);
                    BEAN_TO_DATA.put(beanProperty, dataProperty);
                }
            } catch (IllegalArgumentException | IllegalAccessException e) {
                LOGGER.error("Initilizing", e);
            }
        }
    }

    private RunFileDataBeanBridge(RunFileData rfd, RunFileBean rfb) {
        c_data = rfd;
        c_bean = rfb;

        initilize();

        c_data.addPropertyChangeListener(new InternalDataListener());
        c_bean.addPropertyChangeListener(new InternalBeanListener());
    }

    /**
     *
     * @param rfd
     * @param rfb
     */
    public static void bridge(RunFileData rfd, RunFileBean rfb) {
        new RunFileDataBeanBridge(rfd, rfb);
    }

    private void initilize() {
        //c_bean.setLoading(true);
        try {
            for (String dataProperty : DATA_TO_BEAN.keySet()) {
                String dataValue = c_data.getData(dataProperty);

                //deserialize the value
                String beanProperty = DATA_TO_BEAN.get(dataProperty);

                Object value = deserialize(beanProperty, dataValue);
                if (value != null) {
                    setBeanValue(beanProperty, value);
                }
            }
        } finally {
            //c_bean.setLoading(false);
        }
    }

    private Object deserialize(String beanProperty, String dataValue) {
        if (dataValue == null) {
            return null;
        }
        final boolean useRelativeFileContext = c_data != null && c_data.fileObject != null;
        if (useRelativeFileContext) {
            RelativeFileContext.enter(c_data.fileObject);
        }
        try {
            Method deserializer = PERSISTENCE.getClass().getMethod("deserialize" + capitalize(beanProperty), String.class);
            Object value = deserializer.invoke(PERSISTENCE, dataValue);

            return value;

        } catch (NoSuchMethodException | SecurityException | IllegalAccessException |
                IllegalArgumentException | InvocationTargetException e) {
            LOGGER.error(beanProperty + "=" + dataValue, e);
            return null;
        } finally {
            if (useRelativeFileContext) {
                RelativeFileContext.exit();
            }
        }
    }

    private String serialize(String beanProperty, Object beanValue) {
        if (beanValue == null) {
            return null;
        }
        final boolean useRelativeFileContext = c_data != null && c_data.fileObject != null;
        if (useRelativeFileContext) {
            RelativeFileContext.enter(c_data.fileObject);
        }
        try {
            for (Method serializer : PERSISTENCE.getClass().getMethods()) {
                if (serializer.getName().equals("serialize" + capitalize(beanProperty))
                        && serializer.getParameterTypes().length == 1) {
                    Object value = serializer.invoke(PERSISTENCE, beanValue);

                    if (value instanceof String) {
                        return (String) value;
                    } else {
                        return null;
                    }
                }
            }
            //no method found
            return null;

        } catch (IllegalAccessException | IllegalArgumentException | SecurityException | InvocationTargetException e) {
            LOGGER.error(beanProperty + "=" + (beanValue != null ? beanValue : "NULL"), e);
            return null;
        } finally {
            if (useRelativeFileContext) {
                RelativeFileContext.exit();
            }
        }
    }

    private void setBeanValue(String beanProperty, Object beanValue) {
        try {
            if (PropertyStackContext.isPropertyOnStack(c_bean, beanProperty)) {
                return;
            }
            for (Method setter : RunFileBean.class.getMethods()) {
                if (setter.getName().equals("set" + capitalize(beanProperty)) && setter.getParameterTypes().length == 1) {
                    try {
                        setter.invoke(c_bean, beanValue);
                        return;
                    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                        LOGGER.error("Error setting beanValue: " + beanProperty + "="
                                + (beanValue != null ? beanValue.toString() : "NULL"));
                    }
                }
            }

        } catch (SecurityException e) {
            LOGGER.error(beanProperty + "=" + (beanValue != null ? beanValue : "NULL"), e);
        }
    }

    private void setDataValue(String dataProperty, String dataValue) {
        if (PropertyStackContext.isPropertyOnStack(c_data, dataProperty)) {
            return;
        }
        c_data.setData(dataProperty, dataValue);
    }

    private class InternalDataListener implements PropertyChangeListener {

        @Override
        public void propertyChange(PropertyChangeEvent evt) {

        /**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//            assert c_data == evt.getSource() : "Unexpected property change event source.";

            String dataProperty = evt.getPropertyName();
            if (!DATA_TO_BEAN.containsKey(dataProperty)) {
                return;
            }
            String beanProperty = DATA_TO_BEAN.get(dataProperty);

            String dataValue = c_data.getData(dataProperty);
            Object beanValue = deserialize(beanProperty, dataValue);

            setBeanValue(beanProperty, beanValue);
        }
    }

    private class InternalBeanListener implements PropertyChangeListener {

        @Override
        public void propertyChange(PropertyChangeEvent evt) {

        /**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//            assert c_bean == evt.getSource() : "Unexpected property change event source.";

            String beanProperty = evt.getPropertyName();
            if (!BEAN_TO_DATA.containsKey(beanProperty)) {
                return;
            }
            String dataProperty = BEAN_TO_DATA.get(beanProperty);

            Object beanValue = evt.getNewValue();
            String dataValue = serialize(beanProperty, beanValue);

            setDataValue(dataProperty, dataValue);
        }
    }

    /**
     *
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public static @interface BeanProperty {

        /**
         *
         * @return
         */
        String value();
    }

    private static String capitalize(String name) {
        char[] c = name.toCharArray();
        c[0] = Character.toUpperCase(c[0]);
        return new String(c);
    }
}
