/*
Copyright 2008-2010 Gephi
Authors : Mathieu Bastian <mathieu.bastian@gephi.org>
Website : http://www.gephi.org

This file is part of Gephi.

DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.

Copyright 2011 Gephi Consortium. All rights reserved.

The contents of this file are subject to the terms of either the GNU
General Public License Version 3 only ("GPL") or the Common
Development and Distribution License("CDDL") (collectively, the
"License"). You may not use this file except in compliance with the
License. You can obtain a copy of the License at
http://gephi.org/about/legal/license-notice/
or /cddl-1.0.txt and /gpl-3.0.txt. See the License for the
specific language governing permissions and limitations under the
License.  When distributing the software, include this License Header
Notice in each file and include the License files at
/cddl-1.0.txt and /gpl-3.0.txt. If applicable, add the following below the
License Header, with the fields enclosed by brackets [] replaced by
your own identifying information:
"Portions Copyrighted [year] [name of copyright owner]"

If you wish your version of this file to be governed by only the CDDL
or only the GPL Version 3, indicate your decision by adding
"[Contributor] elects to include this software in this distribution
under the [CDDL or GPL Version 3] license." If you do not indicate a
single choice of license, a recipient has the option to distribute
your version of this file under either the CDDL, the GPL Version 3 or
to extend the choice of license to its licensees as provided above.
However, if you add GPL Version 3 code and therefore, elected the GPL
Version 3 license, then the option applies only if the new code is
made subject to such option by the copyright holder.

Contributor(s):

Portions Copyrighted 2011 Gephi Consortium.
 */
package org.gephi.ui.components.gradientslider;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import javax.swing.JComponent;

/**
 * The UI for the GradientSlider class.
 *
 * There are 3 properties you can use to customize the UI of a GradientSlider.
 * You can set these for each slider by calling
 * <code>slider.putClientProperty(key,value);</code>
 * <P>
 * Or you can set these globally by calling:
 * <BR><code>UIManager.put(key,value);</code>
 * <P>
 * The three properties are:
 * <TABLE BORDER="1" CELLPADDING="5" summary="">
 * <TR>
 * <TD>Property Name</TD><TD>Default Value</TD><TD>Description
 * <TR>
 * <TD>GradientSlider.useBevel</TD><TD>"false"</TD><TD>If this is
 * <code>true</code>, then this slider will be painted in a rectangle with a
 * bevel effect around the borders. If this is <code>false</code>, then this
 * slider will be painted in a rounded rectangle.
 * <TR>
 * <TD>GradientSlider.showTranslucency</TD><TD>"true"</TD><TD>If this is
 * <code>true</code>, then the slider will reflect the opacity of the colors in
 * the gradient, and paint a checkered background underneath the colors to
 * indicate opacity. If this is <code>false</code>, then this slider will always
 * paint with completely opaque colors, although the actual colors may be
 * translucent.
 * <TR>
 * <TD>GradientSlider.includeOpacity</TD><TD>"true"</TD><TD>This is used when
 * the user double-clicks a color and a ColorPicker dialog is invoked. (So this
 * value may not have any meaning if you override
 * <code>GradientSlider.doDoubleClick()</code>.) This controls whether the
 * opacity/alpha controls are available in that dialog. This does <i>not</i>
 * control whether translucent colors can be used in this slider: translucent
 * colors are always allowed, if the user can enter them.</TD>
 * </TR>
 * <TR>
 * <TD>MultiThumbSlider.indicateComponent</TD><TD>"true"</TD><TD>If this is
 * <code>true</code>, then the thumbs will only paint on this component when the
 * mouse is inside this slider <i>or</i> when this slider as the keyboard
 * focus.
 * <TR>
 * <TD>MultiThumbSlider.indicateThumb</TD><TD>"true"</TD><TD>If this is
 * <code>true</code>, then the thumb the mouse is over will gently fade into a
 * slightly different color.
 * </TABLE>
 */
public class GradientSliderUI extends MultiThumbSliderUI {

    int TRIANGLE_SIZE = 8;
    /**
     * The width of this image is the absolute widest the track will ever
     * become.
     */
    BufferedImage img = new BufferedImage(1000, 1, BufferedImage.TYPE_INT_ARGB);
    /**
     * A temporary array used for the buffered image
     */
    int[] array = new int[img.getWidth()];

    public GradientSliderUI(GradientSlider slider) {
        super(slider);
    }

    @Override
    public int getClickLocationTolerance() {
        return TRIANGLE_SIZE;
    }

    protected void calculateImage() {
        float[] f = slider.getThumbPositions();
        Color[] c = ((GradientSlider) slider).getColors();

        /**
         * make sure we DO have a value at 0 and 1:
         */
        if (f[0] != 0) {
            float[] f2 = new float[f.length + 1];
            System.arraycopy(f, 0, f2, 1, f.length);
            Color[] c2 = new Color[c.length + 1];
            System.arraycopy(c, 0, c2, 1, f.length);
            f = f2;
            c = c2;
            f[0] = 0;
            c[0] = c[1];
        }
        if (f[f.length - 1] != 1) {
            float[] f2 = new float[f.length + 1];
            System.arraycopy(f, 0, f2, 0, f.length);
            Color[] c2 = new Color[c.length + 1];
            System.arraycopy(c, 0, c2, 0, f.length);
            f = f2;
            c = c2;
            f[f.length - 1] = 1;
            c[c.length - 1] = c[c.length - 2];
        }

        /**
         * Now, finally paint
         */
        int[] argb = new int[c.length];
        for (int a = 0; a < argb.length; a++) {
            argb[a] = ((c[a].getAlpha() & 0xff) << 24)
                    + ((c[a].getRed() & 0xff) << 16)
                    + ((c[a].getGreen() & 0xff) << 8)
                    + ((c[a].getBlue() & 0xff) << 0);
        }
        int max;
        if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
            max = trackRect.width;
        } else {
            max = trackRect.height;
        }
        if (max <= 0) {
            return;
        }

        boolean alwaysOpaque = getProperty(slider, "GradientSlider.showTranslucency", "true").equals("false");

        float fraction;
        int i1 = 0;
        int i2 = 1;
        int a1 = (argb[0] >> 24) & 0xff;
        int r1 = (argb[0] & 0x00ff0000) >> 16;
        int g1 = (argb[0] & 0x0000ff00) >> 8;
        int b1 = (argb[0] & 0x000000ff) >> 0;
        int a2 = (argb[1] >> 24) & 0xff;
        int r2 = (argb[1] & 0x00ff0000) >> 16;
        int g2 = (argb[1] & 0x0000ff00) >> 8;
        int b2 = (argb[1] & 0x000000ff) >> 0;
        for (int z = 0; z < max; z++) {
            fraction = ((float) z) / ((float) (max - 1));
            if (fraction < 1 && fraction >= f[i2]) {
                while (fraction < 1 && fraction >= f[i2]) {
                    i1++;
                    i2++;
                }

                a1 = (argb[i1] >> 24) & 0xff;
                r1 = (argb[i1] & 0x00ff0000) >> 16;
                g1 = (argb[i1] & 0x0000ff00) >> 8;
                b1 = (argb[i1] & 0x000000ff) >> 0;
                a2 = (argb[i2] >> 24) & 0xff;
                r2 = (argb[i2] & 0x00ff0000) >> 16;
                g2 = (argb[i2] & 0x0000ff00) >> 8;
                b2 = (argb[i2] & 0x000000ff) >> 0;
            }
            float colorFraction = (fraction - f[i1]) / (f[i2] - f[i1]);
            if (colorFraction > 1) {
                colorFraction = 1;
            }
            if (colorFraction < 0) {
                colorFraction = 0;
            }
            if (alwaysOpaque) {
                a1 = 255;
                a2 = 255;
            }
            array[z] = (((int) (a1 * (1 - colorFraction) + a2 * colorFraction)) << 24)
                    + (((int) (r1 * (1 - colorFraction) + r2 * colorFraction)) << 16)
                    + (((int) (g1 * (1 - colorFraction) + g2 * colorFraction)) << 8)
                    + (((int) (b1 * (1 - colorFraction) + b2 * colorFraction)));
        }
        img.getRaster().setDataElements(0, 0, max, 1, array);
    }

    @Override
    public Dimension getMinimumSize(JComponent s) {
        Dimension d = super.getMinimumSize(s);
        if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
            d.height += 2;
        } else {
            d.width += 2;
        }
        return d;
    }

    @Override
    public Dimension getPreferredSize(JComponent s) {
        Dimension d = super.getPreferredSize(s);
        if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
            d.height += 2;
        } else {
            d.width += 2;
        }
        return d;
    }

    @Override
    protected Rectangle calculateTrackRect() {
        int w = slider.getWidth();
        int h = slider.getHeight();

        Rectangle r = new Rectangle();

        if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
            r.x = TRIANGLE_SIZE;
            r.y = 3;
            r.height = h - TRIANGLE_SIZE - r.y;
            r.width = w - 2 * TRIANGLE_SIZE;
            if (r.width > img.getWidth()) {
                r.width = img.getWidth();
                r.x = (w - 2 * TRIANGLE_SIZE) / 2 - r.width / 2;
            }
            if (r.height > 2 * DEPTH) {
                r.height = 2 * DEPTH;
                r.y = (h - TRIANGLE_SIZE) / 2 - r.height / 2;
            }
        } else {
            r.x = 3;
            r.y = TRIANGLE_SIZE;
            r.width = w - TRIANGLE_SIZE - r.x;
            r.height = h - 2 * TRIANGLE_SIZE;
            if (r.height > img.getWidth()) {
                r.height = img.getWidth();
                r.y = (h - 2 * TRIANGLE_SIZE) / 2 - r.height / 2;
            }
            if (r.width > 2 * DEPTH) {
                r.width = 2 * DEPTH;
                r.x = (w - TRIANGLE_SIZE) / 2 - r.width / 2;
            }
        }
        return r;
    }

    @Override
    protected void calculateGeometry() {
        super.calculateGeometry();
        calculateImage();
    }
    static TexturePaint checkerPaint;

    private static void createCheckerPaint() {
        int k = 4;
        BufferedImage bi = new BufferedImage(2 * k, 2 * k, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = bi.createGraphics();
        g.setColor(Color.white);
        g.fillRect(0, 0, 2 * k, 2 * k);
        g.setColor(Color.lightGray);
        g.fillRect(0, 0, k, k);
        g.fillRect(k, k, k, k);
        checkerPaint = new TexturePaint(bi, new Rectangle(0, 0, bi.getWidth(), bi.getHeight()));
    }

    /**
     * The "frame" includes the trackRect and possible some extra padding. For
     * example, the frame might be the rounded rectangle enclosing the track (if
     * rounded rectangles are turned on)
     *
     * @return
     */
    private Shape getFrame() {

        if (getProperty(slider, "GradientSlider.useBevel", "false").equals("true")) {
            return trackRect;
        }

        if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
            int curve = Math.min(TRIANGLE_SIZE - 2, trackRect.height / 2);
            return new RoundRectangle2D.Float(
                    trackRect.x - curve, trackRect.y,
                    trackRect.width + 2 * curve, trackRect.height,
                    curve * 2, curve * 2);
        } else {
            int curve = Math.min(TRIANGLE_SIZE - 2, trackRect.width / 2);
            return new RoundRectangle2D.Float(
                    trackRect.x, trackRect.y - curve,
                    trackRect.width, trackRect.height + 2 * curve,
                    curve * 2, curve * 2);
        }
    }

    @Override
    protected void paintTrack(Graphics2D g) {
        Composite oldComposite = g.getComposite();
        float alpha = slider.isEnabled() ? 1 : .5f;
        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));

        Shape frame = getFrame();

        boolean alwaysOpaque = getProperty(slider, "GradientSlider.showTranslucency", "true").equals("false");

        if (alwaysOpaque == false) {
            if (checkerPaint == null) {
                createCheckerPaint();
            }
            g.setPaint(checkerPaint);
            g.fill(frame);
        }

        TexturePaint tp = new TexturePaint(img, new Rectangle(trackRect.x, 0, img.getWidth(), 1));
        g.setPaint(tp);
        AffineTransform oldTransform = null;

        oldTransform = g.getTransform();
        AffineTransform transform = new AffineTransform();
        if (slider.getOrientation() == GradientSlider.VERTICAL) {
            if (slider.isInverted()) {
                transform.rotate(Math.PI / 2, trackRect.x, trackRect.y);
            } else {
                transform.rotate(-Math.PI / 2, trackRect.x, trackRect.y + trackRect.height);
            }
        } else if (slider.isInverted()) {
            //flip horizontal:
            double x1 = trackRect.x;
            double x2 = trackRect.x + trackRect.width;
                //m00*x1+m02 = x2
            //m00*x2+m02 = x1
            double m00 = (x2 - x1) / (x1 - x2);
            double m02 = x1 - m00 * x2;
            transform.setTransform(m00, 0, 0, 1, m02, 0);
        } else {
            //no transform necessary
        }

        g.transform(transform);

        try {
            g.fill(transform.createInverse().createTransformedShape(trackRect));
        } catch (NoninvertibleTransformException e) {
            //this won't happen; unless a width/height
            //is zero somewhere, in which case we have nothing to paint anyway.
        }
        if (oldTransform != null) {
            g.setTransform(oldTransform);
        }
        if (getProperty(slider, "GradientSlider.useBevel", "false").equals("true")) {
            PaintUtils.drawBevel(g, trackRect);
        } else {
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            Shape oldClip = g.getClip();
            int first, last;
            Color[] colors = ((GradientSlider) slider).getColors();
            float[] f = slider.getThumbPositions();
            if ((slider.isInverted() == false && slider.getOrientation() == GradientSlider.HORIZONTAL)
                    || (slider.isInverted() == true && slider.getOrientation() == GradientSlider.VERTICAL)) {
                first = 0;
                last = colors.length - 1;
                while (f[first] < 0) {
                    first++;
                }
                while (f[last] > 1) {
                    last--;
                }
            } else {
                last = 0;
                first = colors.length - 1;
                while (f[last] < 0) {
                    last++;
                }
                while (f[first] > 1) {
                    first--;
                }
            }
            if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
                g.clip(frame);
                g.setColor(colors[first]);
                g.fillRect(0, 0, trackRect.x, slider.getHeight());
                g.setColor(colors[last]);
                g.fillRect(trackRect.x + trackRect.width, 0,
                        slider.getWidth() - (trackRect.x + trackRect.width), slider.getHeight());
            } else {
                g.clip(frame);
                g.setColor(colors[first]);
                g.fillRect(0, 0, slider.getWidth(), trackRect.y);
                g.setColor(colors[last]);
                g.fillRect(0, trackRect.y + trackRect.height,
                        slider.getWidth(), slider.getHeight() - (trackRect.y + trackRect.height));
            }
            g.setStroke(new BasicStroke(1));
            g.setClip(oldClip);
            g.setColor(new Color(0, 0, 0, 130));
            g.draw(frame);

            g.setColor(new Color(0, 0, 0, 130));
        }

        if (slider.isPaintTicks()) {
            paintTick(g, .25f, 2);
            paintTick(g, .5f, 2);
            paintTick(g, .75f, 2);
            paintTick(g, 0f, 2);
            paintTick(g, 1f, 2);
        }
        g.setComposite(oldComposite);
    }

    protected void paintTick(Graphics2D g, float f, int d) {
        if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
            int x = (int) (trackRect.x + trackRect.width * f + .5f);
            int y = trackRect.y + trackRect.height;
            g.drawLine(x, y, x, y + d);
            y = trackRect.y;
            g.drawLine(x, y, x, y - d);
        } else {
            int y = (int) (trackRect.y + trackRect.height * f + .5f);
            int x = trackRect.x + trackRect.width;
            g.drawLine(x, y, x + d, y);
            x = trackRect.x;
            g.drawLine(x, y, x - d, y);
        }
    }

    @Override
    protected void paintFocus(Graphics2D g) {
    }
    static GeneralPath hTriangle = null;
    static GeneralPath vTriangle = null;

    @Override
    protected void paintThumbs(Graphics2D g) {
        if (slider.isEnabled() == false) {
            return;
        }

        if (hTriangle == null) {
            hTriangle = new GeneralPath();
            hTriangle.moveTo(0, 0);
            hTriangle.lineTo(TRIANGLE_SIZE, TRIANGLE_SIZE);
            hTriangle.lineTo(-TRIANGLE_SIZE, TRIANGLE_SIZE);
            hTriangle.lineTo(0, 0);
            hTriangle.closePath();
            vTriangle = new GeneralPath();
            vTriangle.moveTo(0, 0);
            vTriangle.lineTo(TRIANGLE_SIZE, TRIANGLE_SIZE);
            vTriangle.lineTo(TRIANGLE_SIZE, -TRIANGLE_SIZE);
            vTriangle.lineTo(0, 0);
            vTriangle.closePath();
        }

        AffineTransform t = new AffineTransform();
        int dx = trackRect.x + trackRect.width;
        int dy = trackRect.y + trackRect.height;
        dy -= trackRect.height / 6;
        dx -= trackRect.width / 6;
        int selected = slider.getSelectedThumb(false);
        float[] f = slider.getThumbPositions();
        int orientation = slider.getOrientation();
        Shape shape;

        Composite oldComposite = g.getComposite();

        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                indication));

        for (int a = 0; a < thumbPositions.length; a++) {
            if (f[a] >= 0 && f[a] <= 1 && a != selected) {
                if (orientation == GradientSlider.HORIZONTAL) {
                    dx = thumbPositions[a];
                    shape = hTriangle;
                } else {
                    dy = thumbPositions[a];
                    shape = vTriangle;
                }
                t.setToTranslation(dx, dy);
                g.transform(t);

                float brightness = Math.max(0, thumbIndications[a] * .6f);

                g.setColor(new Color((int) (255 * brightness),
                        (int) (255 * brightness),
                        (int) (255 * brightness)));
                g.fill(shape);
                g.translate(-.5f, -.5f);

                g.setColor(new Color(255, 255, 255));

                g.draw(shape);
                g.translate(.5f, .5f);

                t.setToTranslation(-dx, -dy);
                g.transform(t);
            }
        }

        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                indication));

        if (selected != -1 && f[selected] >= 0 && f[selected] <= 1) {
            if (orientation == GradientSlider.HORIZONTAL) {
                dx = thumbPositions[selected];
                shape = hTriangle;
            } else {
                dy = thumbPositions[selected];
                shape = vTriangle;
            }
            t.setToTranslation(dx, dy);
            g.transform(t);

            g.setColor(new Color(255, 255, 255));
            g.fill(shape);
            g.translate(-.5f, -.5f);

            g.setColor(new Color(0, 0, 0));

            g.draw(shape);
            g.translate(.5f, .5f);

            t.setToTranslation(-dx, -dy);
            g.transform(t);
        }

        g.setComposite(oldComposite);
    }
}
