/*******************************************************************************
 * Copyright (c) 2000, 2025 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.image;


import java.io.*;

import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.internal.image.FileFormat.*;

public final class GIFFileFormat extends StaticImageFileFormat {
	String signature;
	int screenWidth, screenHeight, backgroundPixel, bitsPerPixel, defaultDepth;
	int disposalMethod = 0;
	int delayTime = 0;
	int transparentPixel = -1;
	int repeatCount = 1;

	static final int GIF_APPLICATION_EXTENSION_BLOCK_ID = 0xFF;
	static final int GIF_GRAPHICS_CONTROL_BLOCK_ID = 0xF9;
	static final int GIF_PLAIN_TEXT_BLOCK_ID = 0x01;
	static final int GIF_COMMENT_BLOCK_ID = 0xFE;
	static final int GIF_EXTENSION_BLOCK_ID = 0x21;
	static final int GIF_IMAGE_BLOCK_ID = 0x2C;
	static final int GIF_TRAILER_ID = 0x3B;
	static final byte [] GIF89a = new byte[] { (byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'9', (byte)'a' };
	static final byte [] NETSCAPE2_0 = new byte[] { (byte)'N', (byte)'E', (byte)'T', (byte)'S', (byte)'C', (byte)'A', (byte)'P', (byte)'E', (byte)'2', (byte)'.', (byte)'0' };

	/**
	 * Answer a palette containing numGrays
	 * shades of gray, ranging from black to white.
	 */
	static PaletteData grayRamp(int numGrays) {
		int n = numGrays - 1;
		RGB[] colors = new RGB[numGrays];
		for (int i = 0; i < numGrays; i++) {
			int intensity = (byte)((i * 3) * 256 / n);
			colors[i] = new RGB(intensity, intensity, intensity);
		}
		return new PaletteData(colors);
	}

	@Override
	boolean isFileFormat(LEDataInputStream stream) throws IOException {
		byte[] signature = new byte[3];
		stream.read(signature);
		stream.unread(signature);
		return signature[0] == 'G' && signature[1] == 'I' && signature[2] == 'F';
	}

	/**
	 * Load the GIF image(s) stored in the input stream.
	 * Return an array of ImageData representing the image(s).
	 */
	@Override
	ImageData[] loadFromByteStream() {
		byte[] signature = new byte[3];
		byte[] versionBytes = new byte[3];
		byte[] block = new byte[7];
		try {
			inputStream.read(signature);
			if (!(signature[0] == 'G' && signature[1] == 'I' && signature[2] == 'F'))
				SWT.error(SWT.ERROR_INVALID_IMAGE);

			inputStream.read(versionBytes);

			inputStream.read(block);
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
		screenWidth = (block[0] & 0xFF) | ((block[1] & 0xFF) << 8);
		loader.logicalScreenWidth = screenWidth;
		screenHeight = (block[2] & 0xFF) | ((block[3] & 0xFF) << 8);
		loader.logicalScreenHeight = screenHeight;
		byte bitField = block[4];
		backgroundPixel = block[5] & 0xFF;
		//aspect = block[6] & 0xFF;
		bitsPerPixel = ((bitField >> 4) & 0x07) + 1;
		defaultDepth = (bitField & 0x7) + 1;
		PaletteData palette = null;
		if ((bitField & 0x80) != 0) {
			// Global palette.
			//sorted = (bitField & 0x8) != 0;
			palette = readPalette(1 << defaultDepth);
		} else {
			// No global palette.
			//sorted = false;
			backgroundPixel = -1;
			defaultDepth = bitsPerPixel;
		}
		loader.backgroundPixel = backgroundPixel;

		ImageData[] images = new ImageData[0];
		int id = readID();
		while (id != GIF_TRAILER_ID && id != -1) {
			if (id == GIF_IMAGE_BLOCK_ID) {
				ImageData image = readImageBlock(palette);
				if (loader.hasListeners()) {
					loader.notifyListeners(new ImageLoaderEvent(loader, image, 3, true));
				}
				ImageData[] oldImages = images;
				images = new ImageData[oldImages.length + 1];
				System.arraycopy(oldImages, 0, images, 0, oldImages.length);
				images[images.length - 1] = image;
			} else if (id == GIF_EXTENSION_BLOCK_ID) {
				/* Read the extension block. Currently, only the
				 * interesting parts of certain extensions are kept,
				 * and the rest is discarded. In future, if we want
				 * to keep extensions, they should be grouped with
				 * the image data before which they appear.
				 */
				readExtension();
			} else {
				/* The GIF is not to spec, but try to salvage it
				 * if we read at least one image. */
				if (images.length > 0) break;
				SWT.error(SWT.ERROR_INVALID_IMAGE);
			}
			id = readID(); // block terminator (0)
			if (id == 0) id = readID(); // next block ID (unless we just read it)
		}
		return images;
	}

	/**
	 * Read and return the next block or extension identifier from the file.
	 */
	int readID() {
		try {
			return inputStream.read();
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
		return -1;
	}

	/**
	 * Read a control extension.
	 * Return the extension block data.
	 */
	byte[] readExtension() {
		int extensionID = readID();
		if (extensionID == GIF_COMMENT_BLOCK_ID)
			return readCommentExtension();
		if (extensionID == GIF_PLAIN_TEXT_BLOCK_ID)
			return readPlainTextExtension();
		if (extensionID == GIF_GRAPHICS_CONTROL_BLOCK_ID)
			return readGraphicsControlExtension();
		if (extensionID == GIF_APPLICATION_EXTENSION_BLOCK_ID)
			return readApplicationExtension();
		// Otherwise, we don't recognize the block. If the
		// field size is correct, we can just skip over
		// the block contents.
		try {
			int extSize = inputStream.read();
			if (extSize < 0) {
				SWT.error(SWT.ERROR_INVALID_IMAGE);
			}
			byte[] ext = new byte[extSize];
			inputStream.read(ext, 0, extSize);
			return ext;
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
			return null;
		}
	}

	/**
	 * We have just read the Comment extension identifier
	 * from the input stream. Read in the rest of the comment
	 * and return it. GIF comment blocks are variable size.
	 */
	byte[] readCommentExtension() {
		try {
			byte[] comment = new byte[0];
			byte[] block = new byte[255];
			int size = inputStream.read();
			while ((size > 0) && (inputStream.read(block, 0, size) != -1)) {
				byte[] oldComment = comment;
				comment = new byte[oldComment.length + size];
				System.arraycopy(oldComment, 0, comment, 0, oldComment.length);
				System.arraycopy(block, 0, comment, oldComment.length, size);
				size = inputStream.read();
			}
			return comment;
		} catch (Exception e) {
			SWT.error(SWT.ERROR_IO, e);
			return null;
		}
	}

	/**
	 * We have just read the PlainText extension identifier
	 * from the input stream. Read in the plain text info and text,
	 * and return the text. GIF plain text blocks are variable size.
	 */
	byte[] readPlainTextExtension() {
		try {
			// Read size of block = 0x0C.
			inputStream.read();
			// Read the text information (x, y, width, height, colors).
			byte[] info = new byte[12];
			inputStream.read(info);
			// Read the text.
			byte[] text = new byte[0];
			byte[] block = new byte[255];
			int size = inputStream.read();
			while ((size > 0) && (inputStream.read(block, 0, size) != -1)) {
				byte[] oldText = text;
				text = new byte[oldText.length + size];
				System.arraycopy(oldText, 0, text, 0, oldText.length);
				System.arraycopy(block, 0, text, oldText.length, size);
				size = inputStream.read();
			}
			return text;
		} catch (Exception e) {
			SWT.error(SWT.ERROR_IO, e);
			return null;
		}
	}

	/**
	 * We have just read the GraphicsControl extension identifier
	 * from the input stream. Read in the control information, store
	 * it, and return it.
	 */
	byte[] readGraphicsControlExtension() {
		try {
			// Read size of block = 0x04.
			inputStream.read();
			// Read the control block.
			byte[] controlBlock = new byte[4];
			inputStream.read(controlBlock);
			byte bitField = controlBlock[0];
			// Store the user input field.
			//userInput = (bitField & 0x02) != 0;
			// Store the disposal method.
			disposalMethod = (bitField >> 2) & 0x07;
			// Store the delay time.
			delayTime = (controlBlock[1] & 0xFF) | ((controlBlock[2] & 0xFF) << 8);
			// Store the transparent color.
			if ((bitField & 0x01) != 0) {
				transparentPixel = controlBlock[3] & 0xFF;
			} else {
				transparentPixel = -1;
			}
			return controlBlock;
		} catch (Exception e) {
			SWT.error(SWT.ERROR_IO, e);
			return null;
		}
	}

	/**
	 * We have just read the Application extension identifier
	 * from the input stream.  Read in the rest of the extension,
	 * look for and store 'number of repeats', and return the data.
	 */
	byte[] readApplicationExtension() {
		try {
			// Read block data.
			int blockSize = inputStream.read();
			byte[] blockData = new byte[blockSize];
			inputStream.read(blockData);
			// Read application data.
			byte[] data = new byte[0];
			byte[] block = new byte[255];
			int size = inputStream.read();
			while ((size > 0) && (inputStream.read(block, 0, size) != -1)) {
				byte[] oldData = data;
				data = new byte[oldData.length + size];
				System.arraycopy(oldData, 0, data, 0, oldData.length);
				System.arraycopy(block, 0, data, oldData.length, size);
				size = inputStream.read();
			}
			// Look for the NETSCAPE 'repeat count' field for an animated GIF.
			boolean netscape =
				blockSize > 7 &&
				blockData[0] == 'N' &&
				blockData[1] == 'E' &&
				blockData[2] == 'T' &&
				blockData[3] == 'S' &&
				blockData[4] == 'C' &&
				blockData[5] == 'A' &&
				blockData[6] == 'P' &&
				blockData[7] == 'E';
			boolean authentic =
				blockSize > 10 &&
				blockData[8] == '2' &&
				blockData[9] == '.' &&
				blockData[10] == '0';
			if (netscape && authentic && data[0] == 01) {
				repeatCount = (data[1] & 0xFF) | ((data[2] & 0xFF) << 8);
				loader.repeatCount = repeatCount;
			}
			return data;
		} catch (Exception e) {
			SWT.error(SWT.ERROR_IO, e);
			return null;
		}
	}

	/**
	 * Return a DeviceIndependentImage representing the
	 * image block at the current position in the input stream.
	 * Throw an error if an error occurs.
	 */
	ImageData readImageBlock(PaletteData defaultPalette) {
		int depth;
		PaletteData palette;
		byte[] block = new byte[9];
		try {
			inputStream.read(block);
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
		int left = (block[0] & 0xFF) | ((block[1] & 0xFF) << 8);
		int top = (block[2] & 0xFF) | ((block[3] & 0xFF) << 8);
		int width = (block[4] & 0xFF) | ((block[5] & 0xFF) << 8);
		int height = (block[6] & 0xFF) | ((block[7] & 0xFF) << 8);
		byte bitField = block[8];
		boolean interlaced = (bitField & 0x40) != 0;
		//boolean sorted = (bitField & 0x20) != 0;
		if ((bitField & 0x80) != 0) {
			// Local palette.
			depth = (bitField & 0x7) + 1;
			palette = readPalette(1 << depth);
		} else {
			// No local palette.
			depth = defaultDepth;
			palette = defaultPalette;
		}
		/* Work around: Ignore the case where a GIF specifies an
		 * invalid index for the transparent pixel that is larger
		 * than the number of entries in the palette. */
		if (transparentPixel > 1 << depth) {
			transparentPixel = -1;
		}
		// Promote depth to next highest supported value.
		if (!(depth == 1 || depth == 4 || depth == 8)) {
			if (depth < 4)
				depth = 4;
			else
				depth = 8;
		}
		if (palette == null) {
			palette = grayRamp(1 << depth);
		}
		int initialCodeSize = -1;
		try {
			initialCodeSize = inputStream.read();
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
		if (initialCodeSize < 0) {
			SWT.error(SWT.ERROR_INVALID_IMAGE);
		}
		ImageData image = ImageData.internal_new(
			width,
			height,
			depth,
			palette,
			4,
			null,
			0,
			null,
			null,
			-1,
			transparentPixel,
			SWT.IMAGE_GIF,
			left,
			top,
			disposalMethod,
			delayTime);
		LZWCodec codec = new LZWCodec();
		codec.decode(inputStream, loader, image, interlaced, initialCodeSize);
		return image;
	}

	/**
	 * Read a palette from the input stream.
	 */
	PaletteData readPalette(int numColors) {
		byte[] bytes = new byte[numColors * 3];
		try {
			if (inputStream.read(bytes) != bytes.length)
				SWT.error(SWT.ERROR_INVALID_IMAGE);
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
		RGB[] colors = new RGB[numColors];
		for (int i = 0; i < numColors; i++)
			colors[i] = new RGB(bytes[i*3] & 0xFF,
				bytes[i*3+1] & 0xFF, bytes[i*3+2] & 0xFF);
		return new PaletteData(colors);
	}

	@Override
	void unloadIntoByteStream(ImageLoader loader) {

		/* Step 1: Acquire GIF parameters. */
		ImageData[] data = loader.data;
		int frameCount = data.length;
		boolean multi = frameCount > 1;
		ImageData firstImage = data[0];
		int logicalScreenWidth = multi ? loader.logicalScreenWidth : firstImage.width;
		int logicalScreenHeight = multi ? loader.logicalScreenHeight : firstImage.height;
		int backgroundPixel = loader.backgroundPixel;
		int depth = firstImage.depth;
		PaletteData palette = firstImage.palette;
		RGB[] colors = palette.getRGBs();
		short globalTable = 1;

		/* Step 2: Check for validity and global/local color map. */
		if (!(depth == 1 || depth == 4 || depth == 8)) {
			SWT.error(SWT.ERROR_UNSUPPORTED_DEPTH);
		}
		for (int i=0; i<frameCount; i++) {
			if (data[i].palette.isDirect) {
				SWT.error(SWT.ERROR_INVALID_IMAGE);
			}
			if (multi) {
				if (!(data[i].height <= logicalScreenHeight && data[i].width <= logicalScreenWidth && data[i].depth == depth)) {
					SWT.error(SWT.ERROR_INVALID_IMAGE);
				}
				if (globalTable == 1) {
					RGB rgbs[] = data[i].palette.getRGBs();
					if (rgbs.length != colors.length) {
						globalTable = 0;
					} else {
						for (int j=0; j<colors.length; j++) {
							if (!(rgbs[j].red == colors[j].red &&
								rgbs[j].green == colors[j].green &&
								rgbs[j].blue == colors[j].blue)) {
								globalTable = 0;
								break;
							}
						}
					}
				}
			}
		}

		try {
			/* Step 3: Write the GIF89a Header and Logical Screen Descriptor. */
			outputStream.write(GIF89a);
			int bitField = globalTable*128 + (depth-1)*16 + depth-1;
			outputStream.writeShort((short)logicalScreenWidth);
			outputStream.writeShort((short)logicalScreenHeight);
			outputStream.write(bitField);
			outputStream.write(backgroundPixel);
			outputStream.write(0); // Aspect ratio is 1:1
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}

		/* Step 4: Write Global Color Table if applicable. */
		if (globalTable == 1) {
			writePalette(palette, depth);
		}

		/* Step 5: Write Application Extension if applicable. */
		if (multi) {
			int repeatCount = loader.repeatCount;
			try {
				outputStream.write(GIF_EXTENSION_BLOCK_ID);
				outputStream.write(GIF_APPLICATION_EXTENSION_BLOCK_ID);
				outputStream.write(NETSCAPE2_0.length);
				outputStream.write(NETSCAPE2_0);
				outputStream.write(3); // Three bytes follow
				outputStream.write(1); // Extension type
				outputStream.writeShort((short) repeatCount);
				outputStream.write(0); // Block terminator
			} catch (IOException e) {
				SWT.error(SWT.ERROR_IO, e);
			}
		}

		for (int frame=0; frame<frameCount; frame++) {

			/* Step 6: Write Graphics Control Block for each frame if applicable. */
			if (multi || data[frame].transparentPixel != -1) {
				writeGraphicsControlBlock(data[frame]);
			}

			/* Step 7: Write Image Header for each frame. */
			int x = data[frame].x;
			int y = data[frame].y;
			int width = data[frame].width;
			int height = data[frame].height;
			try {
				outputStream.write(GIF_IMAGE_BLOCK_ID);
				byte[] block = new byte[9];
				block[0] = (byte)(x & 0xFF);
				block[1] = (byte)((x >> 8) & 0xFF);
				block[2] = (byte)(y & 0xFF);
				block[3] = (byte)((y >> 8) & 0xFF);
				block[4] = (byte)(width & 0xFF);
				block[5] = (byte)((width >> 8) & 0xFF);
				block[6] = (byte)(height & 0xFF);
				block[7] = (byte)((height >> 8) & 0xFF);
				block[8] = (byte)(globalTable == 0 ? (depth-1) | 0x80 : 0x00);
				outputStream.write(block);
			} catch (IOException e) {
				SWT.error(SWT.ERROR_IO, e);
			}

			/* Step 8: Write Local Color Table for each frame if applicable. */
			if (globalTable == 0) {
				writePalette(data[frame].palette, depth);
			}

			/* Step 9: Write the actual data for each frame. */
			try {
				outputStream.write(depth); // Minimum LZW Code size
			} catch (IOException e) {
				SWT.error(SWT.ERROR_IO, e);
			}
			new LZWCodec().encode(outputStream, data[frame]);
		}

		/* Step 10: Write GIF terminator. */
		try {
			outputStream.write(0x3B);
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
	}

	/**
	 * Write out a GraphicsControlBlock to describe
	 * the specified device independent image.
	 */
	void writeGraphicsControlBlock(ImageData image) {
		try {
			outputStream.write(GIF_EXTENSION_BLOCK_ID);
			outputStream.write(GIF_GRAPHICS_CONTROL_BLOCK_ID);
			byte[] gcBlock = new byte[4];
			gcBlock[0] = 0;
			gcBlock[1] = 0;
			gcBlock[2] = 0;
			gcBlock[3] = 0;
			if (image.transparentPixel != -1) {
				gcBlock[0] = (byte)0x01;
				gcBlock[3] = (byte)image.transparentPixel;
			}
			if (image.disposalMethod != 0) {
				gcBlock[0] |= (byte)((image.disposalMethod & 0x07) << 2);
			}
			if (image.delayTime != 0) {
				gcBlock[1] = (byte)(image.delayTime & 0xFF);
				gcBlock[2] = (byte)((image.delayTime >> 8) & 0xFF);
			}
			outputStream.write((byte)gcBlock.length);
			outputStream.write(gcBlock);
			outputStream.write(0); // Block terminator
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
	}

	/**
	 * Write the specified palette to the output stream.
	 */
	void writePalette(PaletteData palette, int depth) {
		byte[] bytes = new byte[(1 << depth) * 3];
		int offset = 0;
		for (RGB color : palette.colors) {
			bytes[offset] = (byte)color.red;
			bytes[offset + 1] = (byte)color.green;
			bytes[offset + 2] = (byte)color.blue;
			offset += 3;
		}
		try {
			outputStream.write(bytes);
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
	}
}
