/*
 * $Id: DropShadowBorder.java 4147 2012-02-01 17:13:24Z kschaefe $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jdesktop.swingx.border;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import javax.swing.border.Border;

import org.jdesktop.beans.JavaBean;
import org.jdesktop.swingx.util.GraphicsUtilities;

/**
 * Implements a DropShadow for components. In general, the DropShadowBorder will
 * work with any rectangular components that do not have a default border
 * installed as part of the look and feel, or otherwise. For example,
 * DropShadowBorder works wonderfully with JPanel, but horribly with JComboBox.
 * <p>
 * Note: {@code DropShadowBorder} should usually be added to non-opaque
 * components, otherwise the background is likely to bleed through.</p>
 * <p>Note: Since generating drop shadows is relatively expensive operation, 
 * {@code DropShadowBorder} keeps internal static cache that allows sharing 
 * same border for multiple re-rendering and between different instances of the 
 * class. Since this cache is shared at class level and never reset, it might 
 * bleed your app memory in case you tend to create many different borders 
 * rapidly.</p>
 * @author rbair
 */
@JavaBean
public class DropShadowBorder implements Border, Serializable {
    /**
     * 
     */
    private static final long serialVersionUID = 715287754750604058L;

    private static enum Position {TOP, TOP_LEFT, LEFT, BOTTOM_LEFT,
                    BOTTOM, BOTTOM_RIGHT, RIGHT, TOP_RIGHT}
                    
    private static final Map<Double,Map<Position,BufferedImage>> CACHE 
            = new HashMap<Double,Map<Position,BufferedImage>>();
                        
    private Color shadowColor;
    private int shadowSize;
    private float shadowOpacity;
    private int cornerSize;
    private boolean showTopShadow;
    private boolean showLeftShadow;
    private boolean showBottomShadow;
    private boolean showRightShadow;
    
    public DropShadowBorder() {
        this(Color.BLACK, 5);
    }
    
    public DropShadowBorder(Color shadowColor, int shadowSize) {
        this(shadowColor, shadowSize, .5f, 12, false, false, true, true);
    }
    
    public DropShadowBorder(boolean showLeftShadow) {
        this(Color.BLACK, 5, .5f, 12, false, showLeftShadow, true, true);
    }
    
    public DropShadowBorder(Color shadowColor, int shadowSize,
            float shadowOpacity, int cornerSize, boolean showTopShadow,
            boolean showLeftShadow, boolean showBottomShadow, boolean showRightShadow) {
        this.shadowColor = shadowColor;
        this.shadowSize = shadowSize;
        this.shadowOpacity = shadowOpacity;
        this.cornerSize = cornerSize;
        this.showTopShadow = showTopShadow;
        this.showLeftShadow = showLeftShadow;
        this.showBottomShadow = showBottomShadow;
        this.showRightShadow = showRightShadow;
    }
    
    /**
     * {@inheritDoc}
     */
    @Override
    public void paintBorder(Component c, Graphics graphics, int x, int y, int width, int height) {
        /*
         * 1) Get images for this border
         * 2) Paint the images for each side of the border that should be painted
         */
        Map<Position,BufferedImage> images = getImages((Graphics2D)graphics);
        
        Graphics2D g2 = (Graphics2D)graphics.create();
        
        try {
            //The location and size of the shadows depends on which shadows are being
            //drawn. For instance, if the left & bottom shadows are being drawn, then
            //the left shadow extends all the way down to the corner, a corner is drawn,
            //and then the bottom shadow begins at the corner. If, however, only the
            //bottom shadow is drawn, then the bottom-left corner is drawn to the
            //right of the corner, and the bottom shadow is somewhat shorter than before.
            
            int shadowOffset = 2; //the distance between the shadow and the edge
            
            Point topLeftShadowPoint = null;
            if (showLeftShadow || showTopShadow) {
                topLeftShadowPoint = new Point();
                if (showLeftShadow && !showTopShadow) {
                    topLeftShadowPoint.setLocation(x, y + shadowOffset);
                } else if (showLeftShadow && showTopShadow) {
                    topLeftShadowPoint.setLocation(x, y);
                } else if (!showLeftShadow && showTopShadow) {
                    topLeftShadowPoint.setLocation(x + shadowSize, y);
                }
            }
      
            Point bottomLeftShadowPoint = null;
            if (showLeftShadow || showBottomShadow) {
                bottomLeftShadowPoint = new Point();
                if (showLeftShadow && !showBottomShadow) {
                    bottomLeftShadowPoint.setLocation(x, y + height - shadowSize - shadowSize);
                } else if (showLeftShadow && showBottomShadow) {
                    bottomLeftShadowPoint.setLocation(x, y + height - shadowSize);
                } else if (!showLeftShadow && showBottomShadow) {
                    bottomLeftShadowPoint.setLocation(x + shadowSize, y + height - shadowSize);
                }
            }
            
            Point bottomRightShadowPoint = null;
            if (showRightShadow || showBottomShadow) {
                bottomRightShadowPoint = new Point();
                if (showRightShadow && !showBottomShadow) {
                    bottomRightShadowPoint.setLocation(x + width - shadowSize, y + height - shadowSize - shadowSize);
                } else if (showRightShadow && showBottomShadow) {
                    bottomRightShadowPoint.setLocation(x + width - shadowSize, y + height - shadowSize);
                } else if (!showRightShadow && showBottomShadow) {
                    bottomRightShadowPoint.setLocation(x + width - shadowSize - shadowSize, y + height - shadowSize);
                }
            }
            
            Point topRightShadowPoint = null;
            if (showRightShadow || showTopShadow) {
                topRightShadowPoint = new Point();
                if (showRightShadow && !showTopShadow) {
                    topRightShadowPoint.setLocation(x + width - shadowSize, y + shadowOffset);
                } else if (showRightShadow && showTopShadow) {
                    topRightShadowPoint.setLocation(x + width - shadowSize, y);
                } else if (!showRightShadow && showTopShadow) {
                    topRightShadowPoint.setLocation(x + width - shadowSize - shadowSize, y);
                }
            }
     
            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g2.setRenderingHint(RenderingHints.KEY_RENDERING,
                                RenderingHints.VALUE_RENDER_SPEED);
            
            if (showLeftShadow) {
                Rectangle leftShadowRect =
                    new Rectangle(x,
                                  topLeftShadowPoint.y + shadowSize,
                                  shadowSize,
                                  bottomLeftShadowPoint.y - topLeftShadowPoint.y - shadowSize);
                g2.drawImage(images.get(Position.LEFT),
                             leftShadowRect.x, leftShadowRect.y,
                             leftShadowRect.width, leftShadowRect.height, null);
            }
    
            if (showBottomShadow) {
                Rectangle bottomShadowRect =
                    new Rectangle(bottomLeftShadowPoint.x + shadowSize,
                                  y + height - shadowSize,
                                  bottomRightShadowPoint.x - bottomLeftShadowPoint.x - shadowSize,
                                  shadowSize);
                g2.drawImage(images.get(Position.BOTTOM),
                             bottomShadowRect.x, bottomShadowRect.y,
                             bottomShadowRect.width, bottomShadowRect.height, null);
            }
            
            if (showRightShadow) {
                Rectangle rightShadowRect =
                    new Rectangle(x + width - shadowSize,
                                  topRightShadowPoint.y + shadowSize,
                                  shadowSize,
                                  bottomRightShadowPoint.y - topRightShadowPoint.y - shadowSize);
                g2.drawImage(images.get(Position.RIGHT),
                             rightShadowRect.x, rightShadowRect.y,
                             rightShadowRect.width, rightShadowRect.height, null);
            }
            
            if (showTopShadow) {
                Rectangle topShadowRect =
                    new Rectangle(topLeftShadowPoint.x + shadowSize,
                                  y,
                                  topRightShadowPoint.x - topLeftShadowPoint.x - shadowSize,
                                  shadowSize);
                g2.drawImage(images.get(Position.TOP),
                             topShadowRect.x, topShadowRect.y,
                             topShadowRect.width, topShadowRect.height, null);
            }
            
            if ((showLeftShadow || showTopShadow) && topLeftShadowPoint != null) {
                g2.drawImage(images.get(Position.TOP_LEFT),
                             topLeftShadowPoint.x, topLeftShadowPoint.y, null);
            }
            if ((showLeftShadow || showBottomShadow) && bottomLeftShadowPoint != null) {
                g2.drawImage(images.get(Position.BOTTOM_LEFT),
                             bottomLeftShadowPoint.x, bottomLeftShadowPoint.y, null);
            }
            if ((showRightShadow || showBottomShadow) && bottomRightShadowPoint != null) {
                g2.drawImage(images.get(Position.BOTTOM_RIGHT),
                             bottomRightShadowPoint.x, bottomRightShadowPoint.y, null);
            }
            if ((showRightShadow || showTopShadow) && topRightShadowPoint != null) {
                g2.drawImage(images.get(Position.TOP_RIGHT),
                             topRightShadowPoint.x, topRightShadowPoint.y, null);
            }
        } finally {
            g2.dispose();
        }
    }
    
    private Map<Position,BufferedImage> getImages(Graphics2D g2) {
        //first, check to see if an image for this size has already been rendered
        //if so, use the cache. Else, draw and save
        Map<Position,BufferedImage> images = CACHE.get(shadowSize + (shadowColor.hashCode() * .3) + (shadowOpacity * .12));//TODO do a real hash
        if (images == null) {
            images = new HashMap<Position,BufferedImage>();

            /*
             * To draw a drop shadow, I have to:
             *  1) Create a rounded rectangle
             *  2) Create a BufferedImage to draw the rounded rect in
             *  3) Translate the graphics for the image, so that the rectangle
             *     is centered in the drawn space. The border around the rectangle
             *     needs to be shadowWidth wide, so that there is space for the
             *     shadow to be drawn.
             *  4) Draw the rounded rect as shadowColor, with an opacity of shadowOpacity
             *  5) Create the BLUR_KERNEL
             *  6) Blur the image
             *  7) copy off the corners, sides, etc into images to be used for
             *     drawing the Border
             */
            int rectWidth = cornerSize + 1;
            RoundRectangle2D rect = new RoundRectangle2D.Double(0, 0, rectWidth, rectWidth, cornerSize, cornerSize);
            int imageWidth = rectWidth + shadowSize * 2;
            BufferedImage image = GraphicsUtilities.createCompatibleTranslucentImage(imageWidth, imageWidth);
            Graphics2D buffer = (Graphics2D)image.getGraphics();
            
            try {
                buffer.setPaint(new Color(shadowColor.getRed(), shadowColor.getGreen(),
                        shadowColor.getBlue(), (int)(shadowOpacity * 255)));
//                buffer.setColor(new Color(0.0f, 0.0f, 0.0f, shadowOpacity));
                buffer.translate(shadowSize, shadowSize);
                buffer.fill(rect);
            } finally {
                buffer.dispose();
            }
            
            float blurry = 1.0f / (shadowSize * shadowSize);
            float[] blurKernel = new float[shadowSize * shadowSize];
            for (int i=0; i<blurKernel.length; i++) {
                blurKernel[i] = blurry;
            }
            ConvolveOp blur = new ConvolveOp(new Kernel(shadowSize, shadowSize, blurKernel));
            BufferedImage targetImage = GraphicsUtilities.createCompatibleTranslucentImage(imageWidth, imageWidth);
            Graphics2D g2d = (Graphics2D)targetImage.getGraphics();
            try {
                g2d.drawImage(image, blur, -(shadowSize/2), -(shadowSize/2));
            } finally {
        	g2d.dispose();
            }

            int x = 1;
            int y = 1;
            int w = shadowSize;
            int h = shadowSize;
            images.put(Position.TOP_LEFT, getSubImage(targetImage, x, y, w, h));
            x = 1;
            y = h;
            w = shadowSize;
            h = 1;
            images.put(Position.LEFT, getSubImage(targetImage, x, y, w, h));
            x = 1;
            y = rectWidth;
            w = shadowSize;
            h = shadowSize;
            images.put(Position.BOTTOM_LEFT, getSubImage(targetImage, x, y, w, h));
            x = cornerSize + 1;
            y = rectWidth;
            w = 1;
            h = shadowSize;
            images.put(Position.BOTTOM, getSubImage(targetImage, x, y, w, h));
            x = rectWidth;
            y = x;
            w = shadowSize;
            h = shadowSize;
            images.put(Position.BOTTOM_RIGHT, getSubImage(targetImage, x, y, w, h));
            x = rectWidth;
            y = cornerSize + 1;
            w = shadowSize;
            h = 1;
            images.put(Position.RIGHT, getSubImage(targetImage, x, y, w, h));
            x = rectWidth;
            y = 1;
            w = shadowSize;
            h = shadowSize;
            images.put(Position.TOP_RIGHT, getSubImage(targetImage, x, y, w, h));
            x = shadowSize;
            y = 1;
            w = 1;
            h = shadowSize;
            images.put(Position.TOP, getSubImage(targetImage, x, y, w, h));

            image.flush();
            CACHE.put(shadowSize + (shadowColor.hashCode() * .3) + (shadowOpacity * .12), images); //TODO do a real hash
        }
        return images;
    }
    
    /**
     * Returns a new BufferedImage that represents a subregion of the given
     * BufferedImage.  (Note that this method does not use
     * BufferedImage.getSubimage(), which will defeat image acceleration
     * strategies on later JDKs.)
     */
    private BufferedImage getSubImage(BufferedImage img,
                                      int x, int y, int w, int h) {
        BufferedImage ret = GraphicsUtilities.createCompatibleTranslucentImage(w, h);
        Graphics2D g2 = ret.createGraphics();
        
        try {
            g2.drawImage(img,
                         0, 0, w, h,
                         x, y, x+w, y+h,
                         null);
        } finally {
            g2.dispose();
        }
        
        return ret;
    }
    
    /**
     * @inheritDoc
     */
    @Override
    public Insets getBorderInsets(Component c) {
        int top = showTopShadow ? shadowSize : 0;
        int left = showLeftShadow ? shadowSize : 0;
        int bottom = showBottomShadow ? shadowSize : 0;
        int right = showRightShadow ? shadowSize : 0;
        return new Insets(top, left, bottom, right);
    }
    
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isBorderOpaque() {
        return false;
    }
    
    public boolean isShowTopShadow() {
        return showTopShadow;
    }
    
    public boolean isShowLeftShadow() {
        return showLeftShadow;
    }
    
    public boolean isShowRightShadow() {
        return showRightShadow;
    }
    
    public boolean isShowBottomShadow() {
        return showBottomShadow;
    }
    
    public int getShadowSize() {
        return shadowSize;
    }
    
    public Color getShadowColor() {
        return shadowColor;
    }
    
    public float getShadowOpacity() {
        return shadowOpacity;
    }
    
    public int getCornerSize() {
        return cornerSize;
    }
    
    public void setShadowColor(Color shadowColor) {
        this.shadowColor = shadowColor;
    }

    public void setShadowSize(int shadowSize) {
        this.shadowSize = shadowSize;
    }

    public void setShadowOpacity(float shadowOpacity) {
        this.shadowOpacity = shadowOpacity;
    }

    public void setCornerSize(int cornerSize) {
        this.cornerSize = cornerSize;
    }

    public void setShowTopShadow(boolean showTopShadow) {
        this.showTopShadow = showTopShadow;
    }

    public void setShowLeftShadow(boolean showLeftShadow) {
        this.showLeftShadow = showLeftShadow;
    }

    public void setShowBottomShadow(boolean showBottomShadow) {
        this.showBottomShadow = showBottomShadow;
    }

    public void setShowRightShadow(boolean showRightShadow) {
        this.showRightShadow = showRightShadow;
    }
}