/*
 * 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.ui;

import icy.gui.component.RangeSlider;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Map;

import javax.swing.ButtonModel;
import javax.swing.DefaultButtonModel;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JSlider;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.pushingpixels.lafwidget.LafWidgetUtilities;
import org.pushingpixels.substance.api.ColorSchemeAssociationKind;
import org.pushingpixels.substance.api.ComponentState;
import org.pushingpixels.substance.api.SubstanceColorScheme;
import org.pushingpixels.substance.api.painter.border.SubstanceBorderPainter;
import org.pushingpixels.substance.api.painter.fill.ClassicFillPainter;
import org.pushingpixels.substance.api.painter.fill.SubstanceFillPainter;
import org.pushingpixels.substance.internal.animation.StateTransitionTracker;
import org.pushingpixels.substance.internal.animation.TransitionAwareUI;
import org.pushingpixels.substance.internal.painter.BackgroundPaintingUtils;
import org.pushingpixels.substance.internal.ui.SubstanceSliderUI;
import org.pushingpixels.substance.internal.utils.HashMapKey;
import org.pushingpixels.substance.internal.utils.RolloverControlListener;
import org.pushingpixels.substance.internal.utils.SubstanceColorSchemeUtilities;
import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities;
import org.pushingpixels.substance.internal.utils.SubstanceOutlineUtilities;
import org.pushingpixels.substance.internal.utils.SubstanceSizeUtils;

/**
 * UI delegate for the RangeSlider component with Substance AndFeel.
 * RangeSliderUI paints two thumbs, one for the lower value and one for the upper value.
 * 
 * @author Stephane Dallongeville
 */
public class RangeSliderUI extends SubstanceSliderUI
{
    /** Location and size of thumb for upper value. */
    Rectangle upperThumbRect;
    /** Indicator that determines whether upper thumb is selected. */
    boolean upperThumbSelected;

    /** Indicator that determines whether lower thumb is being dragged. */
    transient boolean lowerDragging;
    /** Indicator that determines whether upper thumb is being dragged. */
    transient boolean upperDragging;

    /**
     * Surrogate button model for tracking the general slider transitions.
     */
    ButtonModel sliderModel;
    /**
     * Surrogate button model for tracking the upper thumb transitions.
     */
    ButtonModel upperThumbModel;
    /**
     * General slider transition tracker.
     */
    protected StateTransitionTracker sliderStateTransitionTracker;
    /**
     * Upper thumb transition tracker.
     */
    protected StateTransitionTracker upperThumbStateTransitionTracker;

    /**
     * Listener for general slider transition animations.
     */
    protected RolloverControlListener sliderRolloverListener;
    /**
     * Listener for upper thumb transition animations.
     */
    protected RolloverControlListener upperThumbRolloverListener;

    /**
     * Listener on property change events.
     */
    protected PropertyChangeListener sliderPropertyChangeListener;

    /**
     * Needed to return correct transition tracker on thumb paint.
     */
    private boolean paintingLowerThumb;
    private boolean paintingUpperThumb;

    /**
     * Constructs a RangeSliderUI for the specified slider component.
     * 
     * @param rangeSlider
     *        RangeSlider
     */
    public RangeSliderUI(RangeSlider rangeSlider)
    {
        super(rangeSlider);

        sliderModel = new DefaultButtonModel();
        sliderModel.setArmed(false);
        sliderModel.setSelected(false);
        sliderModel.setPressed(false);
        sliderModel.setRollover(false);
        sliderModel.setEnabled(rangeSlider.isEnabled());

        upperThumbModel = new DefaultButtonModel();
        upperThumbModel.setArmed(false);
        upperThumbModel.setSelected(false);
        upperThumbModel.setPressed(false);
        upperThumbModel.setRollover(false);
        upperThumbModel.setEnabled(rangeSlider.isEnabled());

        sliderStateTransitionTracker = new StateTransitionTracker(rangeSlider, sliderModel);
        upperThumbStateTransitionTracker = new StateTransitionTracker(rangeSlider, upperThumbModel);

        paintingLowerThumb = false;
        paintingUpperThumb = false;
    }

    /**
     * Installs this UI delegate on the specified component.
     */
    @Override
    public void installUI(JComponent c)
    {
        upperThumbRect = new Rectangle();
        super.installUI(c);
    }

    @Override
    protected void installListeners(JSlider slider)
    {
        super.installListeners(slider);

        sliderRolloverListener = new RolloverControlListener(new TransitionAwareUI()
        {
            @Override
            public boolean isInside(MouseEvent me)
            {
                final double x = me.getX();
                final double y = me.getY();
                return isInsideLowerThumbInternal(x, y) || isInsideUpperThumbInternal(x, y);
            }

            @Override
            public StateTransitionTracker getTransitionTracker()
            {
                return sliderStateTransitionTracker;
            }
        }, sliderModel);

        slider.addMouseListener(sliderRolloverListener);
        slider.addMouseMotionListener(sliderRolloverListener);

        upperThumbRolloverListener = new RolloverControlListener(new TransitionAwareUI()
        {
            @Override
            public boolean isInside(MouseEvent me)
            {
                return isInsideUpperThumb(me.getX(), me.getY());
            }

            @Override
            public StateTransitionTracker getTransitionTracker()
            {
                return upperThumbStateTransitionTracker;
            }
        }, upperThumbModel);

        slider.addMouseListener(upperThumbRolloverListener);
        slider.addMouseMotionListener(upperThumbRolloverListener);

        sliderPropertyChangeListener = new PropertyChangeListener()
        {
            @Override
            public void propertyChange(PropertyChangeEvent evt)
            {
                if ("enabled".equals(evt.getPropertyName()))
                {
                    final boolean enabled = RangeSliderUI.this.slider.isEnabled();

                    sliderModel.setEnabled(enabled);
                    upperThumbModel.setEnabled(enabled);
                }
            }
        };
        slider.addPropertyChangeListener(sliderPropertyChangeListener);

        sliderStateTransitionTracker.registerModelListeners();
        sliderStateTransitionTracker.registerFocusListeners();
        upperThumbStateTransitionTracker.registerModelListeners();
        upperThumbStateTransitionTracker.registerFocusListeners();
    }

    @Override
    protected void uninstallListeners(JSlider slider)
    {
        super.uninstallListeners(slider);

        slider.removeMouseListener(sliderRolloverListener);
        slider.removeMouseMotionListener(sliderRolloverListener);
        sliderRolloverListener = null;
        slider.removeMouseListener(upperThumbRolloverListener);
        slider.removeMouseMotionListener(upperThumbRolloverListener);
        upperThumbRolloverListener = null;

        slider.removePropertyChangeListener(sliderPropertyChangeListener);
        sliderPropertyChangeListener = null;

        sliderStateTransitionTracker.unregisterModelListeners();
        sliderStateTransitionTracker.unregisterFocusListeners();
        upperThumbStateTransitionTracker.unregisterModelListeners();
        upperThumbStateTransitionTracker.unregisterFocusListeners();
    }

    /**
     * Creates a listener to handle track events in the specified slider.
     */
    @Override
    protected TrackListener createTrackListener(JSlider slider)
    {
        return new RangeTrackListener();
    }

    /**
     * Creates a listener to handle change events in the specified slider.
     */
    @Override
    protected ChangeListener createChangeListener(JSlider slider)
    {
        return new ChangeHandler();
    }

    /**
     * Updates the dimensions for both thumbs.
     */
    @Override
    protected void calculateThumbSize()
    {
        // Call superclass method for lower thumb size.
        super.calculateThumbSize();

        // Set upper thumb size.
        upperThumbRect.setSize(thumbRect.width, thumbRect.height);
    }

    /**
     * Updates the locations for both thumbs.
     */
    @Override
    protected void calculateThumbLocation()
    {
        // Call superclass method for lower thumb location.
        super.calculateThumbLocation();

        // Adjust upper value to snap to ticks if necessary.
        if (slider.getSnapToTicks())
        {
            int upperValue = slider.getValue() + slider.getExtent();
            int snappedValue = upperValue;
            int majorTickSpacing = slider.getMajorTickSpacing();
            int minorTickSpacing = slider.getMinorTickSpacing();
            int tickSpacing = 0;

            if (minorTickSpacing > 0)
            {
                tickSpacing = minorTickSpacing;
            }
            else if (majorTickSpacing > 0)
            {
                tickSpacing = majorTickSpacing;
            }

            if (tickSpacing != 0)
            {
                // If it's not on a tick, change the value
                if ((upperValue - slider.getMinimum()) % tickSpacing != 0)
                {
                    float temp = (float) (upperValue - slider.getMinimum()) / (float) tickSpacing;
                    int whichTick = Math.round(temp);
                    snappedValue = slider.getMinimum() + (whichTick * tickSpacing);
                }

                if (snappedValue != upperValue)
                {
                    slider.setExtent(snappedValue - slider.getValue());
                }
            }
        }

        Rectangle trackRect = this.getPaintTrackRect();

        if (slider.getOrientation() == SwingConstants.HORIZONTAL)
        {
            int valuePosition = xPositionForValue(slider.getValue() + slider.getExtent());

            double centerY = trackRect.y + trackRect.height / 2.0;
            upperThumbRect.y = (int) (centerY - upperThumbRect.height / 2.0) + 1;
            upperThumbRect.x = valuePosition - upperThumbRect.width / 2;
        }
        else
        {
            int valuePosition = yPositionForValue(slider.getValue() + slider.getExtent());

            double centerX = trackRect.x + trackRect.width / 2.0;
            upperThumbRect.x = (int) (centerX - upperThumbRect.width / 2.0) + 1;
            upperThumbRect.y = valuePosition - (upperThumbRect.height / 2);
        }
    }

    @Override
    public boolean isInside(MouseEvent me)
    {
        return isInsideLowerThumb(me.getX(), me.getY());
    }

    public boolean isInsideLowerThumbInternal(double x, double y)
    {
        final Rectangle thumbB = this.thumbRect;
        return thumbB != null && thumbB.contains(x, y);
    }

    public boolean isInsideLowerThumb(double x, double y)
    {
        // inside lower ?
        if (isInsideLowerThumbInternal(x, y))
        {
            // also inside upper ?
            if (isInsideUpperThumbInternal(x, y))
            {
                final double dl = Point2D.distance(thumbRect.getCenterX(), thumbRect.getCenterY(), x, y);
                final double du = Point2D.distance(upperThumbRect.getCenterX(), upperThumbRect.getCenterY(), x, y);

                return (dl < du);
            }

            return true;
        }

        return false;
    }

    public boolean isInsideUpperThumbInternal(double x, double y)
    {
        final Rectangle upperThumbR = upperThumbRect;
        return (upperThumbR != null) && upperThumbR.contains(x, y);
    }

    public boolean isInsideUpperThumb(double x, double y)
    {
        // inside lower ?
        if (isInsideUpperThumbInternal(x, y))
        {
            // also inside upper ?
            if (isInsideLowerThumbInternal(x, y))
            {
                // find closest one
                final double dl = Point2D.distance(thumbRect.getCenterX(), thumbRect.getCenterY(), x, y);
                final double du = Point2D.distance(upperThumbRect.getCenterX(), upperThumbRect.getCenterY(), x, y);

                return (du <= dl);
            }

            return true;
        }

        return false;
    }

    @Override
    public StateTransitionTracker getTransitionTracker()
    {
        if (paintingLowerThumb)
            return super.getTransitionTracker();
        if (paintingUpperThumb)
            return upperThumbStateTransitionTracker;

        return sliderStateTransitionTracker;
    }

    /**
     * Returns the rectangle of track for painting.
     * 
     * @return The rectangle of track for painting.
     */
    private Rectangle getPaintTrackRect()
    {
        int trackLeft = 0;
        int trackRight;
        int trackTop = 0;
        int trackBottom;
        int trackWidth = this.getTrackWidth();

        if (this.slider.getOrientation() == SwingConstants.HORIZONTAL)
        {
            trackTop = 3 + this.insetCache.top + 2 * this.focusInsets.top;
            trackBottom = trackTop + trackWidth - 1;
            trackRight = this.trackRect.width;
            return new Rectangle(this.trackRect.x + trackLeft, trackTop, trackRight - trackLeft, trackBottom - trackTop);
        }

        if (this.slider.getPaintLabels() || this.slider.getPaintTicks())
        {
            if (this.slider.getComponentOrientation().isLeftToRight())
            {
                trackLeft = trackRect.x + this.insetCache.left + this.focusInsets.left;
                trackRight = trackLeft + trackWidth - 1;
            }
            else
            {
                trackRight = trackRect.x + trackRect.width - this.insetCache.right - this.focusInsets.right;
                trackLeft = trackRight - trackWidth - 1;
            }
        }
        else
        {
            // horizontally center the track
            if (this.slider.getComponentOrientation().isLeftToRight())
            {
                trackLeft = (this.insetCache.left + this.focusInsets.left + this.slider.getWidth()
                        - this.insetCache.right - this.focusInsets.right)
                        / 2 - trackWidth / 2;
                trackRight = trackLeft + trackWidth - 1;
            }
            else
            {
                trackRight = (this.insetCache.left + this.focusInsets.left + this.slider.getWidth()
                        - this.insetCache.right - this.focusInsets.right)
                        / 2 + trackWidth / 2;
                trackLeft = trackRight - trackWidth - 1;
            }
        }

        trackBottom = this.trackRect.height - 1;
        return new Rectangle(trackLeft, this.trackRect.y + trackTop, trackRight - trackLeft, trackBottom - trackTop);
    }

    @Override
    public void paint(Graphics g, final JComponent c)
    {
        Graphics2D graphics = (Graphics2D) g.create();

        ComponentState currState = ComponentState.getState(sliderModel, slider);
        float alpha = SubstanceColorSchemeUtilities.getAlpha(slider, currState);

        BackgroundPaintingUtils.updateIfOpaque(graphics, c);

        recalculateIfInsetsChanged();
        recalculateIfOrientationChanged();
        final Rectangle clip = graphics.getClipBounds();

        if (!clip.intersects(trackRect) && slider.getPaintTrack())
            calculateGeometry();

        graphics.setComposite(LafWidgetUtilities.getAlphaComposite(this.slider, alpha, g));
        if (slider.getPaintTrack() && clip.intersects(trackRect))
        {
            paintTrack(graphics);
        }
        if (slider.getPaintTicks() && clip.intersects(tickRect))
        {
            paintTicks(graphics);
        }
        // don't paint focus as component is not focusable
        // paintFocus(graphics);
        if (clip.intersects(thumbRect))
        {
            paintLowerThumb(graphics);
        }
        if (clip.intersects(upperThumbRect))
        {
            paintUpperThumb(graphics);
        }
        graphics.setComposite(LafWidgetUtilities.getAlphaComposite(this.slider, 1.0f, g));
        if (slider.getPaintLabels() && clip.intersects(labelRect))
        {
            paintLabels(graphics);
        }

        graphics.dispose();
    }

    public void paintLowerThumb(Graphics g)
    {
        paintingLowerThumb = true;

        // default implementation
        paintThumb(g);

        paintingLowerThumb = false;
    }

    public void paintUpperThumb(Graphics g)
    {
        paintingUpperThumb = true;

        final Graphics2D graphics = (Graphics2D) g.create();
        final Rectangle knobBounds = upperThumbRect;

        graphics.translate(knobBounds.x, knobBounds.y);

        final Icon icon = getIcon();

        if (slider.getOrientation() == SwingConstants.HORIZONTAL)
        {
            if (icon != null)
                icon.paintIcon(this.slider, graphics, -1, 0);
        }
        else
        {
            if (slider.getComponentOrientation().isLeftToRight())
            {
                if (icon != null)
                    icon.paintIcon(this.slider, graphics, 0, -1);
            }
            else
            {
                if (icon != null)
                    icon.paintIcon(this.slider, graphics, 0, 1);
            }
        }

        graphics.dispose();

        paintingUpperThumb = false;
    }

    @Override
    public void paintTrack(Graphics g)
    {
        Graphics2D graphics = (Graphics2D) g.create();

        boolean drawInverted = drawInverted();

        Rectangle paintRect = getPaintTrackRect();

        // Width and height of the painting rectangle.
        int width = paintRect.width;
        int height = paintRect.height;

        if (this.slider.getOrientation() == SwingConstants.VERTICAL)
        {
            // apply rotation / translate transformation on vertical
            // slider tracks
            int temp = width;
            // noinspection SuspiciousNameCombination
            width = height;
            height = temp;
            AffineTransform at = graphics.getTransform();
            at.translate(paintRect.x, width + paintRect.y);
            at.rotate(-Math.PI / 2);
            graphics.setTransform(at);
        }
        else
        {
            graphics.translate(paintRect.x, paintRect.y);
        }

        StateTransitionTracker.ModelStateInfo modelStateInfo = sliderStateTransitionTracker.getModelStateInfo();

        SubstanceColorScheme trackSchemeUnselected = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
                slider.isEnabled() ? ComponentState.ENABLED : ComponentState.DISABLED_UNSELECTED);
        SubstanceColorScheme trackBorderSchemeUnselected = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
                ColorSchemeAssociationKind.BORDER, this.slider.isEnabled() ? ComponentState.ENABLED
                        : ComponentState.DISABLED_UNSELECTED);
        paintSliderTrack(graphics, drawInverted, trackSchemeUnselected, trackBorderSchemeUnselected, width, height);

        Map<ComponentState, StateTransitionTracker.StateContributionInfo> activeStates = modelStateInfo
                .getStateContributionMap();
        for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> activeEntry : activeStates
                .entrySet())
        {
            ComponentState activeState = activeEntry.getKey();
            if (!activeState.isActive())
                continue;

            float contribution = activeEntry.getValue().getContribution();
            if (contribution == 0.0f)
                continue;

            graphics.setComposite(LafWidgetUtilities.getAlphaComposite(slider, contribution, g));

            SubstanceColorScheme activeFillScheme = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
                    activeState);
            SubstanceColorScheme activeBorderScheme = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
                    ColorSchemeAssociationKind.BORDER, activeState);
            paintSliderTrackSelected(graphics, paintRect, activeFillScheme, activeBorderScheme, width, height);
        }

        graphics.dispose();
    }

    /**
     * Paints the slider track.
     * 
     * @param graphics
     *        Graphics.
     * @param drawInverted
     *        Indicates whether the value-range shown for the slider is
     *        reversed.
     * @param fillColorScheme
     *        Fill color scheme.
     * @param borderScheme
     *        Border color scheme.
     * @param width
     *        Track width.
     * @param height
     *        Track height.
     */
    private void paintSliderTrack(Graphics2D graphics, boolean drawInverted, SubstanceColorScheme fillColorScheme,
            SubstanceColorScheme borderScheme, int width, int height)
    {
        Graphics2D g2d = (Graphics2D) graphics.create();

        SubstanceFillPainter fillPainter = ClassicFillPainter.INSTANCE;
        SubstanceBorderPainter borderPainter = SubstanceCoreUtilities.getBorderPainter(this.slider);

        int componentFontSize = SubstanceSizeUtils.getComponentFontSize(this.slider);
        int borderDelta = (int) Math.floor(SubstanceSizeUtils.getBorderStrokeWidth(componentFontSize) / 2.0);
        float radius = SubstanceSizeUtils.getClassicButtonCornerRadius(componentFontSize) / 2.0f;
        int borderThickness = (int) SubstanceSizeUtils.getBorderStrokeWidth(componentFontSize);

        HashMapKey key = SubstanceCoreUtilities.getHashKey(width, height, radius, borderDelta, borderThickness,
                fillColorScheme.getDisplayName(), borderScheme.getDisplayName());

        BufferedImage trackImage = trackCache.get(key);
        if (trackImage == null)
        {
            trackImage = SubstanceCoreUtilities.getBlankImage(width + 1, height + 1);
            Graphics2D cacheGraphics = trackImage.createGraphics();

            Shape contour = SubstanceOutlineUtilities.getBaseOutline(width + 1, height + 1, radius, null, borderDelta);

            fillPainter.paintContourBackground(cacheGraphics, slider, width, height, contour, false, fillColorScheme,
                    false);

            GeneralPath contourInner = SubstanceOutlineUtilities.getBaseOutline(width + 1, height + 1, radius
                    - borderThickness, null, borderThickness + borderDelta);
            borderPainter
                    .paintBorder(cacheGraphics, slider, width + 1, height + 1, contour, contourInner, borderScheme);

            trackCache.put(key, trackImage);
            cacheGraphics.dispose();
        }

        g2d.drawImage(trackImage, 0, 0, null);

        g2d.dispose();
    }

    /**
     * Paints the selected part of the slider track.
     * 
     * @param graphics
     *        Graphics.
     * @param drawInverted
     *        Indicates whether the value-range shown for the slider is
     *        reversed.
     * @param paintRect
     *        Selected portion.
     * @param fillScheme
     *        Fill color scheme.
     * @param borderScheme
     *        Border color scheme.
     * @param width
     *        Track width.
     * @param height
     *        Track height.
     */
    private void paintSliderTrackSelected(Graphics2D graphics, Rectangle paintRect, SubstanceColorScheme fillScheme,
            SubstanceColorScheme borderScheme, int width, int height)
    {
        Graphics2D g2d = (Graphics2D) graphics.create();
        Insets insets = this.slider.getInsets();
        insets.top /= 2;
        insets.left /= 2;
        insets.bottom /= 2;
        insets.right /= 2;

        SubstanceFillPainter fillPainter = SubstanceCoreUtilities.getFillPainter(this.slider);
        SubstanceBorderPainter borderPainter = SubstanceCoreUtilities.getBorderPainter(this.slider);
        float radius = SubstanceSizeUtils.getClassicButtonCornerRadius(SubstanceSizeUtils.getComponentFontSize(slider)) / 2.0f;
        int borderDelta = (int) Math.floor(SubstanceSizeUtils.getBorderStrokeWidth(SubstanceSizeUtils
                .getComponentFontSize(slider)) / 2.0);

        // fill selected portion
        if (this.slider.isEnabled())
        {
            if (this.slider.getOrientation() == SwingConstants.HORIZONTAL)
            {
                int ltPos = thumbRect.x + (this.thumbRect.width / 2) - paintRect.x;
                int utPos = upperThumbRect.x + (this.upperThumbRect.width / 2) - paintRect.x;

                int fillMinX;
                int fillMaxX;

                if (ltPos < utPos)
                {
                    fillMinX = ltPos;
                    fillMaxX = utPos;
                }
                else
                {
                    fillMinX = utPos;
                    fillMaxX = ltPos;
                }

                int fillWidth = fillMaxX - fillMinX;
                int fillHeight = height + 1;
                if ((fillWidth > 0) && (fillHeight > 0))
                {
                    Shape contour = SubstanceOutlineUtilities.getBaseOutline(fillWidth, fillHeight, radius, null,
                            borderDelta);
                    g2d.translate(fillMinX, 0);
                    fillPainter.paintContourBackground(g2d, this.slider, fillWidth, fillHeight, contour, false,
                            fillScheme, false);
                    borderPainter.paintBorder(g2d, this.slider, fillWidth, fillHeight, contour, null, borderScheme);
                }
            }
            else
            {
                int ltPos = thumbRect.y + (this.thumbRect.height / 2) - paintRect.y;
                int utPos = upperThumbRect.y + (this.upperThumbRect.height / 2) - paintRect.y;
                int fillMin;
                int fillMax;

                if (ltPos < utPos)
                {
                    fillMin = ltPos;
                    fillMax = utPos;
                }
                else
                {
                    fillMin = utPos;
                    fillMax = ltPos;
                }

                // if (this.drawInverted())
                // {
                // fillMin = 0;
                // fillMax = middleOfThumb;
                // // fix for issue 368 - inverted vertical sliders
                // g2d.translate(width + 2 - middleOfThumb, 0);
                // }
                // else
                // {
                // fillMin = middleOfThumb;
                // fillMax = width + 1;
                // }

                int fillWidth = fillMax - fillMin;
                int fillHeight = height + 1;
                if ((fillWidth > 0) && (fillHeight > 0))
                {
                    Shape contour = SubstanceOutlineUtilities.getBaseOutline(fillWidth, fillHeight, radius, null,
                            borderDelta);
                    g2d.translate(paintRect.height - fillMax, 0);
                    fillPainter.paintContourBackground(g2d, this.slider, fillWidth, fillHeight, contour, false,
                            fillScheme, false);
                    borderPainter.paintBorder(g2d, this.slider, fillWidth, fillHeight, contour, null, borderScheme);
                }
            }
        }
        g2d.dispose();
    }

    /**
     * Sets the location of the upper thumb, and repaints the slider. This is
     * called when the upper thumb is dragged to repaint the slider. The
     * <code>setThumbLocation()</code> method performs the same task for the
     * lower thumb.
     */
    void setUpperThumbLocation(int x, int y)
    {
        upperThumbRect.setLocation(x, y);
        slider.repaint();
    }

    /**
     * Moves the selected thumb in the specified direction by a block increment.
     * This method is called when the user presses the Page Up or Down keys.
     */
    @Override
    public void scrollByBlock(int direction)
    {
        synchronized (slider)
        {
            int blockIncrement = (slider.getMaximum() - slider.getMinimum()) / 10;
            if (blockIncrement <= 0 && slider.getMaximum() > slider.getMinimum())
            {
                blockIncrement = 1;
            }
            int delta = blockIncrement * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL);

            if (upperThumbSelected)
            {
                int oldValue = ((RangeSlider) slider).getUpperValue();
                ((RangeSlider) slider).setUpperValue(oldValue + delta);
            }
            else
            {
                int oldValue = slider.getValue();
                slider.setValue(oldValue + delta);
            }
        }
    }

    /**
     * Moves the selected thumb in the specified direction by a unit increment.
     * This method is called when the user presses one of the arrow keys.
     */
    @Override
    public void scrollByUnit(int direction)
    {
        synchronized (slider)
        {
            int delta = 1 * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL);

            if (upperThumbSelected)
            {
                int oldValue = ((RangeSlider) slider).getUpperValue();
                ((RangeSlider) slider).setUpperValue(oldValue + delta);
            }
            else
            {
                int oldValue = slider.getValue();
                slider.setValue(oldValue + delta);
            }
        }
    }

    /**
     * Listener to handle model change events. This calculates the thumb
     * locations and repaints the slider if the value change is not caused by
     * dragging a thumb.
     */
    public class ChangeHandler implements ChangeListener
    {
        @Override
        public void stateChanged(ChangeEvent arg0)
        {
            if (!lowerDragging && !upperDragging)
            {
                calculateThumbLocation();
                slider.repaint();
            }
        }
    }

    /**
     * Listener to handle mouse movements in the slider track.
     */
    public class RangeTrackListener extends TrackListener
    {
        @Override
        public void mousePressed(MouseEvent e)
        {
            if (!slider.isEnabled())
                return;

            currentMouseX = e.getX();
            currentMouseY = e.getY();

            if (slider.isRequestFocusEnabled())
                slider.requestFocus();

            // Determine which thumb is pressed. If the upper thumb is
            // selected (last one dragged), then check its position first;
            // otherwise check the position of the lower thumb first.
            boolean lowerPressed = false;
            boolean upperPressed = false;
            if (isInsideLowerThumb(currentMouseX, currentMouseY))
                lowerPressed = true;
            else if (isInsideUpperThumb(currentMouseX, currentMouseY))
                upperPressed = true;

            // Handle lower thumb pressed.
            if (lowerPressed)
            {
                switch (slider.getOrientation())
                {
                    case SwingConstants.VERTICAL:
                        offset = currentMouseY - thumbRect.y;
                        break;
                    case SwingConstants.HORIZONTAL:
                        offset = currentMouseX - thumbRect.x;
                        break;
                }
                upperThumbSelected = false;
                lowerDragging = true;
                return;
            }
            lowerDragging = false;

            // Handle upper thumb pressed.
            if (upperPressed)
            {
                switch (slider.getOrientation())
                {
                    case SwingConstants.VERTICAL:
                        offset = currentMouseY - upperThumbRect.y;
                        break;
                    case SwingConstants.HORIZONTAL:
                        offset = currentMouseX - upperThumbRect.x;
                        break;
                }
                upperThumbSelected = true;
                upperDragging = true;
                return;
            }
            upperDragging = false;
        }

        @Override
        public void mouseReleased(MouseEvent e)
        {
            lowerDragging = false;
            upperDragging = false;
            slider.setValueIsAdjusting(false);
            super.mouseReleased(e);
        }

        @Override
        public void mouseDragged(MouseEvent e)
        {
            if (!slider.isEnabled())
            {
                return;
            }

            currentMouseX = e.getX();
            currentMouseY = e.getY();

            if (lowerDragging)
            {
                slider.setValueIsAdjusting(true);
                moveLowerThumb();

            }
            else if (upperDragging)
            {
                slider.setValueIsAdjusting(true);
                moveUpperThumb();
            }
        }

        @Override
        public boolean shouldScroll(int direction)
        {
            return false;
        }

        /**
         * Moves the location of the lower thumb, and sets its corresponding
         * value in the slider.
         */
        private void moveLowerThumb()
        {
            int thumbMiddle = 0;

            switch (slider.getOrientation())
            {
                case SwingConstants.VERTICAL:
                    int halfThumbHeight = thumbRect.height / 2;
                    int thumbTop = currentMouseY - offset;
                    int trackTop = trackRect.y;
                    int trackBottom = trackRect.y + (trackRect.height - 1);
                    int vMax = yPositionForValue(slider.getValue() + slider.getExtent());

                    // Apply bounds to thumb position.
                    if (drawInverted())
                    {
                        trackBottom = vMax;
                    }
                    else
                    {
                        trackTop = vMax;
                    }
                    thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight);
                    thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight);

                    setThumbLocation(thumbRect.x, thumbTop);

                    // Update slider value.
                    thumbMiddle = thumbTop + halfThumbHeight;
                    slider.setValue(valueForYPosition(thumbMiddle));
                    break;

                case SwingConstants.HORIZONTAL:
                    int halfThumbWidth = thumbRect.width / 2;
                    int thumbLeft = currentMouseX - offset;
                    int trackLeft = trackRect.x;
                    int trackRight = trackRect.x + (trackRect.width - 1);
                    int hMax = xPositionForValue(slider.getValue() + slider.getExtent());

                    // Apply bounds to thumb position.
                    if (drawInverted())
                    {
                        trackLeft = hMax;
                    }
                    else
                    {
                        trackRight = hMax;
                    }
                    thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth);
                    thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth);

                    setThumbLocation(thumbLeft, thumbRect.y);

                    // Update slider value.
                    thumbMiddle = thumbLeft + halfThumbWidth;
                    slider.setValue(valueForXPosition(thumbMiddle));
                    break;

                default:
                    return;
            }
        }

        /**
         * Moves the location of the upper thumb, and sets its corresponding
         * value in the slider.
         */
        private void moveUpperThumb()
        {
            int thumbMiddle = 0;

            switch (slider.getOrientation())
            {
                case SwingConstants.VERTICAL:
                    int halfThumbHeight = thumbRect.height / 2;
                    int thumbTop = currentMouseY - offset;
                    int trackTop = trackRect.y;
                    int trackBottom = trackRect.y + (trackRect.height - 1);
                    int vMin = yPositionForValue(slider.getValue());

                    // Apply bounds to thumb position.
                    if (drawInverted())
                    {
                        trackTop = vMin;
                    }
                    else
                    {
                        trackBottom = vMin;
                    }
                    thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight);
                    thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight);

                    setUpperThumbLocation(thumbRect.x, thumbTop);

                    // Update slider extent.
                    thumbMiddle = thumbTop + halfThumbHeight;
                    slider.setExtent(valueForYPosition(thumbMiddle) - slider.getValue());
                    break;

                case SwingConstants.HORIZONTAL:
                    int halfThumbWidth = thumbRect.width / 2;
                    int thumbLeft = currentMouseX - offset;
                    int trackLeft = trackRect.x;
                    int trackRight = trackRect.x + (trackRect.width - 1);
                    int hMin = xPositionForValue(slider.getValue());

                    // Apply bounds to thumb position.
                    if (drawInverted())
                    {
                        trackRight = hMin;
                    }
                    else
                    {
                        trackLeft = hMin;
                    }
                    thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth);
                    thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth);

                    setUpperThumbLocation(thumbLeft, thumbRect.y);

                    // Update slider extent.
                    thumbMiddle = thumbLeft + halfThumbWidth;
                    slider.setExtent(valueForXPosition(thumbMiddle) - slider.getValue());
                    break;

                default:
                    return;
            }
        }
    }
}