/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.gui.component;

import java.awt.BorderLayout;

import javax.swing.DefaultBoundedRangeModel;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
 * Component letting user to define a range (RangeSlider + 2 inputs fields)
 * 
 * @author Stephane
 */
public class RangeComponent extends JPanel implements ChangeListener
{
    /**
     * 
     */
    private static final long serialVersionUID = 7244476681262628392L;

    final protected JSpinner lowSpinner;
    final protected JSpinner highSpinner;
    final protected RangeSlider slider;

    public RangeComponent(int orientation, double min, double max, double step)
    {
        super();

        lowSpinner = new JSpinner(new SpinnerNumberModel(min, min, max, step));
        lowSpinner.setToolTipText("Set low bound");
        highSpinner = new JSpinner(new SpinnerNumberModel(max, min, max, step));
        highSpinner.setToolTipText("Set high bound");
        slider = new RangeSlider(orientation);
        updateSliderModel();

        lowSpinner.addChangeListener(this);
        highSpinner.addChangeListener(this);
        slider.addChangeListener(this);

        slider.setBorder(new EmptyBorder(0, 0, 0, 0));

        setLayout(new BorderLayout(0, 0));

        if (orientation == SwingConstants.VERTICAL)
        {
            add(lowSpinner, BorderLayout.SOUTH);
            add(slider, BorderLayout.CENTER);
            add(highSpinner, BorderLayout.NORTH);
        }
        else
        {
            add(lowSpinner, BorderLayout.WEST);
            add(slider, BorderLayout.CENTER);
            add(highSpinner, BorderLayout.EAST);
        }

        validate();
    }

    public RangeComponent(double min, double max, double step)
    {
        this(SwingConstants.HORIZONTAL, min, max, step);
    }

    public RangeComponent(int orientation)
    {
        this(orientation, 0d, 100d, 1d);
    }

    public RangeComponent()
    {
        this(SwingConstants.HORIZONTAL, 0d, 100d, 1d);
    }

    public JSpinner getLowSpinner()
    {
        return lowSpinner;
    }

    public JSpinner getHighSpinner()
    {
        return highSpinner;
    }

    public RangeSlider getSlider()
    {
        return slider;
    }

    public SpinnerNumberModel getLowModel()
    {
        return (SpinnerNumberModel) lowSpinner.getModel();
    }

    public SpinnerNumberModel getHighModel()
    {
        return (SpinnerNumberModel) highSpinner.getModel();
    }

    private double getRange()
    {
        return getMax() - getMin();
    }

    private int getSliderRange()
    {
        return slider.getMaximum() - slider.getMinimum();
    }

    private int spinnerToSlider(double value)
    {
        final double spinnerRange = getRange();

        if (spinnerRange == 0)
            return 0;

        return (int) ((value - getMin()) * getSliderRange() / spinnerRange);
    }

    private double sliderToSpinner(int value)
    {
        final int sliderRange = getSliderRange();

        if (sliderRange == 0)
            return 0d;

        return (value * getRange() / sliderRange) + getMin();
    }

    private void updateSliderModel()
    {
        final int sliderRange = (int) Math.round(getRange() / getStep());

        slider.setModel(new DefaultBoundedRangeModel(0, 0, 0, sliderRange));
        slider.setLowerValue(spinnerToSlider(getLow()));
        slider.setUpperValue(spinnerToSlider(getHigh()));
    }

    /**
     * Set the lower and higher range value.
     * 
     * @see #setLow(double)
     * @see #setHigh(double)
     * @see #setMin(double)
     * @see #setMax(double)
     */
    public void setLowHigh(double low, double high)
    {
        getLowModel().setValue(Double.valueOf(low));
        getHighModel().setValue(Double.valueOf(high));
    }

    /**
     * Get the lower range value.
     * 
     * @see #getHigh()
     * @see #getMin()
     */
    public double getLow()
    {
        return getLowModel().getNumber().doubleValue();
    }

    /**
     * Get the higher range value.
     * 
     * @see #getLow()
     * @see #getMax()
     */
    public double getHigh()
    {
        return getHighModel().getNumber().doubleValue();
    }

    /**
     * Set the lower range value.
     * 
     * @see #setHigh(double)
     * @see #setMin(double)
     */
    public void setLow(double value)
    {
        getLowModel().setValue(Double.valueOf(value));
    }

    /**
     * Set the higher range value.
     * 
     * @see #setLow(double)
     * @see #setMax(double)
     */
    public void setHigh(double value)
    {
        getHighModel().setValue(Double.valueOf(value));
    }

    /**
     * Return true if the range use integer number
     */
    public boolean isInteger()
    {
        final SpinnerNumberModel model = getLowModel();
        final Number value = model.getNumber();
        final Number step = model.getStepSize();

        return (value.doubleValue() == value.longValue()) && (step.doubleValue() == step.longValue());
    }

    /**
     * Get range minimum value.
     * 
     * @see #getMax()
     * @see #getLow()
     */
    public double getMin()
    {
        return ((Double) getLowModel().getMinimum()).doubleValue();
    }

    /**
     * Get range minimum value.
     * 
     * @see #getMin()
     * @see #getHigh()
     */
    public double getMax()
    {
        return ((Double) getHighModel().getMaximum()).doubleValue();
    }

    /**
     * Get range step value.
     * 
     * @see #getMin()
     * @see #getMax()
     */
    public double getStep()
    {
        return getLowModel().getStepSize().doubleValue();
    }

    /**
     * Set range bounds and step value.
     * 
     * @see #setMin(double)
     * @see #setMax(double)
     * @see #setStep(double)
     */
    public void setMinMaxStep(double min, double max, double step)
    {
        final double low = Math.max(Math.min(getLow(), max), min);
        final double high = Math.max(Math.min(getHigh(), max), min);

        lowSpinner.setModel(new SpinnerNumberModel(low, min, max, step));
        highSpinner.setModel(new SpinnerNumberModel(high, min, max, step));

        updateSliderModel();
    }

    /**
     * Set range bounds value.
     * 
     * @see #setMin(double)
     * @see #setMax(double)
     * @see #setLow(double)
     * @see #setHigh(double)
     */
    public void setMinMax(double min, double max)
    {
        setMinMaxStep(min, max, getStep());
    }

    /**
     * Set range minimum value.
     * 
     * @see #setMax(double)
     * @see #setLow(double)
     */
    public void setMin(double value)
    {
        setMinMaxStep(value, getMax(), getStep());
    }

    /**
     * Set range maximum value.
     * 
     * @see #setMin(double)
     * @see #setHigh(double)
     */
    public void setMax(double value)
    {
        setMinMaxStep(getMin(), value, getStep());
    }

    /**
     * Set range step value.
     * 
     * @see #setMin(double)
     * @see #setMax(double)
     */
    public void setStep(double value)
    {
        setMinMaxStep(getMin(), getMax(), value);
    }

    /**
     * Set slider visible or not.
     */
    public void setSliderVisible(boolean value)
    {
        if (value)
            add(slider, BorderLayout.CENTER);
        else
            add(new JLabel(" - "), BorderLayout.CENTER);
    }

    @Override
    public void setToolTipText(String text)
    {
        slider.setToolTipText(text);

        super.setToolTipText(text);
    }

    @Override
    public void setEnabled(boolean enabled)
    {
        lowSpinner.setEnabled(enabled);
        highSpinner.setEnabled(enabled);
        slider.setEnabled(enabled);

        super.setEnabled(enabled);
    }

    protected void fireChangedEvent(ChangeEvent event)
    {
        for (ChangeListener listener : getListeners(ChangeListener.class))
            listener.stateChanged(event);
    }

    public void addChangeListener(ChangeListener listener)
    {
        listenerList.add(ChangeListener.class, listener);
    }

    public void removeChangeListener(ChangeListener listener)
    {
        listenerList.remove(ChangeListener.class, listener);
    }

    @Override
    public void stateChanged(ChangeEvent e)
    {
        final Object source = e.getSource();
        final double low = getLow();
        final double high = getHigh();

        if (source == lowSpinner)
        {
            slider.setLowerValue(spinnerToSlider(low));

            if (high < low)
                setHigh(low);

            getHighModel().setMinimum(Double.valueOf(low));
        }
        else if (source == highSpinner)
        {
            slider.setUpperValue(spinnerToSlider(high));

            if (low > high)
                setLow(high);

            getLowModel().setMaximum(Double.valueOf(high));
        }
        else if (source == slider)
        {
            setLow(sliderToSpinner(slider.getLowerValue()));
            setHigh(sliderToSpinner(slider.getUpperValue()));
        }

        fireChangedEvent(e);
    }
}