/*
 * Copyright 2019 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
 *
 *     http://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.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.util.function.Consumer;
import javax.swing.JComponent;
import javax.swing.LookAndFeel;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.plaf.UIResource;
import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.util.DerivedColor;
import com.formdev.flatlaf.util.Graphics2DProxy;
import com.formdev.flatlaf.util.HiDPIUtils;
import com.formdev.flatlaf.util.UIScale;

/**
 * Utility methods for UI delegates.
 *
 * @author Karl Tauber
 */
public class FlatUIUtils
{
	public static final boolean MAC_USE_QUARTZ = Boolean.getBoolean( "apple.awt.graphics.UseQuartz" );

	public static Rectangle addInsets( Rectangle r, Insets insets ) {
		return new Rectangle(
			r.x - insets.left,
			r.y - insets.top,
			r.width + insets.left + insets.right,
			r.height + insets.top + insets.bottom );
	}

	public static Rectangle subtractInsets( Rectangle r, Insets insets ) {
		return new Rectangle(
			r.x + insets.left,
			r.y + insets.top,
			r.width - insets.left - insets.right,
			r.height - insets.top - insets.bottom );
	}

	public static Dimension addInsets( Dimension dim, Insets insets ) {
		return new Dimension(
			dim.width + insets.left + insets.right,
			dim.height + insets.top + insets.bottom );
	}

	public static Insets addInsets( Insets insets1, Insets insets2 ) {
		return new Insets(
			insets1.top + insets2.top,
			insets1.left + insets2.left,
			insets1.bottom + insets2.bottom,
			insets1.right + insets2.right );
	}

	public static void setInsets( Insets dest, Insets src ) {
		dest.top = src.top;
		dest.left = src.left;
		dest.bottom = src.bottom;
		dest.right = src.right;
	}

	public static Color getUIColor( String key, int defaultColorRGB ) {
		Color color = UIManager.getColor( key );
		return (color != null) ? color : new Color( defaultColorRGB );
	}

	public static Color getUIColor( String key, Color defaultColor ) {
		Color color = UIManager.getColor( key );
		return (color != null) ? color : defaultColor;
	}

	public static Color getUIColor( String key, String defaultKey ) {
		Color color = UIManager.getColor( key );
		return (color != null) ? color : UIManager.getColor( defaultKey );
	}

	public static int getUIInt( String key, int defaultValue ) {
		Object value = UIManager.get( key );
		return (value instanceof Integer) ? (Integer) value : defaultValue;
	}

	public static float getUIFloat( String key, float defaultValue ) {
		Object value = UIManager.get( key );
		return (value instanceof Number) ? ((Number)value).floatValue() : defaultValue;
	}

	public static Color nonUIResource( Color c ) {
		return (c instanceof UIResource) ? new Color( c.getRGB(), true ) : c;
	}

	public static Font nonUIResource( Font font ) {
		return (font instanceof UIResource) ? font.deriveFont( font.getStyle() ) : font;
	}

	public static int minimumWidth( JComponent c, int minimumWidth ) {
		return FlatClientProperties.clientPropertyInt( c, FlatClientProperties.MINIMUM_WIDTH, minimumWidth );
	}

	public static int minimumHeight( JComponent c, int minimumHeight ) {
		return FlatClientProperties.clientPropertyInt( c, FlatClientProperties.MINIMUM_HEIGHT, minimumHeight );
	}

	public static boolean isTableCellEditor( Component c ) {
		return c instanceof JComponent && Boolean.TRUE.equals( ((JComponent)c).getClientProperty( "JComboBox.isTableCellEditor" ) );
	}

	public static boolean isPermanentFocusOwner( Component c ) {
		return (KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner() == c);
	}

	public static boolean isRoundRect( Component c ) {
		return c instanceof JComponent && FlatClientProperties.clientPropertyBoolean(
			(JComponent) c, FlatClientProperties.COMPONENT_ROUND_RECT, false );
	}

	/**
	 * Returns the scaled thickness of the outer focus border for the given component.
	 */
	public static float getBorderFocusWidth( JComponent c ) {
		FlatBorder border = getOutsideFlatBorder( c );
		return (border != null)
			? UIScale.scale( (float) border.getFocusWidth( c ) )
			: 0;
	}

	/**
	 * Returns the scaled arc diameter of the border for the given component.
	 */
	public static float getBorderArc( JComponent c ) {
		FlatBorder border = getOutsideFlatBorder( c );
		return (border != null)
			? UIScale.scale( (float) border.getArc( c ) )
			: 0;
	}

	public static boolean hasRoundBorder( JComponent c ) {
		return getBorderArc( c ) >= c.getHeight();
	}

	public static FlatBorder getOutsideFlatBorder( JComponent c ) {
		Border border = c.getBorder();
		for(;;) {
			if( border instanceof FlatBorder )
				return (FlatBorder) border;
			else if( border instanceof CompoundBorder )
				border = ((CompoundBorder)border).getOutsideBorder();
			else
				return null;
		}
	}

	/**
	 * Sets rendering hints used for painting.
	 */
	public static void setRenderingHints( Graphics2D g ) {
		g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
		g.setRenderingHint( RenderingHints.KEY_STROKE_CONTROL,
			MAC_USE_QUARTZ ? RenderingHints.VALUE_STROKE_PURE : RenderingHints.VALUE_STROKE_NORMALIZE );
	}

	public static Color deriveColor( Color color, Color baseColor ) {
		return (color instanceof DerivedColor)
			? ((DerivedColor)color).derive( baseColor )
			: color;
	}

	/**
	 * Paints an outer border, which is usually a focus border.
	 * <p>
	 * The outside bounds of the painted border are {@code x,y,width,height}.
	 * The line width of the painted border is {@code focusWidth + lineWidth}.
	 * The given arc diameter refers to the inner rectangle ({@code x,y,width,height} minus {@code focusWidth}).
	 *
	 * @see #paintComponentBorder
	 * @see #paintComponentBackground
	 */
	public static void paintComponentOuterBorder( Graphics2D g, int x, int y, int width, int height,
		float focusWidth, float lineWidth, float arc )
	{
		double systemScaleFactor = UIScale.getSystemScaleFactor( g );
		if( systemScaleFactor != 1 && systemScaleFactor != 2 ) {
			// paint at scale 1x to avoid clipping on right and bottom edges at 125%, 150% or 175%
			HiDPIUtils.paintAtScale1x( g, x, y, width, height,
				(g2d, x2, y2, width2, height2, scaleFactor) -> {
					paintComponentOuterBorderImpl( g2d, x2, y2, width2, height2,
						(float) (focusWidth * scaleFactor), (float) (lineWidth * scaleFactor), (float) (arc * scaleFactor) );
				} );
			return;
		}

		paintComponentOuterBorderImpl( g, x, y, width, height, focusWidth, lineWidth, arc );
	}

	private static void paintComponentOuterBorderImpl( Graphics2D g, int x, int y, int width, int height,
		float focusWidth, float lineWidth, float arc )
	{
		float ow = focusWidth + lineWidth;
		float outerArc = arc + (focusWidth * 2);
		float innerArc = arc - (lineWidth * 2);

		// reduce outer arc slightly for small arcs to make the curve slightly wider
		if( arc > 0 && arc < UIScale.scale( 10 ) )
			outerArc -= UIScale.scale( 2f );

		Path2D path = new Path2D.Float( Path2D.WIND_EVEN_ODD );
		path.append( createComponentRectangle( x, y, width, height, outerArc ), false );
		path.append( createComponentRectangle( x + ow, y + ow, width - (ow * 2), height - (ow * 2), innerArc ), false );
		g.fill( path );
	}

	/**
	 * Draws the border of a component as round rectangle.
	 * <p>
	 * The outside bounds of the painted border are
	 * {@code x + focusWidth, y + focusWidth, width - (focusWidth * 2), height - (focusWidth * 2)}.
	 * The given arc diameter refers to the painted rectangle (and not to {@code x,y,width,height}).
	 *
	 * @see #paintComponentOuterBorder
	 * @see #paintComponentBackground
	 */
	public static void paintComponentBorder( Graphics2D g, int x, int y, int width, int height,
		float focusWidth, float lineWidth, float arc )
	{
		double systemScaleFactor = UIScale.getSystemScaleFactor( g );
		if( systemScaleFactor != 1 && systemScaleFactor != 2 ) {
			// paint at scale 1x to avoid clipping on right and bottom edges at 125%, 150% or 175%
			HiDPIUtils.paintAtScale1x( g, x, y, width, height,
				(g2d, x2, y2, width2, height2, scaleFactor) -> {
					paintComponentBorderImpl( g2d, x2, y2, width2, height2,
						(float) (focusWidth * scaleFactor), (float) (lineWidth * scaleFactor), (float) (arc * scaleFactor) );
				} );
			return;
		}

		paintComponentBorderImpl( g, x, y, width, height, focusWidth, lineWidth, arc );
	}

	private static void paintComponentBorderImpl( Graphics2D g, int x, int y, int width, int height,
		float focusWidth, float lineWidth, float arc )
	{
		float x1 = x + focusWidth;
		float y1 = y + focusWidth;
		float width1 = width - focusWidth * 2;
		float height1 = height - focusWidth * 2;
		float arc2 = arc - (lineWidth * 2);

		Shape r1 = createComponentRectangle( x1, y1, width1, height1, arc );
		Shape r2 = createComponentRectangle(
			x1 + lineWidth, y1 + lineWidth,
			width1 - lineWidth * 2, height1 - lineWidth * 2, arc2 );

		Path2D border = new Path2D.Float( Path2D.WIND_EVEN_ODD );
		border.append( r1, false );
		border.append( r2, false );
		g.fill( border );
	}

	/**
	 * Fills the background of a component with a round rectangle.
	 * <p>
	 * The bounds of the painted round rectangle are
	 * {@code x + focusWidth, y + focusWidth, width - (focusWidth * 2), height - (focusWidth * 2)}.
	 * The given arc diameter refers to the painted rectangle (and not to {@code x,y,width,height}).
	 *
	 * @see #paintComponentOuterBorder
	 * @see #paintComponentBorder
	 */
	public static void paintComponentBackground( Graphics2D g, int x, int y, int width, int height,
		float focusWidth, float arc )
	{
		double systemScaleFactor = UIScale.getSystemScaleFactor( g );
		if( systemScaleFactor != 1 && systemScaleFactor != 2 ) {
			// paint at scale 1x to avoid clipping on right and bottom edges at 125%, 150% or 175%
			HiDPIUtils.paintAtScale1x( g, x, y, width, height,
				(g2d, x2, y2, width2, height2, scaleFactor) -> {
					paintComponentBackgroundImpl( g2d, x2, y2, width2, height2,
						(float) (focusWidth * scaleFactor), (float) (arc * scaleFactor) );
				} );
			return;
		}

		paintComponentBackgroundImpl( g, x, y, width, height, focusWidth, arc );
	}

	private static void paintComponentBackgroundImpl( Graphics2D g, int x, int y, int width, int height,
		float focusWidth, float arc )
	{
		g.fill( createComponentRectangle(
			x + focusWidth, y + focusWidth,
			width - focusWidth * 2, height - focusWidth * 2, arc ) );
	}

	/**
	 * Creates a (rounded) rectangle used to paint components (border, background, etc).
	 * The given arc diameter is limited to min(width,height).
	 */
	public static Shape createComponentRectangle( float x, float y, float w, float h, float arc ) {
		if( arc <= 0 )
			return new Rectangle2D.Float( x, y, w, h );

		arc = Math.min( arc, Math.min( w, h ) );
		return new RoundRectangle2D.Float( x, y, w, h, arc, arc );
	}

	/**
	 * Fill background with parent's background color because the visible component
	 * is smaller than its bounds (for the focus decoration).
	 */
	public static void paintParentBackground( Graphics g, JComponent c ) {
		Container parent = findOpaqueParent( c );
		if( parent != null ) {
			g.setColor( parent.getBackground() );
			g.fillRect( 0, 0, c.getWidth(), c.getHeight() );
		}
	}

	/**
	 * Gets the background color of the first opaque parent.
	 */
	public static Color getParentBackground( JComponent c ) {
		Container parent = findOpaqueParent( c );
		return (parent != null)
			? parent.getBackground()
			: UIManager.getColor( "Panel.background" ); // fallback, probably never used
	}

	/**
	 * Find the first parent that is opaque.
	 */
	private static Container findOpaqueParent( Container c ) {
		while( (c = c.getParent()) != null ) {
			if( c.isOpaque() )
				return c;
		}
		return null;
	}

	/**
	 * Creates a not-filled rectangle shape with the given line width.
	 */
	public static Path2D createRectangle( float x, float y, float width, float height, float lineWidth ) {
		Path2D path = new Path2D.Float( Path2D.WIND_EVEN_ODD );
		path.append( new Rectangle2D.Float( x, y, width, height ), false );
		path.append( new Rectangle2D.Float( x + lineWidth, y + lineWidth,
			width - (lineWidth * 2), height - (lineWidth * 2) ), false );
		return path;
	}

	/**
	 * Creates a not-filled rounded rectangle shape and allows specifying the line width and the radius or each corner.
	 */
	public static Path2D createRoundRectangle( float x, float y, float width, float height,
		float lineWidth, float arcTopLeft, float arcTopRight, float arcBottomLeft, float arcBottomRight )
	{
		Path2D path = new Path2D.Float( Path2D.WIND_EVEN_ODD );
		path.append( createRoundRectanglePath( x, y, width, height, arcTopLeft, arcTopRight, arcBottomLeft, arcBottomRight ), false );
		path.append( createRoundRectanglePath( x + lineWidth, y + lineWidth, width - (lineWidth * 2), height - (lineWidth * 2),
			arcTopLeft - lineWidth, arcTopRight - lineWidth, arcBottomLeft - lineWidth, arcBottomRight - lineWidth ), false );
		return path;
	}

	/**
	 * Creates a filled rounded rectangle shape and allows specifying the radius of each corner.
	 */
	public static Shape createRoundRectanglePath( float x, float y, float width, float height,
		float arcTopLeft, float arcTopRight, float arcBottomLeft, float arcBottomRight )
	{
		if( arcTopLeft <= 0 && arcTopRight <= 0 && arcBottomLeft <= 0 && arcBottomRight <= 0 )
			return new Rectangle2D.Float( x, y, width, height );

		// limit arcs to min(width,height)
		float maxArc = Math.min( width, height ) / 2;
		arcTopLeft = (arcTopLeft > 0) ? Math.min( arcTopLeft, maxArc ) : 0;
		arcTopRight = (arcTopRight > 0) ? Math.min( arcTopRight, maxArc ) : 0;
		arcBottomLeft = (arcBottomLeft > 0) ? Math.min( arcBottomLeft, maxArc ) : 0;
		arcBottomRight = (arcBottomRight > 0) ? Math.min( arcBottomRight, maxArc ) : 0;

		float x2 = x + width;
		float y2 = y + height;

		Path2D rect = new Path2D.Float();
		rect.moveTo( x2 - arcTopRight, y );
		rect.quadTo( x2, y, x2, y + arcTopRight );
		rect.lineTo( x2, y2 - arcBottomRight );
		rect.quadTo( x2, y2, x2 - arcBottomRight, y2 );
		rect.lineTo( x + arcBottomLeft, y2 );
		rect.quadTo( x, y2, x, y2 - arcBottomLeft );
		rect.lineTo( x, y + arcTopLeft );
		rect.quadTo( x, y, x + arcTopLeft, y );
		rect.closePath();

		return rect;
	}

	/**
	 * Creates a closed path for the given points.
	 */
	public static Path2D createPath( double... points ) {
		return createPath( true, points );
	}

	/**
	 * Creates a open or closed path for the given points.
	 */
	public static Path2D createPath( boolean close, double... points ) {
		Path2D path = new Path2D.Float();
		path.moveTo( points[0], points[1] );
		for( int i = 2; i < points.length; i += 2 )
			path.lineTo( points[i], points[i + 1] );
		if( close )
			path.closePath();
		return path;
	}

	/**
	 * Draws the given string at the specified location.
	 * The provided component is used to query text properties and anti-aliasing hints.
	 * <p>
	 * Use this method instead of {@link Graphics#drawString(String, int, int)} for correct anti-aliasing.
	 * <p>
	 * Replacement for {@code SwingUtilities2.drawString()}.
	 * Uses {@link HiDPIUtils#drawStringWithYCorrection(JComponent, Graphics2D, String, int, int)}.
	 */
	public static void drawString( JComponent c, Graphics g, String text, int x, int y ) {
		HiDPIUtils.drawStringWithYCorrection( c, (Graphics2D) g, text, x, y );
	}

	/**
	 * Draws the given string at the specified location underlining the specified character.
	 * The provided component is used to query text properties and anti-aliasing hints.
	 * <p>
	 * Replacement for {@code SwingUtilities2.drawStringUnderlineCharAt()}.
	 * Uses {@link HiDPIUtils#drawStringUnderlineCharAtWithYCorrection(JComponent, Graphics2D, String, int, int, int)}.
	 */
	public static void drawStringUnderlineCharAt( JComponent c, Graphics g,
		String text, int underlinedIndex, int x, int y )
	{
		// scale underline height if necessary
		if( underlinedIndex >= 0 && UIScale.getUserScaleFactor() > 1 ) {
			g = new Graphics2DProxy( (Graphics2D) g ) {
				@Override
				public void fillRect( int x, int y, int width, int height ) {
					if( height == 1 ) {
						// scale height and correct y position
						// (using 0.9f so that underline height is 1 at scale factor 1.5x)
						height = Math.round( UIScale.scale( 0.9f ) );
						y += height - 1;
					}

					super.fillRect( x, y, width, height );
				}
			};
		}

		HiDPIUtils.drawStringUnderlineCharAtWithYCorrection( c, (Graphics2D) g, text, underlinedIndex, x, y );
	}

	public static boolean hasOpaqueBeenExplicitlySet( JComponent c ) {
		boolean oldOpaque = c.isOpaque();
		LookAndFeel.installProperty( c, "opaque", !oldOpaque );
		boolean explicitlySet = c.isOpaque() == oldOpaque;
		LookAndFeel.installProperty( c, "opaque", oldOpaque );
		return explicitlySet;
	}

	//---- class HoverListener ------------------------------------------------

	public static class HoverListener
		extends MouseAdapter
	{
		private final Component repaintComponent;
		private final Consumer<Boolean> hoverChanged;

		public HoverListener( Component repaintComponent, Consumer<Boolean> hoverChanged ) {
			this.repaintComponent = repaintComponent;
			this.hoverChanged = hoverChanged;
		}

		@Override
		public void mouseEntered( MouseEvent e ) {
			hoverChanged.accept( true );
			repaint();
		}

		@Override
		public void mouseExited( MouseEvent e ) {
			hoverChanged.accept( false );
			repaint();
		}

		private void repaint() {
			if( repaintComponent != null && repaintComponent.isEnabled() )
				repaintComponent.repaint();
		}
	}

	//---- class RepaintFocusListener -----------------------------------------

	public static class RepaintFocusListener
		implements FocusListener
	{
		private final Component repaintComponent;

		public RepaintFocusListener( Component repaintComponent ) {
			this.repaintComponent = repaintComponent;
		}

		@Override
		public void focusGained( FocusEvent e ) {
			repaintComponent.repaint();
		}

		@Override
		public void focusLost( FocusEvent e ) {
			repaintComponent.repaint();
		}
	}
}