/*
 * Copyright 2020 FormDev Software GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.formdev.flatlaf.ui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.RadialGradientPaint;
import java.awt.image.BufferedImage;
import com.formdev.flatlaf.util.HiDPIUtils;
import com.formdev.flatlaf.util.UIScale;

/**
 * Paints a drop shadow border around the component.
 * Supports 1-sided, 2-side, 3-sided or 4-sided drop shadows.
 * <p>
 * The shadow insets allow specifying drop shadow thickness for each side.
 * A zero or negative value hides the drop shadow on that side.
 * A negative value can be used to indent the drop shadow on corners.
 * E.g. -4 on left indents drop shadow at top-left and bottom-left corners by 4 pixels.
 *
 * @author Karl Tauber
 */
public class FlatDropShadowBorder
	extends FlatEmptyBorder
{
	private final Color shadowColor;
	private final Insets shadowInsets;
	private final float shadowOpacity;

	private final int shadowSize;
	private Image shadowImage;
	private Color lastShadowColor;
	private double lastSystemScaleFactor;
	private float lastUserScaleFactor;

	public FlatDropShadowBorder() {
		this( null );
	}

	public FlatDropShadowBorder( Color shadowColor ) {
		this( shadowColor, 4, 0.5f );
	}

	public FlatDropShadowBorder( Color shadowColor, int shadowSize, float shadowOpacity ) {
		this( shadowColor, new Insets( -shadowSize, -shadowSize, shadowSize, shadowSize ), shadowOpacity );
	}

	public FlatDropShadowBorder( Color shadowColor, Insets shadowInsets, float shadowOpacity ) {
		super( Math.max( shadowInsets.top, 0 ), Math.max( shadowInsets.left, 0 ),
			Math.max( shadowInsets.bottom, 0 ), Math.max( shadowInsets.right, 0 ) );
		this.shadowColor = shadowColor;
		this.shadowInsets = shadowInsets;
		this.shadowOpacity = shadowOpacity;

		shadowSize = Math.max(
			Math.max( shadowInsets.left, shadowInsets.right ),
			Math.max( shadowInsets.top, shadowInsets.bottom ) );
	}

	@Override
	public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) {
		if( shadowSize <= 0 )
			return;

		HiDPIUtils.paintAtScale1x( (Graphics2D) g, x, y, width, height, this::paintImpl );
	}

	private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) {
		Color shadowColor = (this.shadowColor != null) ? this.shadowColor : g.getColor();
		int shadowSize = scale( this.shadowSize, scaleFactor );

		// create and cache shadow image
		float userScaleFactor = UIScale.getUserScaleFactor();
		if( shadowImage == null ||
			!shadowColor.equals( lastShadowColor ) ||
			lastSystemScaleFactor != scaleFactor ||
			lastUserScaleFactor != userScaleFactor )
		{
			shadowImage = createShadowImage( shadowColor, shadowSize, shadowOpacity,
				(float) (scaleFactor * userScaleFactor) );
			lastShadowColor = shadowColor;
			lastSystemScaleFactor = scaleFactor;
			lastUserScaleFactor = userScaleFactor;
		}

/*debug
		int m = shadowImage.getWidth( null );
		Color oldColor = g.getColor();
		g.setColor( Color.lightGray );
		g.drawRect( x - m - 1, y - m - 1, m + 1, m + 1 );
		g.setColor( Color.white );
		g.fillRect( x - m, y - m, m, m );
		g.drawImage( shadowImage, x - m, y - m, null );
		g.setColor( oldColor );
debug*/

		int left = scale( shadowInsets.left, scaleFactor );
		int right = scale( shadowInsets.right, scaleFactor );
		int top = scale( shadowInsets.top, scaleFactor );
		int bottom = scale( shadowInsets.bottom, scaleFactor );

		// shadow outer coordinates
		int x1o = x - Math.min( left, 0 );
		int y1o = y - Math.min( top, 0 );
		int x2o = x + width + Math.min( right, 0 );
		int y2o = y + height + Math.min( bottom, 0 );

		// shadow inner coordinates
		int x1i = x1o + shadowSize;
		int y1i = y1o + shadowSize;
		int x2i = x2o - shadowSize;
		int y2i = y2o - shadowSize;

		int wh = (shadowSize * 2) - 1;
		int center = shadowSize - 1;

		// left-top edge
		if( left > 0 || top > 0 ) {
			g.drawImage( shadowImage, x1o, y1o, x1i, y1i,
				0, 0, center, center, null );
		}

		// top shadow
		if( top > 0 ) {
			g.drawImage( shadowImage, x1i, y1o, x2i, y1i,
				center, 0, center + 1, center, null );
		}

		// right-top edge
		if( right > 0 || top > 0 ) {
			g.drawImage( shadowImage, x2i, y1o, x2o, y1i,
				center, 0, wh, center, null );
		}

		// left shadow
		if( left > 0 ) {
			g.drawImage( shadowImage, x1o, y1i, x1i, y2i,
				0, center, center, center + 1, null );
		}

		// right shadow
		if( right > 0 ) {
			g.drawImage( shadowImage, x2i, y1i, x2o, y2i,
				center, center, wh, center + 1, null );
		}

		// left-bottom edge
		if( left > 0 || bottom > 0 ) {
			g.drawImage( shadowImage, x1o, y2i, x1i, y2o,
				0, center, center, wh, null );
		}

		// bottom shadow
		if( bottom > 0 ) {
			g.drawImage( shadowImage, x1i, y2i, x2i, y2o,
				center, center, center + 1, wh, null );
		}

		// right-bottom edge
		if( right > 0 || bottom > 0 ) {
			g.drawImage( shadowImage, x2i, y2i, x2o, y2o,
				center, center, wh, wh, null );
		}
	}

	private int scale( int value, double scaleFactor ) {
		return (int) Math.ceil( UIScale.scale( value ) * scaleFactor );
	}

	private static BufferedImage createShadowImage( Color shadowColor, int shadowSize,
		float shadowOpacity, float scaleFactor )
	{
		int shadowRGB = shadowColor.getRGB() & 0xffffff;
		int shadowAlpha = (int) (255 * shadowOpacity);
		Color startColor = new Color( shadowRGB | ((shadowAlpha & 0xff) << 24), true );
		Color midColor = new Color( shadowRGB | (((shadowAlpha / 2) & 0xff) << 24), true );
		Color endColor = new Color( shadowRGB, true );

/*debug
		startColor = Color.red;
		midColor = Color.green;
		endColor = Color.blue;
debug*/

		int wh = (shadowSize * 2) - 1;
		int center = shadowSize - 1;

		RadialGradientPaint p = new RadialGradientPaint( center, center,
			shadowSize - (0.75f * scaleFactor),
			new float[] { 0, 0.35f, 1 },
			new Color[] { startColor, midColor, endColor } );

		BufferedImage image = new BufferedImage( wh, wh, BufferedImage.TYPE_INT_ARGB );

		Graphics2D g = image.createGraphics();
		try {
			g.setPaint( p );
			g.fillRect( 0, 0, wh, wh );
		} finally {
			g.dispose();
		}

		return image;
	}
}
