package de.renew.logging.gui;

import java.awt.BorderLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;

import org.apache.log4j.Appender;
import org.apache.log4j.Layout;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.Priority;


/**
 * Displays an editor window that allows users to change the attributes of an {@link Appender}.
 * This panel is usually displayed via the menus {@code Simulation -> Configure Simulation -> Logging}.
 *
 * @author Sven Offermann
 */
public class GenericAppenderEditor extends JPanel {
    private static final Logger LOGGER = Logger.getLogger(GenericAppenderEditor.class);
    private static final Set<Class<?>> SUPPORTED_TYPES = new HashSet<>();

    static {
        SUPPORTED_TYPES.add(Integer.class);
        SUPPORTED_TYPES.add(Long.class);
        SUPPORTED_TYPES.add(Float.class);
        SUPPORTED_TYPES.add(Double.class);
        SUPPORTED_TYPES.add(Boolean.class);
        SUPPORTED_TYPES.add(String.class);
        SUPPORTED_TYPES.add(Byte.class);
        SUPPORTED_TYPES.add(int.class);
        SUPPORTED_TYPES.add(long.class);
        SUPPORTED_TYPES.add(float.class);
        SUPPORTED_TYPES.add(double.class);
        SUPPORTED_TYPES.add(boolean.class);
        SUPPORTED_TYPES.add(byte.class);
        SUPPORTED_TYPES.add(Priority.class);
        SUPPORTED_TYPES.add(Layout.class);
    }

    /**
     * The appender to be edited
     */
    private Appender _appender;

    /**
     * A panel for the fields
     */
    private JPanel _fieldsPanel;

    /**
     * A pannel for buttons
     */
    private JPanel _buttonPanel;

    /**
     * A map with attributeFields
     */
    private Map<String, JComponent> _attributeFields = new Hashtable<>();

    /**
     * A map with attributeTypes
     */
    private Map<String, Class<?>> _attributeTypes;

    /**
     * Initializes a new GenericAppenderEditor which sets up configuration for the
     * supplied {@link Appender}.
     * @param appender the {@link Appender} to be edited.
     */
    public GenericAppenderEditor(Appender appender) {
        super();
        this._appender = appender;

        initComponents();
    }

    private void initComponents() {
        this.setLayout(new BorderLayout());

        _fieldsPanel = new JPanel();
        _fieldsPanel.setLayout(new GridBagLayout());
        this.add(_fieldsPanel, BorderLayout.CENTER);

        _buttonPanel = new JPanel();
        _buttonPanel.setLayout(new BorderLayout());
        this.add(_buttonPanel, BorderLayout.SOUTH);

        // create fields to configure a appender
        _attributeTypes = findAttributeTypes(_appender);

        String[] attributes = _attributeTypes.keySet().toArray(new String[] { });
        for (int x = 0; x < attributes.length; x++) {
            Object currentValue = getCurrentValue(attributes[x], _appender);

            JComponent c =
                createComponents(attributes[x], _attributeTypes.get(attributes[x]), currentValue);
            if (c != null) {
                _attributeFields.put(attributes[x], c);
            }
        }
    }

    /**
     * Initialized row variable
     */
    private int _row = 0;

    /**
     * Creates an input field to make changes to the attributes of
     * an appender, and adds the input field and a label with the
     * attribute name to the attributes panel. The input fields
     * can be Spinners to display and change number attributes,
     * TextFields for string attributes, CheckBoxes for boolean
     * attributes and ComboBoxes to set the level of an appender.
     *
     * @param aName the name of the attribute
     * @param type the type of the attribute represented by a class object
     * @param currentValue the currently set value of the attribute
     * @return the created and added input field
     */
    private JComponent createComponents(String aName, Class<?> type, Object currentValue) {
        JComponent component = null;
        if ((type == Number.class) || (type == int.class) || (type == long.class)
            || (type == byte.class) || (type == double.class) || (type == float.class)
            || (type == long.class)) {
            SpinnerNumberModel model = new SpinnerNumberModel();
            model.setValue(currentValue);

            component = new JSpinner(model);
            if ((type == int.class) || (type == Integer.class)) {
                model.setMinimum(Integer.valueOf(Integer.MIN_VALUE));
                model.setMaximum(Integer.valueOf(Integer.MAX_VALUE));
                model.setStepSize(Integer.valueOf(1));
            } else if ((type == byte.class) || (type == Byte.class)) {
                model.setMinimum(Byte.valueOf(Byte.MIN_VALUE));
                model.setMaximum(Byte.valueOf(Byte.MAX_VALUE));
                model.setStepSize(Integer.valueOf(1));
            } else if ((type == long.class) || (type == Long.class)) {
                model.setMinimum(Long.valueOf(Long.MIN_VALUE));
                model.setMaximum(Long.valueOf(Long.MAX_VALUE));
                model.setStepSize(Integer.valueOf(1));
            } else if ((type == float.class) || (type == Float.class)) {
                model.setMinimum(Float.valueOf(Float.MIN_VALUE));
                model.setMaximum(Float.valueOf(Float.MAX_VALUE));
            } else if ((type == double.class) || (type == Double.class)) {
                model.setMinimum(Double.valueOf(Double.MIN_VALUE));
                model.setMaximum(Double.valueOf(Double.MAX_VALUE));
            }
        } else if (type == int.class) {
            SpinnerNumberModel model = new SpinnerNumberModel();
            model.setValue(currentValue);

            component = new JSpinner(model);
        } else if (String.class.isAssignableFrom(type)) {
            component = new JTextField((String) currentValue);
        } else if (Boolean.class.isAssignableFrom(type)) {
            component = new JCheckBox();
        } else if (Priority.class.isAssignableFrom(type)) {
            Priority[] levels =
                { Level.ALL, Level.INFO, Level.DEBUG, Level.ERROR, Level.FATAL, Level.OFF };
            component = new JComboBox(levels);

            // set the current level
            ((JComboBox) component).getModel().setSelectedItem(currentValue);

        } else if (Layout.class.isAssignableFrom(type)) {
            /*
            Layout[] layouts = {new PatternLayout("%m%n"), new PatternLayout("%t%m%n")};
            component = new JComboBox(layouts);
            ((JComboBox) component).getModel().setSelectedItem(currentValue);*/
            if (currentValue != null) {
                component = new JTextField(((PatternLayout) currentValue).getConversionPattern());
            } else {
                component = new JTextField();
            }
        }

        if (component != null) {
            GridBagConstraints c = new GridBagConstraints();
            c.gridx = 0;
            c.gridy = _row;
            c.gridwidth = 1;
            c.gridheight = 1;
            c.anchor = GridBagConstraints.EAST;
            JLabel label = new JLabel(aName + ":");
            ((GridBagLayout) _fieldsPanel.getLayout()).setConstraints(label, c);
            _fieldsPanel.add(label);

            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = _row++;
            c.weightx = 0.7;
            c.fill = GridBagConstraints.HORIZONTAL;
            c.anchor = GridBagConstraints.WEST;
            c.gridwidth = 1;
            c.gridheight = 1;
            ((GridBagLayout) _fieldsPanel.getLayout()).setConstraints(component, c);
            _fieldsPanel.add(component);
        }

        return component;
    }

    private Map<String, Class<?>> findAttributeTypes(Appender appender) {
        Hashtable<String, Class<?>> setters = new Hashtable<String, Class<?>>();
        Class<?> clazz = appender.getClass();
        Method[] methods = clazz.getMethods();
        for (int x = 0; x < methods.length; x++) {
            if (methods[x].getName().startsWith("set")) {
                if (methods[x].getParameterTypes().length == 1) {
                    if (SUPPORTED_TYPES.contains(methods[x].getParameterTypes()[0])) {
                        String name = methods[x].getName().substring("set".length());
                        setters.put(name, methods[x].getParameterTypes()[0]);
                    }
                }
            }
        }

        TreeMap<String, Class<?>> attributeTypes = new TreeMap<String, Class<?>>();
        for (int x = 0; x < methods.length; x++) {
            if (methods[x].getName().startsWith("get")) {
                String name = methods[x].getName().substring("get".length());
                if (setters.containsKey(name)) {
                    if (methods[x].getParameterTypes().length == 0) {
                        if (SUPPORTED_TYPES.contains(methods[x].getReturnType())) {
                            attributeTypes.put(name, methods[x].getReturnType());
                        }
                    }
                }
            }
        }

        return attributeTypes;
    }

    /**
     * Gets the current value of an appender attribute
     *
     * @param aName the name of the attribute
     * @param appender the appender
     * @return the current value of the attribute of the given appender
     */
    private Object getCurrentValue(String aName, Appender appender) {
        Object value = null;
        Class<?> clazz = appender.getClass();
        try {
            Method method = clazz.getMethod("get" + aName, new Class<?>[] { });

            value = method.invoke(appender, new Object[] { });
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }

        return value;
    }

    /**
     * Sets a given attribute of a given appender to the new
     * given value.
     *
     * @param appender the appender of which the attribute should be changed.
     * @param aName the name of the attribute which should be set
     * @param value the value to which the attribute should be set
     *         */
    private void setValue(Appender appender, String aName, Object value) {
        Class<?> clazz = appender.getClass();
        if (value != null) {
            Class<?> type = value.getClass();
            if (type == Level.class) {
                type = Priority.class;
            } else if (type == PatternLayout.class) {
                type = Layout.class;
            }

            Method method = null;
            try {
                method = clazz.getMethod("set" + aName, new Class<?>[] { type });

            } catch (NoSuchMethodException ex) {
                try {
                    if (type == Integer.class) {
                        method = clazz.getMethod("set" + aName, new Class<?>[] { int.class });
                    } else if (type == Byte.class) {
                        method = clazz.getMethod("set" + aName, new Class<?>[] { byte.class });
                    } else if (type == Long.class) {
                        method = clazz.getMethod("set" + aName, new Class<?>[] { long.class });
                    } else if (type == Float.class) {
                        method = clazz.getMethod("set" + aName, new Class<?>[] { float.class });
                    } else if (type == Double.class) {
                        method = clazz.getMethod("set" + aName, new Class<?>[] { double.class });
                    }
                } catch (Exception e) {
                    LOGGER.error(e.getMessage(), e);
                }
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), e);
            }

            try {
                if (method != null) {
                    value = method.invoke(appender, new Object[] { value });

                }
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), e);
            }
        }
    }

    /**
     * Applies the made changes to the attributes of the appender.
     * This method is called when the user presses the apply button.
     */
    public void applyChanges() {
        String[] attributes = _attributeFields.keySet().toArray(new String[] { });

        for (int x = 0; x < attributes.length; x++) {
            Class<?> type = _attributeTypes.get(attributes[x]);
            JComponent c = _attributeFields.get(attributes[x]);

            if ((type != null) && (c != null)) {
                if (c instanceof JTextField) {
                    String text = ((JTextField) c).getText();

                    if (attributes[x].equals("Layout")) {
                        setValue(_appender, attributes[x], new PatternLayout(text));
                    } else {
                        setValue(_appender, attributes[x], text);
                    }
                } else if (c instanceof JSpinner) {
                    SpinnerModel model = ((JSpinner) c).getModel();
                    setValue(_appender, attributes[x], model.getValue());
                } else if (c instanceof JCheckBox) {
                    setValue(
                        _appender, attributes[x], Boolean.valueOf(((JCheckBox) c).isSelected()));
                } else if (c instanceof JComboBox) {
                    setValue(
                        _appender, attributes[x], ((JComboBox) c).getModel().getSelectedItem());
                }
            }
        }
    }
}