/*
 * Copyright (c) 2007, 2011, 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 org.graalvm.visualvm.core.ui.components;

import java.awt.AWTException;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.ImageCapabilities;
import java.awt.Insets;
import java.awt.image.VolatileImage;
import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleRole;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JComponent.AccessibleJComponent;
import javax.swing.border.BevelBorder;


/**
 * Graphical component for fall-off level indicator
 * @author Jaroslav Bachorik
 */
public class LevelIndicator extends JComponent {
    //~ Static fields/initializers -----------------------------------------------------------------------------------------------

    private static final Insets NULL_INSETS = new Insets(0, 0, 0, 0);
    private static final Dimension PREFERRED_SIZE = new Dimension(40, 20);

    //~ Instance fields ----------------------------------------------------------------------------------------------------------

    private Color maximumColor = Color.RED;
    private Color minimumColor = Color.GREEN;
    private Color peakColor = null;
    private Dimension canvasDimension = null;
    private Insets canvasInsets = NULL_INSETS;
    private boolean autoRepaint = true;
    private boolean followPeak;
    private int peakMarkSize = 8; // peak mark size in pixels
    private long max = 0;
    private long min = 0;
    private long peak = Long.MIN_VALUE;
    private long val = min;

    //~ Constructors -------------------------------------------------------------------------------------------------------------

    public LevelIndicator() {
        setPreferredSize(PREFERRED_SIZE);
        setMinimumSize(PREFERRED_SIZE);
        setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
    }

    //~ Methods ------------------------------------------------------------------------------------------------------------------

    /**
     * Sets the auto repainting of the component on/off
     * @param autoRepaint The auto repainting on/off
     */
    public void setAutoRepaint(boolean autoRepaint) {
        this.autoRepaint = autoRepaint;
    }

    /**
     * Status of the component auto repainting
     * @return Returns the status of the component auto repainting
     */
    public boolean isAutoRepaint() {
        return autoRepaint;
    }

    /**
     * When set the component will mark the so-far highest value
     * @param markPeaks Set peak following on/off
     */
    public void setFollowPeak(boolean markPeaks) {
        this.followPeak = markPeaks;
    }

    /**
     * Status of the follow-peak
     * @return Returns the status of the follow-peak property
     */
    public boolean isFollowPeak() {
        return followPeak;
    }

    /**
     * Sets the component maximum value
     * Will repaint the component if {@linkplain #isAutoRepaint() } is on
     * @param max The maximum value the component would display
     */
    public void setMaximum(long max) {
        this.max = max;

        if (autoRepaint) {
            repaint();
        }
    }

    /**
     * Returns the component maximum value
     * @return Returns the component maximum value
     */
    public long getMaximum() {
        return max;
    }

    /**
     * With this property you can control the {@linkplain Color} of the highest value
     * Will repaint the component if {@linkplain #isAutoRepaint() } is on
     * @param maximumColor The {@linkplain Color} to be used for the highest values
     */
    public void setMaximumColor(Color maximumColor) {
        this.maximumColor = maximumColor;
    }

    /**
     * Returns the {@linkplain Color} of the highest values
     * @return Returns the {@linkplain Color} of the highest values
     */
    public Color getMaximumColor() {
        return maximumColor;
    }

    /**
     * Sets the minimum displayable value of the component
     * Will repaint the component if {@linkplain #isAutoRepaint() } is on
     * @param min The minimal value that should be displayed
     */
    public void setMinimum(long min) {
        this.min = min;

        if (autoRepaint) {
            repaint();
        }
    }

    /**
     * Returns the component minimal value
     * @return Returns the component minimal value
     */
    public long getMinimum() {
        return min;
    }

    /**
     * Sets the {@linkplain Color} for the minimal value
     * Will repaint the component if {@linkplain #isAutoRepaint() } is on
     * @param minimumColor The {@linkplain Color} to use for the minimal value
     */
    public void setMinimumColor(Color minimumColor) {
        this.minimumColor = minimumColor;
    }

    /**
     * Returns the {@linkplain Color} used for the minimal value
     * @return Returns the {@linkplain Color} used for the minimal value
     */
    public Color getMinimumColor() {
        return minimumColor;
    }

    /**
     * Manually sets the actual value as the peak
     * Will repaint the component if {@linkplain #isAutoRepaint() } is on
     */
    public void setPeak() {
        peak = val;

        if (autoRepaint) {
            repaint();
        }
    }

    /**
     * Manually sets the peak
     * Will repaint the component if {@linkplain #isAutoRepaint() } is on
     * @param value The value to set as the peak
     */
    public void setPeak(long value) {
        if (peak <= max) {
            peak = value;

            if (autoRepaint) {
                repaint();
            }
        }
    }

    /**
     * Returns the current peak value
     * @return Returns the current peak value
     */
    public long getPeak() {
        return peak;
    }

    /**
     * Sets the peak mark size in pixels
     * The peak mark is rendered in the indicator at the place of peak value
     * @param peakMarkSize The peak mark size in pixels
     */
    public void setPeakMarkSize(int peakMarkSize) {
        this.peakMarkSize = peakMarkSize;

        if (autoRepaint) {
            repaint();
        }
    }

    /**
     * Returns the peak mark size in pixels
     * The peak mark is rendered in the indicator at the place of peak value
     * @return Returns the peak mark size in pixels
     */
    public int getPeakMarkSize() {
        return peakMarkSize;
    }

    /**
     * Sets the current value of the inidicator
     * @param value The current value
     */
    public void setValue(long value) {
        val = Math.max(Math.min(value, max), 0);

        if (followPeak && (val > peak)) {
            peak = this.val;
        }

        if (autoRepaint) {
            repaint();
        }
    }

    /**
     * Returns the current value
     * @return Returns the current value
     */
    public long getValue() {
        return val;
    }

    @Override
    public void doLayout() {
        super.doLayout();

        canvasInsets = getInsets();

        canvasDimension = new Dimension(getBounds().width - (canvasInsets.left + canvasInsets.right),
                                        getBounds().height - (canvasInsets.top + canvasInsets.bottom));
    }

    @Override
    public void paintComponent(Graphics g) {
        if (canvasDimension == null) {
            return;
        }

        if ((canvasDimension.getHeight() < 0) || (canvasDimension.getWidth() < 0)) {
            return; // no rendering if dimensions are negative
        }

        try {
            VolatileImage img = createVolatileImage(getBounds().width - (canvasInsets.left + canvasInsets.right),
                                                    getBounds().height - (canvasInsets.top + canvasInsets.bottom),
                                                    new ImageCapabilities(true));
            Graphics2D gr = img.createGraphics();
            renderLevel(gr, img);
            renderPeak(gr);

            gr.dispose();
            g.drawImage(img, canvasInsets.left, canvasInsets.top, this);

            //
            //            if (getBorder() != null) {
            //                getBorder().paintBorder(this, g, 0, 0, getBounds().width, getBounds().height);
            //            }
        } catch (AWTException e) {
        }
    }

    /**
     * Cleans the peak mark
     * Will repaint the component
     */
    public void unsetPeak() {
        peak = Integer.MIN_VALUE;
        repaint();
    }


    /**
     * Gets a dummy AccessibleContext associated with this LevelIndicator.
     *
     * @return a dummy AccessibleContext associated with this LevelIndicator
     */
    public AccessibleContext getAccessibleContext() {
        if (accessibleContext == null) {
            accessibleContext = new AccessibleLevelIndicator();
        }
        return accessibleContext;
    }


    private Color getColorAt(VolatileImage img, int x, int y) {
        if ((x <= 0) || (y < 0) || (x > img.getWidth()) || (y > img.getHeight())) {
            return null;
        }

        int RGB = img.getSnapshot().getRGB(x - 1, 0);
        int red = (RGB & 0x00ff0000) >> 16;
        int green = (RGB & 0x0000ff00) >> 8;
        int blue = RGB & 0x000000ff;

        // and the Java Color is ...
        return new Color(red, green, blue);
    }

    private int getPosition(long value) {
        float ratio = (float) value / (float) (max - min);

        return Math.round((float) (ratio * canvasDimension.getWidth()));
    }

    private void renderLevel(Graphics2D gr, VolatileImage img) {
        gr.setPaint(new GradientPaint(0, 0, minimumColor, canvasDimension.width, 0, maximumColor));
        gr.fillRect(0, 0, canvasDimension.width, canvasDimension.height);

        if (peak > Integer.MIN_VALUE) {
            peakColor = getColorAt(img, getPosition(peak), 0);
        }

        int position = getPosition(val);

        gr.setPaint(getBackground());
        gr.fillRect(position, 0, canvasDimension.width - position + 1, canvasDimension.height);
    }

    private void renderPeak(Graphics2D gr) {
        if (peakColor == null) {
            return;
        }

        int position = getPosition(peak);

        int decrement = 0;
        int left = 0;
        int right = 0;

        do {
            left = Math.round(position - ((peakMarkSize - decrement) / 2f));
            right = Math.round(left + ((peakMarkSize - decrement) / 2f));

            if (left < 0) {
                right += Math.abs(left);
                left = 0;
            }

            if (right > canvasDimension.getWidth()) {
                left -= (right - canvasDimension.getWidth());
                right = (int) canvasDimension.getWidth();
            }

            decrement++;
        } while (((left < 0) || (right > canvasDimension.getWidth())) && (left != right));

        gr.setPaint(peakColor);
        gr.fillRect(left, 0, right - left + 1, canvasDimension.height);
    }


    /**
     * Dummy AccessibleContext implementation for the LevelIndicator.
     */
    private class AccessibleLevelIndicator extends AccessibleJComponent {

        /**
         * Get the role of this object.
         *
         * @return an instance of AccessibleRole describing the role of the
         * object
         */
        public AccessibleRole getAccessibleRole() {
            return AccessibleRole.SWING_COMPONENT;
        }
    }
}
