/*
 * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.apple.laf;

import java.awt.*;
import java.awt.event.MouseEvent;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicSliderUI;

import apple.laf.*;
import apple.laf.JRSUIUtils.NineSliceMetricsProvider;
import apple.laf.JRSUIConstants.*;

import com.apple.laf.AquaUtilControlSize.*;
import com.apple.laf.AquaImageFactory.NineSliceMetrics;
import com.apple.laf.AquaUtils.RecyclableSingleton;

public class AquaSliderUI extends BasicSliderUI implements Sizeable {
//    static final Dimension roundThumbSize = new Dimension(21 + 4, 21 + 4); // +2px on both sides for focus fuzz
//    static final Dimension pointingThumbSize = new Dimension(19 + 4, 22 + 4);

    protected static final RecyclableSingleton<SizeDescriptor> roundThumbDescriptor = new RecyclableSingleton<SizeDescriptor>() {
        protected SizeDescriptor getInstance() {
            return new SizeDescriptor(new SizeVariant(25, 25)) {
                public SizeVariant deriveSmall(final SizeVariant v) {
                    return super.deriveSmall(v.alterMinSize(-2, -2));
                }
                public SizeVariant deriveMini(final SizeVariant v) {
                    return super.deriveMini(v.alterMinSize(-2, -2));
                }
            };
        }
    };
    protected static final RecyclableSingleton<SizeDescriptor> pointingThumbDescriptor = new RecyclableSingleton<SizeDescriptor>() {
        protected SizeDescriptor getInstance() {
            return new SizeDescriptor(new SizeVariant(23, 26)) {
                public SizeVariant deriveSmall(final SizeVariant v) {
                    return super.deriveSmall(v.alterMinSize(-2, -2));
                }
                public SizeVariant deriveMini(final SizeVariant v) {
                    return super.deriveMini(v.alterMinSize(-2, -2));
                }
            };
        }
    };

    static final AquaPainter<JRSUIState> trackPainter = AquaPainter.create(JRSUIStateFactory.getSliderTrack(), new NineSliceMetricsProvider() {
        @Override
        public NineSliceMetrics getNineSliceMetricsForState(JRSUIState state) {
            if (state.is(Orientation.VERTICAL)) {
                return new NineSliceMetrics(5, 7, 0, 0, 3, 3, true, false, true);
            }
            return new NineSliceMetrics(7, 5, 3, 3, 0, 0, true, true, false);
        }
    });
    final AquaPainter<JRSUIState> thumbPainter = AquaPainter.create(JRSUIStateFactory.getSliderThumb());

    protected Color tickColor;
    protected Color disabledTickColor;

    protected transient boolean fIsDragging = false;

    // From AppearanceManager doc
    static final int kTickWidth = 3;
    static final int kTickLength = 8;

    // Create PLAF
    public static ComponentUI createUI(final JComponent c) {
        return new AquaSliderUI((JSlider)c);
    }

    public AquaSliderUI(final JSlider b) {
        super(b);
    }

    public void installUI(final JComponent c) {
        super.installUI(c);

        LookAndFeel.installProperty(slider, "opaque", Boolean.FALSE);
        tickColor = UIManager.getColor("Slider.tickColor");
    }

    protected BasicSliderUI.TrackListener createTrackListener(final JSlider s) {
        return new TrackListener();
    }

    protected void installListeners(final JSlider s) {
        super.installListeners(s);
        AquaFocusHandler.install(s);
        AquaUtilControlSize.addSizePropertyListener(s);
    }

    protected void uninstallListeners(final JSlider s) {
        AquaUtilControlSize.removeSizePropertyListener(s);
        AquaFocusHandler.uninstall(s);
        super.uninstallListeners(s);
    }

    public void applySizeFor(final JComponent c, final Size size) {
        thumbPainter.state.set(size);
        trackPainter.state.set(size);
    }

    // Paint Methods
    public void paint(final Graphics g, final JComponent c) {
        // We have to override paint of BasicSliderUI because we need slight differences.
        // We don't paint focus the same way - it is part of the thumb.
        // We also need to repaint the whole track when the thumb moves.
        recalculateIfInsetsChanged();
        final Rectangle clip = g.getClipBounds();

        final Orientation orientation = slider.getOrientation() == SwingConstants.HORIZONTAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;
        final State state = getState();

        if (slider.getPaintTrack()) {
            // This is needed for when this is used as a renderer. It is the same as BasicSliderUI.java
            // and is missing from our reimplementation.
            //
            // <rdar://problem/3721898> JSlider in TreeCellRenderer component not painted properly.
            //
            final boolean trackIntersectsClip = clip.intersects(trackRect);
            if (!trackIntersectsClip) {
                calculateGeometry();
            }

            if (trackIntersectsClip || clip.intersects(thumbRect)) paintTrack(g, c, orientation, state);
        }

        if (slider.getPaintTicks() && clip.intersects(tickRect)) {
            paintTicks(g);
        }

        if (slider.getPaintLabels() && clip.intersects(labelRect)) {
            paintLabels(g);
        }

        if (clip.intersects(thumbRect)) {
            paintThumb(g, c, orientation, state);
        }
    }

    // Paints track and thumb
    public void paintTrack(final Graphics g, final JComponent c, final Orientation orientation, final State state) {
        trackPainter.state.set(orientation);
        trackPainter.state.set(state);

        // for debugging
        //g.setColor(Color.green);
        //g.drawRect(trackRect.x, trackRect.y, trackRect.width - 1, trackRect.height - 1);
        trackPainter.paint(g, c, trackRect.x, trackRect.y, trackRect.width, trackRect.height);
    }

    // Paints thumb only
    public void paintThumb(final Graphics g, final JComponent c, final Orientation orientation, final State state) {
        thumbPainter.state.set(orientation);
        thumbPainter.state.set(state);
        thumbPainter.state.set(slider.hasFocus() ? Focused.YES : Focused.NO);
        thumbPainter.state.set(getDirection(orientation));

        // for debugging
        //g.setColor(Color.blue);
        //g.drawRect(thumbRect.x, thumbRect.y, thumbRect.width - 1, thumbRect.height - 1);
        thumbPainter.paint(g, c, thumbRect.x, thumbRect.y, thumbRect.width, thumbRect.height);
    }

    Direction getDirection(final Orientation orientation) {
        if (shouldUseArrowThumb()) {
            return orientation == Orientation.HORIZONTAL ? Direction.DOWN : Direction.RIGHT;
        }

        return Direction.NONE;
    }

    State getState() {
        if (!slider.isEnabled()) {
            return State.DISABLED;
        }

        if (fIsDragging) {
            return State.PRESSED;
        }

        if (!AquaFocusHandler.isActive(slider)) {
            return State.INACTIVE;
        }

        return State.ACTIVE;
    }

    public void paintTicks(final Graphics g) {
        if (slider.isEnabled()) {
            g.setColor(tickColor);
        } else {
            if (disabledTickColor == null) {
                disabledTickColor = new Color(tickColor.getRed(), tickColor.getGreen(), tickColor.getBlue(), tickColor.getAlpha() / 2);
            }
            g.setColor(disabledTickColor);
        }

        super.paintTicks(g);
    }

    // Layout Methods

    // Used lots
    protected void calculateThumbLocation() {
        super.calculateThumbLocation();

        if (shouldUseArrowThumb()) {
            final boolean isHorizonatal = slider.getOrientation() == SwingConstants.HORIZONTAL;
            final Size size = AquaUtilControlSize.getUserSizeFrom(slider);

            if (size == Size.REGULAR) {
                if (isHorizonatal) thumbRect.y += 3; else thumbRect.x += 2; return;
            }

            if (size == Size.SMALL) {
                if (isHorizonatal) thumbRect.y += 2; else thumbRect.x += 2; return;
            }

            if (size == Size.MINI) {
                if (isHorizonatal) thumbRect.y += 1; return;
            }
        }
    }

    // Only called from calculateGeometry
    protected void calculateThumbSize() {
        final SizeDescriptor descriptor = shouldUseArrowThumb() ? pointingThumbDescriptor.get() : roundThumbDescriptor.get();
        final SizeVariant variant = descriptor.get(slider);

        if (slider.getOrientation() == SwingConstants.HORIZONTAL) {
            thumbRect.setSize(variant.w, variant.h);
        } else {
            thumbRect.setSize(variant.h, variant.w);
        }
    }

    protected boolean shouldUseArrowThumb() {
        if (slider.getPaintTicks() || slider.getPaintLabels()) return true;

        final Object shouldPaintArrowThumbProperty = slider.getClientProperty("Slider.paintThumbArrowShape");
        if (shouldPaintArrowThumbProperty != null && shouldPaintArrowThumbProperty instanceof Boolean) {
            return ((Boolean)shouldPaintArrowThumbProperty).booleanValue();
        }

        return false;
    }

    protected void calculateTickRect() {
        // super assumes tickRect ends align with trackRect ends.
        // Ours need to inset by trackBuffer
        // Ours also needs to be *inside* trackRect
        final int tickLength = slider.getPaintTicks() ? getTickLength() : 0;
        if (slider.getOrientation() == SwingConstants.HORIZONTAL) {
            tickRect.height = tickLength;
            tickRect.x = trackRect.x + trackBuffer;
            tickRect.y = trackRect.y + trackRect.height - (tickRect.height / 2);
            tickRect.width = trackRect.width - (trackBuffer * 2);
        } else {
            tickRect.width = tickLength;
            tickRect.x = trackRect.x + trackRect.width - (tickRect.width / 2);
            tickRect.y = trackRect.y + trackBuffer;
            tickRect.height = trackRect.height - (trackBuffer * 2);
        }
    }

    // Basic's preferred size doesn't allow for our focus ring, throwing off things like SwingSet2
    public Dimension getPreferredHorizontalSize() {
        return new Dimension(190, 21);
    }

    public Dimension getPreferredVerticalSize() {
        return new Dimension(21, 190);
    }

    protected ChangeListener createChangeListener(final JSlider s) {
        return new ChangeListener() {
            public void stateChanged(final ChangeEvent e) {
                if (fIsDragging) return;
                calculateThumbLocation();
                slider.repaint();
            }
        };
    }

    // This is copied almost verbatim from superclass, except we changed things to use fIsDragging
    // instead of isDragging since isDragging was a private member.
    class TrackListener extends javax.swing.plaf.basic.BasicSliderUI.TrackListener {
        protected transient int offset;
        protected transient int currentMouseX = -1, currentMouseY = -1;

        public void mouseReleased(final MouseEvent e) {
            if (!slider.isEnabled()) return;

            currentMouseX = -1;
            currentMouseY = -1;

            offset = 0;
            scrollTimer.stop();

            // This is the way we have to determine snap-to-ticks.  It's hard to explain
            // but since ChangeEvents don't give us any idea what has changed we don't
            // have a way to stop the thumb bounds from being recalculated.  Recalculating
            // the thumb bounds moves the thumb over the current value (i.e., snapping
            // to the ticks).
            if (slider.getSnapToTicks() /*|| slider.getSnapToValue()*/) {
                fIsDragging = false;
                slider.setValueIsAdjusting(false);
            } else {
                slider.setValueIsAdjusting(false);
                fIsDragging = false;
            }

            slider.repaint();
        }

        public void mousePressed(final MouseEvent e) {
            if (!slider.isEnabled()) return;

            // We should recalculate geometry just before
            // calculation of the thumb movement direction.
            // It is important for the case, when JSlider
            // is a cell editor in JTable. See 6348946.
            calculateGeometry();

            final boolean firstClick = (currentMouseX == -1) && (currentMouseY == -1);

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

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

            boolean isMouseEventInThumb = thumbRect.contains(currentMouseX, currentMouseY);

            // we don't want to move the thumb if we just clicked on the edge of the thumb
            if (!firstClick || !isMouseEventInThumb) {
                slider.setValueIsAdjusting(true);

                switch (slider.getOrientation()) {
                    case SwingConstants.VERTICAL:
                        slider.setValue(valueForYPosition(currentMouseY));
                        break;
                    case SwingConstants.HORIZONTAL:
                        slider.setValue(valueForXPosition(currentMouseX));
                        break;
                }

                slider.setValueIsAdjusting(false);

                isMouseEventInThumb = true; // since we just moved it in there
            }

            // Clicked in the Thumb area?
            if (isMouseEventInThumb) {
                switch (slider.getOrientation()) {
                    case SwingConstants.VERTICAL:
                        offset = currentMouseY - thumbRect.y;
                        break;
                    case SwingConstants.HORIZONTAL:
                        offset = currentMouseX - thumbRect.x;
                        break;
                }

                fIsDragging = true;
                return;
            }

            fIsDragging = false;
        }

        public boolean shouldScroll(final int direction) {
            final Rectangle r = thumbRect;
            if (slider.getOrientation() == SwingConstants.VERTICAL) {
                if (drawInverted() ? direction < 0 : direction > 0) {
                    if (r.y + r.height <= currentMouseY) return false;
                } else {
                    if (r.y >= currentMouseY) return false;
                }
            } else {
                if (drawInverted() ? direction < 0 : direction > 0) {
                    if (r.x + r.width >= currentMouseX) return false;
                } else {
                    if (r.x <= currentMouseX) return false;
                }
            }

            if (direction > 0 && slider.getValue() + slider.getExtent() >= slider.getMaximum()) {
                return false;
            }

            if (direction < 0 && slider.getValue() <= slider.getMinimum()) {
                return false;
            }

            return true;
        }

        /**
         * Set the models value to the position of the top/left
         * of the thumb relative to the origin of the track.
         */
        public void mouseDragged(final MouseEvent e) {
            int thumbMiddle = 0;

            if (!slider.isEnabled()) return;

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

            if (!fIsDragging) return;

            slider.setValueIsAdjusting(true);

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

                    if (drawInverted()) {
                        trackBottom = vMax;
                    } else {
                        trackTop = vMax;
                    }
                    thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight);
                    thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight);

                    setThumbLocation(thumbRect.x, thumbTop);

                    thumbMiddle = thumbTop + halfThumbHeight;
                    slider.setValue(valueForYPosition(thumbMiddle));
                    break;
                case SwingConstants.HORIZONTAL:
                    final int halfThumbWidth = thumbRect.width / 2;
                    int thumbLeft = e.getX() - offset;
                    int trackLeft = trackRect.x;
                    int trackRight = trackRect.x + (trackRect.width - 1);
                    final int hMax = xPositionForValue(slider.getMaximum() - slider.getExtent());

                    if (drawInverted()) {
                        trackLeft = hMax;
                    } else {
                        trackRight = hMax;
                    }
                    thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth);
                    thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth);

                    setThumbLocation(thumbLeft, thumbRect.y);

                    thumbMiddle = thumbLeft + halfThumbWidth;
                    slider.setValue(valueForXPosition(thumbMiddle));
                    break;
                default:
                    return;
            }

            // enable live snap-to-ticks <rdar://problem/3165310>
            if (slider.getSnapToTicks()) {
                calculateThumbLocation();
                setThumbLocation(thumbRect.x, thumbRect.y); // need to call to refresh the repaint region
            }
        }

        public void mouseMoved(final MouseEvent e) { }
    }

    // Super handles snap-to-ticks by recalculating the thumb rect in the TrackListener
    // See setThumbLocation for why that doesn't work
    int getScale() {
        if (!slider.getSnapToTicks()) return 1;
        int scale = slider.getMinorTickSpacing();
            if (scale < 1) scale = slider.getMajorTickSpacing();
        if (scale < 1) return 1;
        return scale;
    }
}
