/*******************************************************************************
 * Copyright (c) 2000, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.swt.examples.paint;

import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter;

import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Region;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;

/**
 * Manages a simple drawing surface.
 */
public class PaintSurface {
	private Point currentPosition = new Point(0, 0);
	private Canvas paintCanvas;

	private PaintSession paintSession;
	private Image image;
	private Image paintImage; // buffer for refresh blits
	private int   imageWidth, imageHeight;
	private int   visibleWidth, visibleHeight;

	private FigureDrawContext displayFDC = new FigureDrawContext();
	private FigureDrawContext imageFDC = new FigureDrawContext();
	private FigureDrawContext paintFDC = new FigureDrawContext();

	/* Rubberband */
	private ContainerFigure rubberband = new ContainerFigure();
		// the active rubberband selection
	private int rubberbandHiddenNestingCount = 0;
		// always >= 0, if > 0 rubberband has been hidden

	/* Status */
	private Text statusText;
	private String statusActionInfo, statusMessageInfo, statusCoordInfo;

	/**
	 * Constructs a PaintSurface.
	 * <p>
	 * paintCanvas must have SWT.NO_REDRAW_RESIZE and SWT.NO_BACKGROUND styles,
	 *     and may have SWT.V_SCROLL and/or SWT.H_SCROLL.
	 * </p>
	 * @param paintCanvas the Canvas object in which to render
	 * @param paintStatus the PaintStatus object to use for providing user feedback
	 * @param fillColor the color to fill the canvas with initially
	 */
	public PaintSurface(Canvas paintCanvas, Text statusText, Color fillColor) {
		this.paintCanvas = paintCanvas;
		this.statusText = statusText;
		clearStatus();

		/* Set up the drawing surface */
		Rectangle displayRect = paintCanvas.getDisplay().getClientArea();
		imageWidth = displayRect.width;
		imageHeight = displayRect.height;
		image = new Image(paintCanvas.getDisplay(), imageWidth, imageHeight);

		imageFDC.gc = new GC(image);
		imageFDC.gc.setBackground(fillColor);
		imageFDC.gc.fillRectangle(0, 0, imageWidth, imageHeight);
		displayFDC.gc = new GC(paintCanvas);

		/* Initialize the session */
		setPaintSession(null);

		/* Add our listeners */
		paintCanvas.addDisposeListener(e -> displayFDC.gc.dispose());
		paintCanvas.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseDown(MouseEvent event) {
				processMouseEventCoordinates(event);
				if (paintSession != null) paintSession.mouseDown(event);
			}
			@Override
			public void mouseUp(MouseEvent event) {
				processMouseEventCoordinates(event);
				if (paintSession != null) paintSession.mouseUp(event);
			}
			@Override
			public void mouseDoubleClick(MouseEvent event) {
				processMouseEventCoordinates(event);
				if (paintSession != null) paintSession.mouseDoubleClick(event);
			}
		});
		paintCanvas.addMouseMoveListener(event -> {
			processMouseEventCoordinates(event);
			if (paintSession != null) paintSession.mouseMove(event);
		});
		paintCanvas.addPaintListener(event -> {
			if (rubberband.isEmpty()) {
				// Nothing to merge, so we just refresh
				event.gc.drawImage(image,
					displayFDC.xOffset + event.x, displayFDC.yOffset + event.y, event.width, event.height,
					event.x, event.y, event.width, event.height);
			} else {
				/*
				 * Avoid flicker when merging overlayed objects by constructing the image on
				 * a backbuffer first, then blitting it to the screen.
				 */
				// Check that the backbuffer is large enough
				if (paintImage != null) {
					Rectangle rect1 = paintImage.getBounds();
					if ((event.width + event.x > rect1.width) ||
						(event.height + event.y > rect1.height)) {
						paintFDC.gc.dispose();
						paintImage.dispose();
						paintImage = null;
					}
				}
				if (paintImage == null) {
					Display display = getDisplay();
					Rectangle rect2 = display.getClientArea();
					paintImage = new Image(display,
						Math.max(rect2.width, event.width + event.x),
						Math.max(rect2.height, event.height + event.y));
					paintFDC.gc = new GC(paintImage);
				}
				// Setup clipping and the FDC
				Region clipRegion = new Region();
				event.gc.getClipping(clipRegion);
				paintFDC.gc.setClipping(clipRegion);
				clipRegion.dispose();

				paintFDC.xOffset = displayFDC.xOffset;
				paintFDC.yOffset = displayFDC.yOffset;
				paintFDC.xScale = displayFDC.xScale;
				paintFDC.yScale = displayFDC.yScale;

				// Merge the overlayed objects into the image, then blit
				paintFDC.gc.drawImage(image,
					displayFDC.xOffset + event.x, displayFDC.yOffset + event.y, event.width, event.height,
					event.x, event.y, event.width, event.height);
				rubberband.draw(paintFDC);
				event.gc.drawImage(paintImage,
					event.x, event.y, event.width, event.height,
					event.x, event.y, event.width, event.height);
			}
		});
		paintCanvas.addControlListener(ControlListener.controlResizedAdapter(e -> handleResize()));

		/* Set up the paint canvas scroll bars */
		ScrollBar horizontal = paintCanvas.getHorizontalBar();
		horizontal.setVisible(true);
		horizontal.addSelectionListener(widgetSelectedAdapter(event -> scrollHorizontally((ScrollBar)event.widget)));
		ScrollBar vertical = paintCanvas.getVerticalBar();
		vertical.setVisible(true);
		vertical.addSelectionListener(widgetSelectedAdapter(event -> scrollVertically((ScrollBar)event.widget)));
		handleResize();
	}

	/**
	 * Disposes of the PaintSurface's resources.
	 */
	public void dispose() {
		imageFDC.gc.dispose();
		image.dispose();
		if (paintImage != null) {
			paintImage.dispose();
			paintFDC.gc.dispose();
		}

		currentPosition = null;
		paintCanvas = null;
		paintSession = null;
		image = null;
		paintImage = null;
		displayFDC = null;
		imageFDC = null;
		paintFDC = null;
		rubberband = null;
		statusText = null;
		statusActionInfo = null;
		statusMessageInfo = null;
		statusCoordInfo = null;
	}

	/**
	 * Called when we must grab focus.
	 */
	public void setFocus()  {
		paintCanvas.setFocus();
	}

	/**
	 * Returns the Display on which the PaintSurface resides.
	 * @return the Display
	 */
	public Display getDisplay() {
		return paintCanvas.getDisplay();
	}

	/**
	 * Returns the Shell in which the PaintSurface resides.
	 * @return the Shell
	 */
	public Shell getShell() {
		return paintCanvas.getShell();
	}

	/**
	 * Sets the current paint session.
	 * <p>
	 * If oldPaintSession != paintSession calls oldPaintSession.end()
	 * and paintSession.begin()
	 * </p>
	 *
	 * @param paintSession the paint session to activate; null to disable all sessions
	 */
	public void setPaintSession(PaintSession paintSession) {
		if (this.paintSession != null) {
			if (this.paintSession == paintSession) return;
			this.paintSession.endSession();
		}
		this.paintSession = paintSession;
		clearStatus();
		if (paintSession != null) {
			setStatusAction(paintSession.getDisplayName());
			paintSession.beginSession();
		} else {
			setStatusAction(PaintExample.getResourceString("tool.Null.label"));
			setStatusMessage(PaintExample.getResourceString("session.Null.message"));
		}
	}

	/**
	 * Returns the current paint session.
	 *
	 * @return the current paint session, null if none is active
	 */
	public PaintSession getPaintSession() {
		return paintSession;
	}

	/**
	 * Returns the current paint tool.
	 *
	 * @return the current paint tool, null if none is active (though some other session
	 *         might be)
	 */
	public PaintTool getPaintTool() {
		return (paintSession != null && paintSession instanceof PaintTool) ?
			(PaintTool)paintSession : null;
	}

	/**
	 * Returns the current position in an interactive operation.
	 *
	 * @return the last known position of the pointer
	 */
	public Point getCurrentPosition() {
		return currentPosition;
	}

	/**
	 * Draws a Figure object to the screen and to the backing store permanently.
	 *
	 * @param object the object to draw onscreen
	 */
	public void drawFigure(Figure object) {
		object.draw(imageFDC);
		object.draw(displayFDC);
	}

	/**
	 * Adds a Figure object to the active rubberband selection.
	 * <p>
	 * This object will be drawn to the screen as a preview and refreshed appropriately
	 * until the selection is either cleared or committed.
	 * </p>
	 *
	 * @param object the object to add to the selection
	 */
	public void addRubberbandSelection(Figure object) {
		rubberband.add(object);
		if (! isRubberbandHidden()) object.draw(displayFDC);
	}

	/**
	 * Clears the active rubberband selection.
	 * <p>
	 * Erases any rubberband objects on the screen then clears the selection.
	 * </p>
	 */
	public void clearRubberbandSelection() {
		if (! isRubberbandHidden()) {
			Region region = new Region();
			rubberband.addDamagedRegion(displayFDC, region);
			Rectangle r = region.getBounds();
			paintCanvas.redraw(r.x, r.y, r.width, r.height, true);
			region.dispose();
		}
		rubberband.clear();

	}

	/**
	 * Commits the active rubberband selection.
	 * <p>
	 * Redraws any rubberband objects on the screen as permanent objects then clears the selection.
	 * </p>
	 */
	public void commitRubberbandSelection() {
		rubberband.draw(imageFDC);
		if (isRubberbandHidden()) rubberband.draw(displayFDC);
		rubberband.clear();
	}

	/**
	 * Hides the rubberband (but does not eliminate it).
	 * <p>
	 * Increments by one the rubberband "hide" nesting count.  The rubberband
	 * is hidden from view (but remains active) if it wasn't already hidden.
	 * </p>
	 */
	public void hideRubberband() {
		if (rubberbandHiddenNestingCount++ <= 0) {
			Region region = new Region();
			rubberband.addDamagedRegion(displayFDC, region);
			Rectangle r = region.getBounds();
			paintCanvas.redraw(r.x, r.y, r.width, r.height, true);
			region.dispose();
		}
	}

	/**
	 * Shows (un-hides) the rubberband.
	 * <p>
	 * Decrements by one the rubberband "hide" nesting count.  The rubberband
	 * is only made visible when showRubberband() has been called once for each
	 * previous hideRubberband().  It is not permitted to call showRubberband() if
	 * the rubber band is not presently hidden.
	 * </p>
	 */
	public void showRubberband() {
		if (rubberbandHiddenNestingCount <= 0)
			throw new IllegalStateException("rubberbandHiddenNestingCount > 0");
		if (--rubberbandHiddenNestingCount == 0) {
			rubberband.draw(displayFDC);
		}
	}

	/**
	 * Determines if the rubberband is hidden.
	 *
	 * @return true iff the rubber is hidden
	 */
	public boolean isRubberbandHidden() {
		return rubberbandHiddenNestingCount > 0;
	}

	/**
	 * Handles a horizontal scroll event
	 *
	 * @param scrollbar the horizontal scroll bar that posted this event
	 */
	public void scrollHorizontally(ScrollBar scrollBar) {
		if (image == null) return;
		if (imageWidth > visibleWidth) {
			final int oldOffset = displayFDC.xOffset;
			final int newOffset = Math.min(scrollBar.getSelection(), imageWidth - visibleWidth);
			if (oldOffset != newOffset) {
				paintCanvas.update();
				displayFDC.xOffset = newOffset;
				paintCanvas.scroll(Math.max(oldOffset - newOffset, 0), 0, Math.max(newOffset - oldOffset, 0), 0,
					visibleWidth, visibleHeight, false);
			}
		}
	}

	/**
	 * Handles a vertical scroll event
	 *
	 * @param scrollbar the vertical scroll bar that posted this event
	 */
	public void scrollVertically(ScrollBar scrollBar) {
		if (image == null) return;
		if (imageHeight > visibleHeight) {
			final int oldOffset = displayFDC.yOffset;
			final int newOffset = Math.min(scrollBar.getSelection(), imageHeight - visibleHeight);
			if (oldOffset != newOffset) {
				paintCanvas.update();
				displayFDC.yOffset = newOffset;
				paintCanvas.scroll(0, Math.max(oldOffset - newOffset, 0), 0, Math.max(newOffset - oldOffset, 0),
					visibleWidth, visibleHeight, false);
			}
		}
	}

	/**
	 * Handles resize events
	 */
	private void handleResize() {
		paintCanvas.update();

		Rectangle visibleRect = paintCanvas.getClientArea();
		visibleWidth = visibleRect.width;
		visibleHeight = visibleRect.height;

		ScrollBar horizontal = paintCanvas.getHorizontalBar();
		if (horizontal != null) {
			displayFDC.xOffset = Math.min(horizontal.getSelection(), imageWidth - visibleWidth);
			if (imageWidth <= visibleWidth) {
				horizontal.setEnabled(false);
				horizontal.setSelection(0);
			} else {
				horizontal.setEnabled(true);
				horizontal.setValues(displayFDC.xOffset, 0, imageWidth, visibleWidth,
					8, visibleWidth);
			}
		}

		ScrollBar vertical = paintCanvas.getVerticalBar();
		if (vertical != null) {
			displayFDC.yOffset = Math.min(vertical.getSelection(), imageHeight - visibleHeight);
			if (imageHeight <= visibleHeight) {
				vertical.setEnabled(false);
				vertical.setSelection(0);
			} else {
				vertical.setEnabled(true);
				vertical.setValues(displayFDC.yOffset, 0, imageHeight, visibleHeight,
					8, visibleHeight);
			}
		}
	}

	/**
	 * Virtualizes MouseEvent coordinates and stores the current position.
	 */
	private void processMouseEventCoordinates(MouseEvent event) {
		currentPosition.x = event.x =
			Math.min(Math.max(event.x, 0), visibleWidth - 1) + displayFDC.xOffset;
		currentPosition.y = event.y =
			Math.min(Math.max(event.y, 0), visibleHeight - 1) + displayFDC.yOffset;
	}

	/**
	 * Clears the status bar.
	 */
	public void clearStatus() {
		statusActionInfo = "";
		statusMessageInfo = "";
		statusCoordInfo = "";
		updateStatus();
	}

	/**
	 * Sets the status bar action text.
	 *
	 * @param action the action in progress, null to clear
	 */
	public void setStatusAction(String action) {
		statusActionInfo = (action != null) ? action : "";
		updateStatus();
	}

	/**
	 * Sets the status bar message text.
	 *
	 * @param message the message to display, null to clear
	 */
	public void setStatusMessage(String message) {
		statusMessageInfo = (message != null) ? message : "";
		updateStatus();
	}

	/**
	 * Sets the coordinates in the status bar.
	 *
	 * @param coord the coordinates to display, null to clear
	 */
	public void setStatusCoord(Point coord) {
		statusCoordInfo = (coord != null) ? PaintExample.getResourceString("status.Coord.format", new Object[]
			{ Integer.valueOf(coord.x), Integer.valueOf(coord.y)}) : "";
		updateStatus();
	}

	/**
	 * Sets the coordinate range in the status bar.
	 *
	 * @param a the "from" coordinate, must not be null
	 * @param b the "to" coordinate, must not be null
	 */
	public void setStatusCoordRange(Point a, Point b) {
		statusCoordInfo = PaintExample.getResourceString("status.CoordRange.format", new Object[]
			{ Integer.valueOf(a.x), Integer.valueOf(a.y), Integer.valueOf(b.x), Integer.valueOf(b.y)});
		updateStatus();
	}

	/**
	 * Updates the display.
	 */
	private void updateStatus() {
		statusText.setText(
			PaintExample.getResourceString("status.Bar.format", new Object[]
			{ statusActionInfo, statusMessageInfo, statusCoordInfo }));
	}
}
