/*******************************************************************************
 * Copyright (c) 2004, 2023 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
 *     Stefan Nöbauer - Bug 553765
 *******************************************************************************/
package org.eclipse.jface.tests.images;

import java.util.Objects;

import org.eclipse.jface.resource.ColorDescriptor;
import org.eclipse.jface.resource.DeviceResourceDescriptor;
import org.eclipse.jface.resource.DeviceResourceException;
import org.eclipse.jface.resource.DeviceResourceManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.resource.ResourceLocator;
import org.eclipse.jface.resource.ResourceManager;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageDataProvider;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Display;
import org.junit.Assert;

import junit.framework.TestCase;

/**
 * @since 3.1
 */
public class ResourceManagerTest extends TestCase {

	private DeviceResourceDescriptor<?>[] descriptors;
	private Image testImage;
	private Image testImage2;
	private Color testColor;
	private Color testColor2;
	private int numDupes;

	private DeviceResourceManager globalResourceManager;

	private static ImageDescriptor getImage(String path) {
		return ResourceLocator.imageDescriptorFromBundle("org.eclipse.jface.tests", path).orElse(null);
	}

	private static final class TestDescriptor<R> extends DeviceResourceDescriptor<R> {
		DeviceResourceDescriptor<R> toWrap;
		public static int refCount = 0;

		public TestDescriptor(DeviceResourceDescriptor<R> toWrap) {
			this.toWrap = toWrap;
		}

		@Override
		public Object createResource(Device device) throws DeviceResourceException {
			Object result = toWrap.createResource(device);
			refCount++;
			return result;
		}

		@Override
		public void destroyResource(Object previouslyCreatedObject) {
			refCount--;
			toWrap.destroyResource(previouslyCreatedObject);
		}

		@Override
		public boolean equals(Object arg0) {
			if (arg0 instanceof TestDescriptor td) {
				return td.toWrap.equals(toWrap);
			}

			return false;
		}

		@Override
		public int hashCode() {
			return toWrap.hashCode();
		}
	}

	@Override
	protected void setUp() throws Exception {
		super.setUp();
		TestDescriptor.refCount = 0;
		Display display = Display.getCurrent();
		globalResourceManager = new DeviceResourceManager(display);
		testImage = getImage("icons/anything.gif").createImage(display);
		testImage2 = getImage("icons/binary_co.gif").createImage(display);
		testColor = new Color(display, new RGB(10, 40, 20));
		testColor2 = new Color(display, new RGB(230, 100, 26));

		// Array of resource descriptors containing at least one duplicate of each type.
		// If you modify this array, be sure to adjust numDupes as well. Note that some
		// tests index the array directly, so it is a good idea to always add to this
		// array rather than remove from it.
		descriptors = new DeviceResourceDescriptor[] { new TestDescriptor<>(getImage("icons/anything.gif")),
				new TestDescriptor<>(getImage("icons/anything.gif")),
				new TestDescriptor<>(getImage("icons/binary_co.gif")),
				new TestDescriptor<>(getImage("icons/binary_co.gif")),
				new TestDescriptor<>(getImage("icons/mockeditorpart1.gif")),

				new TestDescriptor<>(getImage("icons/view.gif")), // 5
				new TestDescriptor<>(ImageDescriptor.createFromImage(testImage2)),
				new TestDescriptor<>(ImageDescriptor.createFromImage(testImage)),
				new TestDescriptor<>(ImageDescriptor.createFromImage(testImage)),
				new TestDescriptor<>(ImageDescriptor.createFromImage(testImage)),

				new TestDescriptor<>(ImageDescriptor.createFromImage(testImage)), // 10
				new TestDescriptor<>(ImageDescriptor.createFromImage(testImage2)),
				new TestDescriptor<>(ColorDescriptor.createFrom(new RGB(10, 200, 54))),
				new TestDescriptor<>(ColorDescriptor.createFrom(new RGB(10, 200, 54))),
				new TestDescriptor<>(ColorDescriptor.createFrom(new RGB(200, 220, 54))),

				new TestDescriptor<>(ColorDescriptor.createFrom(testColor)), // 15
				new TestDescriptor<>(ColorDescriptor.createFrom(testColor)),
				new TestDescriptor<>(ColorDescriptor.createFrom(testColor2)),
				new TestDescriptor<>(ColorDescriptor.createFrom(testColor)),
				new TestDescriptor<>(ColorDescriptor.createFrom(testColor)),

				new TestDescriptor<>(ColorDescriptor.createFrom(testColor2)), // 20
				new TestDescriptor<>(ImageDescriptor.createFromImageDataProvider(new MyImageDataProvider("1"))),
				new TestDescriptor<>(ImageDescriptor.createFromImageDataProvider(new MyImageDataProvider("1"))),
				new TestDescriptor<>(ImageDescriptor.createFromImageDataProvider(new MyImageDataProvider("2"))),

		};

		// Let the tests know how many duplicates are in the array
		numDupes = 12;
	}

	@Override
	protected void tearDown() throws Exception {
		super.tearDown();
		globalResourceManager.dispose();
		Assert.assertEquals("Detected leaks", 0, TestDescriptor.refCount);
		testImage.dispose();
		testImage2.dispose();
	}

	protected void validateResource(Object resource) throws Exception {
		Assert.assertNotNull("Allocated resource was null", resource);
		if (resource instanceof Image image) {
			assertFalse("Image is disposed", image.isDisposed());
			return;
		}
	}

	/**
	 * Tests that the descriptors themselves w
	 *
	 * @throws Exception
	 */
	public void testDescriptorAllocations() throws Exception {
		Display display = Display.getCurrent();

		// Allocate resources directly using the descriptors.
		Object[] resources = new Object[descriptors.length];

		for (int i = 0; i < descriptors.length; i++) {
			DeviceResourceDescriptor<?> next = descriptors[i];

			Object resource = next.createResource(display);
			// Ensure that this resource was allocated correctly
			validateResource(resource);
			resources[i] = resource;
		}

		// Allocating resources without the manager will not reuse duplicates, so we
		// should get exactly one resource per descriptor.

		Assert.assertEquals("Expecting one resource to be allocated per descriptor", descriptors.length,
				TestDescriptor.refCount);

		// Deallocate resources directly using the descriptors
		for (int i = 0; i < descriptors.length; i++) {
			DeviceResourceDescriptor<?> next = descriptors[i];

			next.destroyResource(resources[i]);
		}
	}

	public void testDeviceManagerAllocations() throws Exception {

		// Allocate resources directly using the descriptors.
		Object[] resources = new Object[descriptors.length];

		for (int i = 0; i < descriptors.length; i++) {
			DeviceResourceDescriptor<?> next = descriptors[i];

			Object resource = globalResourceManager.create(next);
			// Ensure that this resource was allocated correctly
			validateResource(resource);
			resources[i] = resource;
		}

		Assert.assertEquals(
				"Descriptors created from Images should not reallocate Images when the original can be reused",
				testImage, resources[9]);

		// Allocating resources without the manager will reuse duplicates, so the number
		// of resources should equal the number of unique descriptors.

		Assert.assertEquals("Duplicate descriptors should be reused", descriptors.length - numDupes,
				TestDescriptor.refCount);

		// Deallocate resources directly using the descriptors
		for (DeviceResourceDescriptor<?> next : descriptors) {
			globalResourceManager.destroy(next);
		}
	}

	private void allocateResources(ResourceManager mgr, int[] toAllocate) throws Exception {
		for (int j : toAllocate) {
			validateResource(mgr.create(descriptors[j]));
		}
	}

	private void deallocateResources(ResourceManager mgr, int[] toDeallocate) {
		for (int j : toDeallocate) {
			mgr.destroy(descriptors[j]);
		}
	}

	public void testLocalManagerAllocations() throws Exception {
		// These arrays are indices into the descriptors array. For example, {0,1,7}
		// is a quick shorthand to indicate we should allocate resources 0, 1, and 7.
		int[] gResources = { 0, 1, 5, 3, 7, 12, 13, 14 };
		int[] lm1Resources = { 0, 2, 3, 4, 11, 12, 13, 15 };
		int[] lm2Resources = { 0, 1, 6, 7, 8, 12, 14, 16 };

		LocalResourceManager lm1 = new LocalResourceManager(globalResourceManager);
		LocalResourceManager lm2 = new LocalResourceManager(globalResourceManager);

		// Allocate a bunch of global resources
		allocateResources(globalResourceManager, gResources);

		// Remember how many global resources were allocated
		int initialCount = TestDescriptor.refCount;

		// Allocate a bunch of resources in lm1
		allocateResources(lm1, lm1Resources);

		// Remember how many global + lm1 resources there are
		int lm1Count = TestDescriptor.refCount;

		// Allocate and deallocate a bunch of resources in lm2
		// Ensure that all the lm2 resources were deallocated (we should be
		// back down to the count we had after we allocated lm1 resources)
		allocateResources(lm2, lm2Resources);
		deallocateResources(lm2, lm2Resources);
		Assert.assertEquals(lm1Count, TestDescriptor.refCount);

		// Dispose lm2 (shouldn't do anything since we already deallocated all of its
		// resources)
		// Ensure that this doesn't change any refcounts
		lm2.dispose();
		Assert.assertEquals(lm1Count, TestDescriptor.refCount);

		// Dispose lm1 without first deallocating its resources. This should correctly
		// detect
		// what needs to be deallocated from lm1 without affecting any global resources.
		// If everything works, we should be back to the resource count we had after
		// allocating
		// the global resources.
		lm1.dispose();
		Assert.assertEquals(initialCount, TestDescriptor.refCount);

	}

	public void testImageDataResourceAllocations() throws Exception {
		// These arrays are indices into the descriptors array. For example, {0,1,7}
		// is a quick shorthand to indicate we should allocate resources 0, 1, and 7.
		int[] gResources = { 21, 22, 23 };

		// Allocate a bunch of global resources
		allocateResources(globalResourceManager, gResources);

		Assert.assertEquals(2, TestDescriptor.refCount);
	}

	/*
	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=135088
	 */
	public void testResourceManagerFind() throws Exception {
		DeviceResourceDescriptor<?> descriptor = descriptors[0];
		Object resource = globalResourceManager.find(descriptor);
		assertNull("Resource should be null since it is not allocated", resource);
		resource = globalResourceManager.create(descriptor);

		// Ensure that the resource was allocated correctly
		validateResource(resource);

		// Now the resource should be found
		Object foundResource = globalResourceManager.find(descriptor);
		assertNotNull("Found resource should not be null", foundResource);
		assertTrue("Found resource should be an image", foundResource instanceof Image);

		// Destroy the resource we created
		globalResourceManager.destroy(descriptor);
	}

	/**
	 * ImageDataProvider to identify identical ImageData by the given ID.
	 */
	private static class MyImageDataProvider implements ImageDataProvider {
		private final String id;

		public MyImageDataProvider(String id) {
			this.id = id;
		}

		@Override
		public ImageData getImageData(int zoom) {
			PaletteData paletteData = new PaletteData(new RGB(255, 0, 0), new RGB(0, 255, 0));
			ImageData imageData = new ImageData(48, 48, 1, paletteData);
			for (int x = 11; x < 35; x++) {
				for (int y = 11; y < 35; y++) {
					imageData.setPixel(x, y, 1);
				}
			}

			return imageData;
		}

		@Override
		public int hashCode() {
			return Objects.hashCode(id);
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			if (obj == null) {
				return false;
			}
			if (getClass() != obj.getClass()) {
				return false;
			}
			MyImageDataProvider other = (MyImageDataProvider) obj;
			return Objects.equals(id, other.id);
		}

	}
}
