package usda.weru.util;

import java.awt.event.FocusEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.Iterator;
import java.util.List;
import javax.measure.Quantity;
import tec.uom.se.quantity.Quantities;
import javax.measure.Unit;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.text.DefaultFormatterFactory;

/**
 *
 * @param <Q> Quantity to type the field to.
 * @author Joseph Levin <joelevin@weru.ksu.edu>
 */
public class MeasurableField<Q extends Quantity<Q>> extends JFormattedTextField {

    private static final long serialVersionUID = 1L;

    public static final String PROP_UNITS_LABEL = "unitsLabel";
    public static final String PROP_UNITS = "units";
    public static final String PROP_DEFAULT_PATTERN = "defaultPattern";
    public static final String PROP_EDIT_PATTERN = "editPattern";
    public static final String PROP_DISPLAY_PATTERN = "displayPattern";
    public static final String PROP_VALUE = "value";
    
    protected JLabel c_unitsLabel;
    protected Unit<Q> c_units;
    protected final DefaultFormatterFactory c_formatterFactory;

    private boolean c_fireValueChanges = true;

    private String c_defaultPattern;
    private String c_editPattern;
    private String c_displayPattern;

    private final List<ValueListener<Q>> c_listeners;

    public MeasurableField() {
        c_listeners = new ArrayList<>();
        c_formatterFactory = new DefaultFormatterFactory();
        setFormatterFactory(c_formatterFactory);
        setDefaultPattern("#0.00000");

        addPropertyChangeListener(PROP_VALUE, new PropertyChangeListener() {

            @Override
            @SuppressWarnings("unchecked")
            public void propertyChange(PropertyChangeEvent evt) {
                if (!c_fireValueChanges) {
                    return;
                }
                Quantity<Q> oldValue = (Quantity<Q>) evt.getOldValue();
                Quantity<Q> newValue = (Quantity<Q>) evt.getNewValue();
                Iterator<ValueListener<Q>> it = c_listeners.iterator();

                while (it.hasNext()) {
                    it.next().valueChanged(oldValue, newValue);
                }
            }
        });
    }

    public void setUnitsLabel(JLabel label) {
        JLabel old = c_unitsLabel;
        c_unitsLabel = label;
        updateUnitsLabel();
        firePropertyChange(PROP_UNITS_LABEL, old, c_unitsLabel);
    }

    public JLabel getUnitsLabel() {
        return c_unitsLabel;
    }

    public void setUnits(Unit<Q> units) {
        Unit<Q> old = c_units;
        c_units = units;
        setValue(getValue());
        updateUnitsLabel();
        firePropertyChange(PROP_UNITS, old, c_units);
    }

    public Unit<Q> getUnits() {
        return c_units;
    }

    public String getUnitSymbol() {
        return c_units.getSymbol();
    }
    
    public String getUnitName() {
        return c_units.getName();
    }
    
    private void updateUnitsLabel() {
        if (c_unitsLabel != null) {
            if (c_units != null) {
                if(c_unitsLabel.getText().equals("deg")) {
                    //Don't change the latitude/longitude fields
                    c_unitsLabel.setText("deg");
                }else {
                    if(c_units.toString().contains("rev/360")) {
                        c_unitsLabel.setText("deg");
                    } else {
                        c_unitsLabel.setText(c_units.toString());
                    }
                }
            } else {
                c_unitsLabel.setText("?");
            }
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public Quantity<Q> getValue() {
        if (super.getValue() instanceof Quantity) {
            return (Quantity<Q>) super.getValue();
        } else {
            return null;
        }
    }

    @Override
    @Deprecated
    @SuppressWarnings("unchecked")
    public void setValue(Object value) {
        //Although deprecated, we pass off valid values so the beans binding will work.
        if (value == null || value instanceof Quantity) {
            setValue((Quantity<Q>) value);
        } else {
            throw new IllegalArgumentException("Value must be a Measurable<Q> object.");
        }
    }

    public void setValue(Quantity<Q> value) {
        c_fireValueChanges = false;
        try {
            super.setValue(value);
        } finally {
            c_fireValueChanges = true;
        }
    }

    @Override
    protected void processFocusEvent(FocusEvent e) {

        if (FocusEvent.FOCUS_GAINED == e.getID()) {
            super.processFocusEvent(e);
            selectAll();
        } else if (FocusEvent.FOCUS_LOST == e.getID()) {
            try {
                super.commitEdit();
                super.processFocusEvent(e);
            } catch (ParseException pe) {
                JOptionPane.showMessageDialog(MeasurableField.this,
                        "Unable to parse value: " + getText(),
                        "Numeric Value", JOptionPane.WARNING_MESSAGE);
                setValue(getValue());
                MeasurableField.this.requestFocusInWindow();
            }

        }
    }

    public void setDefaultPattern(String pattern) {
        String old = c_defaultPattern;
        c_defaultPattern = pattern;
        c_formatterFactory.setDefaultFormatter(new InternalMeasurableFormatter(c_defaultPattern));
        firePropertyChange(PROP_DEFAULT_PATTERN, old, c_defaultPattern);
    }

    public String getDefaultPattern() {
        return c_defaultPattern;
    }

    public void setEditPattern(String pattern) {
        String old = c_editPattern;
        c_editPattern = pattern;
        c_formatterFactory.setEditFormatter(new InternalMeasurableFormatter(c_editPattern));
        firePropertyChange(PROP_EDIT_PATTERN, old, c_editPattern);
    }

    public String getEditPattern() {
        return c_editPattern;
    }

    public void setDisplayPattern(String pattern) {
        String old = c_displayPattern;
        c_displayPattern = pattern;
        c_formatterFactory.setDisplayFormatter(new InternalMeasurableFormatter(c_displayPattern));
        firePropertyChange(PROP_DISPLAY_PATTERN, old, c_displayPattern);
    }

    public String getDisplayPattern() {
        return c_displayPattern;
    }

    private class InternalMeasurableFormatter extends AbstractFormatter {

        private static final long serialVersionUID = 1L;

        private final NumberFormat c_format;

        public InternalMeasurableFormatter(String pattern) {
            c_format = new DecimalFormat(pattern);
        }

        @Override
        public Object stringToValue(String text) throws ParseException {
            try {
                Number number = c_format.parse(text);
                Double value = number.doubleValue();
                return Quantities.getQuantity(value, c_units);
            } catch (ParseException e) {
                throw new ParseException(text, 0);
            }

        }

        @Override
        @SuppressWarnings("unchecked")
        public String valueToString(Object value) throws ParseException {
            try {
                if (value instanceof Quantity) {
                    Quantity<Q> measure = (Quantity<Q>) value;
                    double d = measure.to(c_units).getValue().doubleValue();
                    return c_format.format(d);
                } else {
                    return null;
                }
            } catch (Exception e) {
                throw new ParseException(null, 0);
            }
        }
    }

    public void addValueListener(ValueListener<Q> listener) {
        c_listeners.add(listener);
    }

    public void removeValueListener(ValueListener<Q> listener) {
        c_listeners.remove(listener);
    }

    public interface ValueListener<Q extends Quantity<Q>> extends EventListener {

        public void valueChanged(Quantity<Q> oldValue, Quantity<Q> newValue);
    }

}
