/*
 * 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 static com.formdev.flatlaf.util.UIScale.scale;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JComponent;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.LookAndFeel;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicSpinnerUI;
import com.formdev.flatlaf.FlatClientProperties;

/**
 * Provides the Flat LaF UI delegate for {@link javax.swing.JSpinner}.
 *
 * <!-- BasicSpinnerUI -->
 *
 * @uiDefault Spinner.font						Font
 * @uiDefault Spinner.background				Color
 * @uiDefault Spinner.foreground				Color
 * @uiDefault Spinner.border					Border
 * @uiDefault Spinner.disableOnBoundaryValues	boolean	default is false
 * @uiDefault Spinner.editorAlignment			int		0=center, 2=left, 4=right, 10=leading, 11=trailing
 * @uiDefault Spinner.editorBorderPainted		boolean	paint inner editor border; defaults to false
 *
 * <!-- FlatSpinnerUI -->
 *
 * @uiDefault Component.minimumWidth			int
 * @uiDefault Spinner.buttonStyle				String	button (default) or none
 * @uiDefault Component.arrowType				String	chevron (default) or triangle
 * @uiDefault Component.isIntelliJTheme			boolean
 * @uiDefault Component.borderColor				Color
 * @uiDefault Component.disabledBorderColor		Color
 * @uiDefault Spinner.disabledBackground		Color
 * @uiDefault Spinner.disabledForeground		Color
 * @uiDefault Spinner.buttonBackground			Color
 * @uiDefault Spinner.buttonArrowColor			Color
 * @uiDefault Spinner.buttonDisabledArrowColor	Color
 * @uiDefault Spinner.buttonHoverArrowColor		Color
 * @uiDefault Spinner.buttonPressedArrowColor	Color
 * @uiDefault Spinner.padding					Insets
 *
 * @author Karl Tauber
 */
public class FlatSpinnerUI
	extends BasicSpinnerUI
{
	private Handler handler;

	protected int minimumWidth;
	protected String buttonStyle;
	protected String arrowType;
	protected boolean isIntelliJTheme;
	protected Color borderColor;
	protected Color disabledBorderColor;
	protected Color disabledBackground;
	protected Color disabledForeground;
	protected Color buttonBackground;
	protected Color buttonArrowColor;
	protected Color buttonDisabledArrowColor;
	protected Color buttonHoverArrowColor;
	protected Color buttonPressedArrowColor;
	protected Insets padding;

	public static ComponentUI createUI( JComponent c ) {
		return new FlatSpinnerUI();
	}

	@Override
	protected void installDefaults() {
		super.installDefaults();

		LookAndFeel.installProperty( spinner, "opaque", false );

		minimumWidth = UIManager.getInt( "Component.minimumWidth" );
		buttonStyle = UIManager.getString( "Spinner.buttonStyle" );
		arrowType = UIManager.getString( "Component.arrowType" );
		isIntelliJTheme = UIManager.getBoolean( "Component.isIntelliJTheme" );
		borderColor = UIManager.getColor( "Component.borderColor" );
		disabledBorderColor = UIManager.getColor( "Component.disabledBorderColor" );
		disabledBackground = UIManager.getColor( "Spinner.disabledBackground" );
		disabledForeground = UIManager.getColor( "Spinner.disabledForeground" );
		buttonBackground = UIManager.getColor( "Spinner.buttonBackground" );
		buttonArrowColor = UIManager.getColor( "Spinner.buttonArrowColor" );
		buttonDisabledArrowColor = UIManager.getColor( "Spinner.buttonDisabledArrowColor" );
		buttonHoverArrowColor = UIManager.getColor( "Spinner.buttonHoverArrowColor" );
		buttonPressedArrowColor = UIManager.getColor( "Spinner.buttonPressedArrowColor" );
		padding = UIManager.getInsets( "Spinner.padding" );

		// scale
		padding = scale( padding );

		MigLayoutVisualPadding.install( spinner );
	}

	@Override
	protected void uninstallDefaults() {
		super.uninstallDefaults();

		borderColor = null;
		disabledBorderColor = null;
		disabledBackground = null;
		disabledForeground = null;
		buttonBackground = null;
		buttonArrowColor = null;
		buttonDisabledArrowColor = null;
		buttonHoverArrowColor = null;
		buttonPressedArrowColor = null;
		padding = null;

		MigLayoutVisualPadding.uninstall( spinner );
	}

	@Override
	protected void installListeners() {
		super.installListeners();

		addEditorFocusListener( spinner.getEditor() );
		spinner.addFocusListener( getHandler() );
		spinner.addPropertyChangeListener( getHandler() );
	}

	@Override
	protected void uninstallListeners() {
		super.uninstallListeners();

		removeEditorFocusListener( spinner.getEditor() );
		spinner.removeFocusListener( getHandler() );
		spinner.removePropertyChangeListener( getHandler() );

		handler = null;
	}

	private Handler getHandler() {
		if( handler == null )
			handler = new Handler();
		return handler;
	}

	@Override
	protected JComponent createEditor() {
		JComponent editor = super.createEditor();

		// explicitly make non-opaque
		editor.setOpaque( false );
		JTextField textField = getEditorTextField( editor );
		if( textField != null )
			textField.setOpaque( false );

		updateEditorColors();
		return editor;
	}

	@Override
	protected void replaceEditor( JComponent oldEditor, JComponent newEditor ) {
		super.replaceEditor( oldEditor, newEditor );

		removeEditorFocusListener( oldEditor );
		addEditorFocusListener( newEditor );
		updateEditorColors();
	}

	private void addEditorFocusListener( JComponent editor ) {
		JTextField textField = getEditorTextField( editor );
		if( textField != null )
			textField.addFocusListener( getHandler() );
	}

	private void removeEditorFocusListener( JComponent editor ) {
		JTextField textField = getEditorTextField( editor );
		if( textField != null )
			textField.removeFocusListener( getHandler() );
	}

	private void updateEditorColors() {
		JTextField textField = getEditorTextField( spinner.getEditor() );
		if( textField != null ) {
			// use non-UIResource colors because when SwingUtilities.updateComponentTreeUI()
			// is used, then the text field is updated after the spinner and the
			// colors are again replaced with default colors
			textField.setForeground( FlatUIUtils.nonUIResource( getForeground( true ) ) );
			textField.setDisabledTextColor( FlatUIUtils.nonUIResource( getForeground( false ) ) );
		}
	}

	private static JTextField getEditorTextField( JComponent editor ) {
		return editor instanceof JSpinner.DefaultEditor
			? ((JSpinner.DefaultEditor)editor).getTextField()
			: null;
	}

	protected Color getBackground( boolean enabled ) {
		return enabled
			? spinner.getBackground()
			: (isIntelliJTheme ? FlatUIUtils.getParentBackground( spinner ) : disabledBackground);
	}

	protected Color getForeground( boolean enabled ) {
		return enabled ? spinner.getForeground() : disabledForeground;
	}

	@Override
	protected LayoutManager createLayout() {
		return getHandler();
	}

	@Override
	protected Component createNextButton() {
		return createArrowButton( SwingConstants.NORTH, "Spinner.nextButton" );
	}

	@Override
	protected Component createPreviousButton() {
		return createArrowButton( SwingConstants.SOUTH, "Spinner.previousButton" );
	}

	private Component createArrowButton( int direction, String name ) {
		FlatArrowButton button = new FlatArrowButton( direction, arrowType, buttonArrowColor,
			buttonDisabledArrowColor, buttonHoverArrowColor, null, buttonPressedArrowColor, null );
		button.setName( name );
		button.setYOffset( (direction == SwingConstants.NORTH) ? 1 : -1 );
		if( direction == SwingConstants.NORTH )
			installNextButtonListeners( button );
		else
			installPreviousButtonListeners( button );
		return button;
	}

	@Override
	public void update( Graphics g, JComponent c ) {
		float focusWidth = FlatUIUtils.getBorderFocusWidth( c );
		float arc = FlatUIUtils.getBorderArc( c );

		// fill background if opaque to avoid garbage if user sets opaque to true
		if( c.isOpaque() && (focusWidth > 0 || arc > 0) )
			FlatUIUtils.paintParentBackground( g, c );

		Graphics2D g2 = (Graphics2D) g;
		Object[] oldRenderingHints = FlatUIUtils.setRenderingHints( g2 );

		int width = c.getWidth();
		int height = c.getHeight();
		boolean enabled = spinner.isEnabled();

		// paint background
		g2.setColor( getBackground( enabled ) );
		FlatUIUtils.paintComponentBackground( g2, 0, 0, width, height, focusWidth, arc );

		// paint button background and separator
		boolean paintButton = !"none".equals( buttonStyle );
		Handler handler = getHandler();
		if( paintButton && (handler.nextButton != null || handler.previousButton != null) ) {
			Component button = (handler.nextButton != null) ? handler.nextButton : handler.previousButton;
			int arrowX = button.getX();
			int arrowWidth = button.getWidth();
			boolean isLeftToRight = spinner.getComponentOrientation().isLeftToRight();

			// paint arrow buttons background
			if( enabled ) {
				g2.setColor( buttonBackground );
				Shape oldClip = g2.getClip();
				if( isLeftToRight )
					g2.clipRect( arrowX, 0, width - arrowX, height );
				else
					g2.clipRect( 0, 0, arrowX + arrowWidth, height );
				FlatUIUtils.paintComponentBackground( g2, 0, 0, width, height, focusWidth, arc );
				g2.setClip( oldClip );
			}

			// paint vertical line between value and arrow buttons
			g2.setColor( enabled ? borderColor : disabledBorderColor );
			float lw = scale( 1f );
			float lx = isLeftToRight ? arrowX : arrowX + arrowWidth - lw;
			g2.fill( new Rectangle2D.Float( lx, focusWidth, lw, height - 1 - (focusWidth * 2) ) );
		}

		paint( g, c );

		FlatUIUtils.resetRenderingHints( g, oldRenderingHints );
	}

	//---- class Handler ------------------------------------------------------

	private class Handler
		implements LayoutManager, FocusListener, PropertyChangeListener
	{
		//---- interface LayoutManager ----

		private Component editor = null;
		private Component nextButton;
		private Component previousButton;

		@Override
		public void addLayoutComponent( String name, Component c ) {
			switch( name ) {
				case "Editor":	editor = c; break;
				case "Next":		nextButton = c; break;
				case "Previous":	previousButton = c; break;
			}
		}

		@Override
		public void removeLayoutComponent( Component c ) {
			if( c == editor )
				editor = null;
			else if( c == nextButton )
				nextButton = null;
			else if( c == previousButton )
				previousButton = null;
		}

		@Override
		public Dimension preferredLayoutSize( Container parent ) {
			Insets insets = parent.getInsets();
			Dimension editorSize = (editor != null) ? editor.getPreferredSize() : new Dimension( 0, 0 );

			// the arrows width is the same as the inner height so that the arrows area is square
			int minimumWidth = FlatUIUtils.minimumWidth( spinner, FlatSpinnerUI.this.minimumWidth );
			int innerHeight = editorSize.height + padding.top + padding.bottom;
			float focusWidth = FlatUIUtils.getBorderFocusWidth( spinner );
			return new Dimension(
				Math.max( insets.left + insets.right + editorSize.width + padding.left + padding.right + innerHeight, scale( minimumWidth ) + Math.round( focusWidth * 2 ) ),
				insets.top + insets.bottom + innerHeight );
		}

		@Override
		public Dimension minimumLayoutSize( Container parent ) {
			return preferredLayoutSize( parent );
		}

		@Override
		public void layoutContainer( Container parent ) {
			Dimension size = parent.getSize();
			Insets insets = parent.getInsets();
			Rectangle r = FlatUIUtils.subtractInsets( new Rectangle( size ), insets );

			if( nextButton == null && previousButton == null ) {
				if( editor != null )
					editor.setBounds( FlatUIUtils.subtractInsets( r, padding ) );
				return;
			}

			Rectangle editorRect = new Rectangle( r );
			Rectangle buttonsRect = new Rectangle( r );

			// make button area square
			int buttonsWidth = r.height;
			buttonsRect.width = buttonsWidth;

			if( parent.getComponentOrientation().isLeftToRight() ) {
				editorRect.width -= buttonsWidth;
				buttonsRect.x += editorRect.width;
			} else {
				editorRect.x += buttonsWidth;
				editorRect.width -= buttonsWidth;
			}

			if( editor != null )
				editor.setBounds( FlatUIUtils.subtractInsets( editorRect, padding ) );

			int nextHeight = (buttonsRect.height / 2) + (buttonsRect.height % 2); // round up
			if( nextButton != null )
				nextButton.setBounds( buttonsRect.x, buttonsRect.y, buttonsRect.width, nextHeight );
			if( previousButton != null ) {
				// for precise layout of arrows, the "previous" button has the same height
				// as the "next" button and may overlap on uneven buttonsRect.height
				int previousY = buttonsRect.y + buttonsRect.height - nextHeight;
				previousButton.setBounds( buttonsRect.x, previousY, buttonsRect.width, nextHeight );
			}
		}

		//---- interface FocusListener ----

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

			// if spinner gained focus, transfer it to the editor text field
			if( e.getComponent() == spinner ) {
				JTextField textField = getEditorTextField( spinner.getEditor() );
				if( textField != null )
					textField.requestFocusInWindow();
			}
		}

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

		//---- interface PropertyChangeListener ----

		@Override
		public void propertyChange( PropertyChangeEvent e ) {
			switch( e.getPropertyName() ) {
				case "foreground":
				case "enabled":
					updateEditorColors();
					break;

				case FlatClientProperties.COMPONENT_ROUND_RECT:
					spinner.repaint();
					break;

				case FlatClientProperties.MINIMUM_WIDTH:
					spinner.revalidate();
					break;
			}
		}
	}
}
