/*******************************************************************************
 * Copyright (c) 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.internal;

import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;

/**
 * This class hold common constants and utility functions w.r.t. to SWT high DPI
 * functionality.
 * <p>
 * The {@code autoScaleUp(..)} methods convert from API coordinates (in
 * SWT points) to internal high DPI coordinates (in pixels) that interface with
 * native widgets.
 * </p>
 * <p>
 * The {@code autoScaleDown(..)} convert from high DPI pixels to API coordinates
 * (in SWT points).
 * </p>
 *
 * @since 3.105
 */
public class DPIUtil {

	private static final int DPI_ZOOM_100 = 96;

	private static int deviceZoom = 100;
	private static int nativeDeviceZoom = 100;

	private static enum AutoScaleMethod { AUTO, NEAREST, SMOOTH }
	private static AutoScaleMethod autoScaleMethodSetting = AutoScaleMethod.AUTO;
	private static AutoScaleMethod autoScaleMethod = AutoScaleMethod.NEAREST;

	private static String autoScaleValue;
	private static boolean useCairoAutoScale = false;

	/**
	 * System property that controls the autoScale functionality.
	 * <ul>
	 * <li><b>false</b>: deviceZoom is set to 100%</li>
	 * <li><b>integer</b>: deviceZoom depends on the current display resolution,
	 *     but only uses integer multiples of 100%. The detected native zoom is
	 *     generally rounded down (e.g. at 150%, will use 100%), unless close to
	 *     the next integer multiple (currently at 175%, will use 200%).</li>
	 * <li><b>integer200</b>: like <b>integer</b>, but the maximal zoom level is 200%.</li>
	 * <li><b>quarter</b>: deviceZoom depends on the current display resolution,
	 *     but only uses integer multiples of 25%. The detected native zoom is
	 *     rounded to the closest permissible value.</li>
	 * <li><b>exact</b>: deviceZoom uses the native zoom (with 1% as minimal
	 *     step).</li>
	 * <li><i>&lt;value&gt;</i>: deviceZoom uses the given integer value in
	 *     percent as zoom level.</li>
	 * </ul>
	 * The current default is "integer200".
	 */
	private static final String SWT_AUTOSCALE = "swt.autoScale";

	/**
	 * System property that controls the method for scaling images:
	 * <ul>
	 * <li>"nearest": nearest-neighbor interpolation, may look jagged</li>
	 * <li>"smooth": smooth edges, may look blurry</li>
	 * </ul>
	 * The current default is to use "nearest", except on
	 * GTK when the deviceZoom is not an integer multiple of 100%.
	 * The smooth strategy currently doesn't work on Win32 and Cocoa, see
	 * <a href="https://bugs.eclipse.org/493455">bug 493455</a>.
	 */
	private static final String SWT_AUTOSCALE_METHOD = "swt.autoScale.method";
	static {
		autoScaleValue = System.getProperty (SWT_AUTOSCALE);

		String value = System.getProperty (SWT_AUTOSCALE_METHOD);
		if (value != null) {
			if (AutoScaleMethod.NEAREST.name().equalsIgnoreCase(value)) {
				autoScaleMethod = autoScaleMethodSetting = AutoScaleMethod.NEAREST;
			} else if (AutoScaleMethod.SMOOTH.name().equalsIgnoreCase(value)) {
				autoScaleMethod = autoScaleMethodSetting = AutoScaleMethod.SMOOTH;
			}
		}
	}

/**
 * Auto-scale down ImageData
 */
public static ImageData autoScaleDown (Device device, final ImageData imageData) {
	if (deviceZoom == 100 || imageData == null || (device != null && !device.isAutoScalable())) return imageData;
	float scaleFactor = 1.0f / getScalingFactor ();
	return autoScaleImageData(device, imageData, scaleFactor);
}

public static int[] autoScaleDown(int[] pointArray) {
	if (deviceZoom == 100 || pointArray == null) return pointArray;
	float scaleFactor = getScalingFactor ();
	int [] returnArray = new int[pointArray.length];
	for (int i = 0; i < pointArray.length; i++) {
		returnArray [i] =  Math.round (pointArray [i] / scaleFactor);
	}
	return returnArray;
}

public static int[] autoScaleDown(Drawable drawable, int[] pointArray) {
	if (drawable != null && !drawable.isAutoScalable ()) return pointArray;
	return autoScaleDown (pointArray);
}

/**
 * Auto-scale up float array dimensions.
 */
public static float[] autoScaleDown (float size[]) {
	if (deviceZoom == 100 || size == null) return size;
	float scaleFactor = getScalingFactor ();
	float scaledSize[] = new float[size.length];
	for (int i = 0; i < scaledSize.length; i++) {
		scaledSize[i] = size[i] / scaleFactor;
	}
	return scaledSize;
}

/**
 * Auto-scale up float array dimensions if enabled for Drawable class.
 */
public static float[] autoScaleDown (Drawable drawable, float size[]) {
	if (drawable != null && !drawable.isAutoScalable ()) return size;
	return autoScaleDown (size);
}

/**
 * Auto-scale down int dimensions.
 */
public static int autoScaleDown (int size) {
	if (deviceZoom == 100 || size == SWT.DEFAULT) return size;
	float scaleFactor = getScalingFactor ();
	return Math.round (size / scaleFactor);
}
/**
 * Auto-scale down int dimensions if enabled for Drawable class.
 */
public static int autoScaleDown (Drawable drawable, int size) {
	if (drawable != null && !drawable.isAutoScalable ()) return size;
	return autoScaleDown (size);
}

/**
 * Auto-scale down float dimensions.
 */
public static float autoScaleDown (float size) {
	if (deviceZoom == 100 || size == SWT.DEFAULT) return size;
	float scaleFactor = getScalingFactor ();
	return (size / scaleFactor);
}

/**
 * Auto-scale down float dimensions if enabled for Drawable class.
 */
public static float autoScaleDown (Drawable drawable, float size) {
	if (drawable != null && !drawable.isAutoScalable ()) return size;
	return autoScaleDown (size);
}

/**
 * Returns a new scaled down Point.
 */
public static Point autoScaleDown (Point point) {
	if (deviceZoom == 100 || point == null) return point;
	float scaleFactor = getScalingFactor ();
	Point scaledPoint = new Point (0,0);
	scaledPoint.x = Math.round (point.x / scaleFactor);
	scaledPoint.y = Math.round (point.y / scaleFactor);
	return scaledPoint;
}

/**
 * Returns a new scaled down Point if enabled for Drawable class.
 */
public static Point autoScaleDown (Drawable drawable, Point point) {
	if (drawable != null && !drawable.isAutoScalable ()) return point;
	return autoScaleDown (point);
}

/**
 * Returns a new scaled down Rectangle.
 */
public static Rectangle autoScaleDown (Rectangle rect) {
	if (deviceZoom == 100 || rect == null) return rect;
	Rectangle scaledRect = new Rectangle (0,0,0,0);
	Point scaledTopLeft = DPIUtil.autoScaleDown (new Point (rect.x, rect.y));
	Point scaledBottomRight = DPIUtil.autoScaleDown (new Point (rect.x + rect.width, rect.y + rect.height));

	scaledRect.x = scaledTopLeft.x;
	scaledRect.y = scaledTopLeft.y;
	scaledRect.width = scaledBottomRight.x - scaledTopLeft.x;
	scaledRect.height = scaledBottomRight.y - scaledTopLeft.y;
	return scaledRect;
}
/**
 * Returns a new scaled down Rectangle if enabled for Drawable class.
 */
public static Rectangle autoScaleDown (Drawable drawable, Rectangle rect) {
	if (drawable != null && !drawable.isAutoScalable ()) return rect;
	return autoScaleDown (rect);
}

/**
 * Auto-scale image with ImageData
 */
public static ImageData autoScaleImageData (Device device, final ImageData imageData, int targetZoom, int currentZoom) {
	if (imageData == null || targetZoom == currentZoom || (device != null && !device.isAutoScalable())) return imageData;
	float scaleFactor = (float) targetZoom / (float) currentZoom;
	return autoScaleImageData(device, imageData, scaleFactor);
}

private static ImageData autoScaleImageData (Device device, final ImageData imageData, float scaleFactor) {
	// Guards are already implemented in callers: if (deviceZoom == 100 || imageData == null || scaleFactor == 1.0f) return imageData;
	int width = imageData.width;
	int height = imageData.height;
	int scaledWidth = Math.round ((float) width * scaleFactor);
	int scaledHeight = Math.round ((float) height * scaleFactor);
	switch (autoScaleMethod) {
	case SMOOTH:
		Image original = new Image (device, (ImageDataProvider) zoom -> imageData);

		/* Create a 24 bit image data with alpha channel */
		final ImageData resultData = new ImageData (scaledWidth, scaledHeight, 24, new PaletteData (0xFF, 0xFF00, 0xFF0000));
		resultData.alphaData = new byte [scaledWidth * scaledHeight];

		Image resultImage = new Image (device, (ImageDataProvider) zoom -> resultData);
		GC gc = new GC (resultImage);
		gc.setAntialias (SWT.ON);
		gc.drawImage (original, 0, 0, DPIUtil.autoScaleDown (width), DPIUtil.autoScaleDown (height),
				/* E.g. destWidth here is effectively DPIUtil.autoScaleDown (scaledWidth), but avoiding rounding errors.
				 * Nevertheless, we still have some rounding errors due to the point-based API GC#drawImage(..).
				 */
				0, 0, Math.round (DPIUtil.autoScaleDown ((float) width * scaleFactor)), Math.round (DPIUtil.autoScaleDown ((float) height * scaleFactor)));
		gc.dispose ();
		original.dispose ();
		ImageData result = resultImage.getImageData (DPIUtil.getDeviceZoom ());
		resultImage.dispose ();
		return result;
	case NEAREST:
	default:
		return imageData.scaledTo (scaledWidth, scaledHeight);
	}
}

/**
 * Returns a new rectangle as per the scaleFactor.
 */
public static Rectangle autoScaleBounds (Rectangle rect, int targetZoom, int currentZoom) {
	if (deviceZoom == 100 || rect == null || targetZoom == currentZoom) return rect;
	float scaleFactor = ((float)targetZoom) / (float)currentZoom;
	Rectangle returnRect = new Rectangle (0,0,0,0);
	returnRect.x = Math.round (rect.x * scaleFactor);
	returnRect.y = Math.round (rect.y * scaleFactor);
	returnRect.width = Math.round (rect.width * scaleFactor);
	returnRect.height = Math.round (rect.height * scaleFactor);
	return returnRect;
}

/**
 * Auto-scale up ImageData
 */
public static ImageData autoScaleUp (Device device, final ImageData imageData) {
	if (deviceZoom == 100 || imageData == null || (device != null && !device.isAutoScalable())) return imageData;
	float scaleFactor = deviceZoom / 100f;
	return autoScaleImageData(device, imageData, scaleFactor);
}

public static int[] autoScaleUp(int[] pointArray) {
	if (deviceZoom == 100 || pointArray == null) return pointArray;
	float scaleFactor = getScalingFactor ();
	int [] returnArray = new int[pointArray.length];
	for (int i = 0; i < pointArray.length; i++) {
		returnArray [i] =  Math.round (pointArray [i] * scaleFactor);
	}
	return returnArray;
}

public static int[] autoScaleUp(Drawable drawable, int[] pointArray) {
	if (drawable != null && !drawable.isAutoScalable ()) return pointArray;
	return autoScaleUp (pointArray);
}

/**
 * Auto-scale up int dimensions.
 */
public static int autoScaleUp (int size) {
	if (deviceZoom == 100 || size == SWT.DEFAULT) return size;
	float scaleFactor = getScalingFactor ();
	return Math.round (size * scaleFactor);
}

/**
 * Auto-scale up int dimensions using Native DPI
 */
public static int autoScaleUpUsingNativeDPI (int size) {
	if (nativeDeviceZoom == 100 || size == SWT.DEFAULT) return size;
	float nativeScaleFactor = nativeDeviceZoom / 100f;
	return Math.round (size * nativeScaleFactor);
}

/**
 * Auto-scale up int dimensions if enabled for Drawable class.
 */
public static int autoScaleUp (Drawable drawable, int size) {
	if (drawable != null && !drawable.isAutoScalable ()) return size;
	return autoScaleUp (size);
}

public static float autoScaleUp(float size) {
	if (deviceZoom == 100 || size == SWT.DEFAULT) return size;
	float scaleFactor = getScalingFactor ();
	return (size * scaleFactor);
}

public static float autoScaleUp(Drawable drawable, float size) {
	if (drawable != null && !drawable.isAutoScalable ()) return size;
	return autoScaleUp (size);
}

/**
 * Returns a new scaled up Point.
 */
public static Point autoScaleUp (Point point) {
	if (deviceZoom == 100 || point == null) return point;
	float scaleFactor = getScalingFactor ();
	Point scaledPoint = new Point (0,0);
	scaledPoint.x = Math.round (point.x * scaleFactor);
	scaledPoint.y = Math.round (point.y * scaleFactor);
	return scaledPoint;
}

/**
 * Returns a new scaled up Point if enabled for Drawable class.
 */
public static Point autoScaleUp (Drawable drawable, Point point) {
	if (drawable != null && !drawable.isAutoScalable ()) return point;
	return autoScaleUp (point);
}

/**
 * Returns a new scaled up Rectangle.
 */
public static Rectangle autoScaleUp (Rectangle rect) {
	if (deviceZoom == 100 || rect == null) return rect;
	Rectangle scaledRect = new Rectangle (0,0,0,0);
	Point scaledTopLeft = DPIUtil.autoScaleUp (new Point (rect.x, rect.y));
	Point scaledBottomRight = DPIUtil.autoScaleUp (new Point (rect.x + rect.width, rect.y + rect.height));

	scaledRect.x = scaledTopLeft.x;
	scaledRect.y = scaledTopLeft.y;
	scaledRect.width = scaledBottomRight.x - scaledTopLeft.x;
	scaledRect.height = scaledBottomRight.y - scaledTopLeft.y;
	return scaledRect;
}

/**
 * Returns a new scaled up Rectangle if enabled for Drawable class.
 */
public static Rectangle autoScaleUp (Drawable drawable, Rectangle rect) {
	if (drawable != null && !drawable.isAutoScalable ()) return rect;
	return autoScaleUp (rect);
}

/**
 * Returns Scaling factor from the display
 * @return float scaling factor
 */
private static float getScalingFactor () {
	if (useCairoAutoScale) {
		return 1;
	}
	return deviceZoom / 100f;
}

/**
 * Compute the zoom value based on the DPI value.
 *
 * @return zoom
 */
public static int mapDPIToZoom (int dpi) {
	double zoom = (double) dpi * 100 / DPI_ZOOM_100;
	int roundedZoom = (int) Math.round (zoom);
	return roundedZoom;
}
/**
 * Gets Image data at specified zoom level, if image is missing then
 * fall-back to 100% image. If provider or fall-back image is not available,
 * throw error.
 */
public static ImageData validateAndGetImageDataAtZoom (ImageDataProvider provider, int zoom, boolean[] found) {
	if (provider == null) SWT.error (SWT.ERROR_NULL_ARGUMENT);
	ImageData data = provider.getImageData (zoom);
	found [0] = (data != null);
	/* If image is null when (zoom != 100%), fall-back to image at 100% zoom */
	if (zoom != 100 && !found [0]) data = provider.getImageData (100);
	if (data == null) SWT.error (SWT.ERROR_INVALID_ARGUMENT, null, ": ImageDataProvider [" + provider + "] returns null ImageData at 100% zoom.");
	return data;
}

/**
 * Gets Image file path at specified zoom level, if image is missing then
 * fall-back to 100% image. If provider or fall-back image is not available,
 * throw error.
 */
public static String validateAndGetImagePathAtZoom (ImageFileNameProvider provider, int zoom, boolean[] found) {
	if (provider == null) SWT.error (SWT.ERROR_NULL_ARGUMENT);
	String filename = provider.getImagePath (zoom);
	found [0] = (filename != null);
	/* If image is null when (zoom != 100%), fall-back to image at 100% zoom */
	if (zoom != 100 && !found [0]) filename = provider.getImagePath (100);
	if (filename == null) SWT.error (SWT.ERROR_INVALID_ARGUMENT, null, ": ImageFileNameProvider [" + provider + "] returns null filename at 100% zoom.");
	return filename;
}

public static int getDeviceZoom() {
	return deviceZoom;
}

public static void setDeviceZoom (int nativeDeviceZoom) {
	DPIUtil.nativeDeviceZoom = nativeDeviceZoom;
	int deviceZoom = getZoomForAutoscaleProperty (nativeDeviceZoom);

	DPIUtil.deviceZoom = deviceZoom;
	System.setProperty("org.eclipse.swt.internal.deviceZoom", Integer.toString(deviceZoom));
	if (deviceZoom != 100 && autoScaleMethodSetting == AutoScaleMethod.AUTO) {
		if (deviceZoom / 100 * 100 == deviceZoom || !"gtk".equals(SWT.getPlatform())) {
			autoScaleMethod = AutoScaleMethod.NEAREST;
		} else {
			autoScaleMethod = AutoScaleMethod.SMOOTH;
		}
	}
}

public static void setUseCairoAutoScale (boolean cairoAutoScale) {
	useCairoAutoScale = cairoAutoScale;
}

public static boolean useCairoAutoScale() {
	return useCairoAutoScale;
}

public static int getZoomForAutoscaleProperty (int nativeDeviceZoom) {
	int zoom = 0;
 	if (autoScaleValue != null) {
		if ("false".equalsIgnoreCase (autoScaleValue)) {
			zoom = 100;
		} else if ("quarter".equalsIgnoreCase (autoScaleValue)) {
			zoom = (int) (Math.round (nativeDeviceZoom / 25f) * 25);
		} else if ("exact".equalsIgnoreCase (autoScaleValue)) {
			zoom = nativeDeviceZoom;
		} else {
			try {
				int zoomValue = Integer.parseInt (autoScaleValue);
				zoom = Math.max (Math.min (zoomValue, 1600), 25);
			} catch (NumberFormatException e) {
				// unsupported value, use default
			}
		}
	}
 	if (zoom == 0) { // || "integer".equalsIgnoreCase (value) || "integer200".equalsIgnoreCase (value)
		zoom = Math.max ((nativeDeviceZoom + 25) / 100 * 100, 100);
		if (!"integer".equalsIgnoreCase(autoScaleValue)) {
			// integer200, or default
			zoom = Math.min (zoom, 200);
		}
	}
	return zoom;
}

/**
 * AutoScale ImageDataProvider.
 */
public static final class AutoScaleImageDataProvider implements ImageDataProvider {
	Device device;
	ImageData imageData;
	int currentZoom;
	public AutoScaleImageDataProvider(Device device, ImageData data, int zoom){
		this.device = device;
		this.imageData = data;
		this.currentZoom = zoom;
	}
	@Override
	public ImageData getImageData(int zoom) {
		return DPIUtil.autoScaleImageData(device, imageData, zoom, currentZoom);
	}
}
}
