/**
 * Copyright (C) 2009 - 2021 <a href="https://www.wudsn.com" target="_top">Peter Dell</a>
 *
 * This file is part of WUDSN IDE.
 * 
 * WUDSN IDE is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 * 
 * WUDSN IDE is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with WUDSN IDE.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.wudsn.ide.gfx.converter.atari8bit;

import com.wudsn.ide.gfx.converter.FilesConverterData;
import com.wudsn.ide.gfx.converter.PaletteMapper;
import com.wudsn.ide.gfx.converter.generic.LinearBitMapConverter;
import com.wudsn.ide.gfx.model.Palette;
import com.wudsn.ide.gfx.model.RBGUtility;

/**
 * <u>RIP picture format description</u><br/>
 * 
 * Original text by Rocky of Madteam taken from Syzygy Magazine <br/>
 * RIP picture was shown first time in Orneta of Poland in '97 in Igor demo by
 * Madteam. There we copied several disks of RIP images. RIP is a new graphics
 * storage format on Atari 8-bit. It is based on already famous HIP picture
 * format (Gr.9 + Gr.10). Theoretically it is possible to get 144 colors (there
 * are about 40 in practice) with resolution of 160x238 (RIP mode 32; 16 lum x 9
 * col). RIP is more complicated than HIP, because of the header which contains
 * a lot of information about colors, resolution and a note from the author. RIP
 * can have various vertical sizes (max. 238 lines) which cause less space on
 * disk and faster reading. <br/>
 * <br/>
 * 
 * (E.g. we have a logo with 50 lines. Our logo will be saved with 200 lines as
 * HIP and takes about 16kB but saved as RIP - only 4kB plus several bytes of
 * header. That's a difference, isn't it?) In addition RIP can be colored and
 * HIP can only be grey scaled. (HIP can also be colored but there is no room
 * for color info in the header.) The first one can still be packed (algorithm
 * in end phase) for RIP dedicated Visage viewer which is able to recognize its
 * size, colors, compression method and displaying on screen. Saving HIP as RIP
 * is no problem 'couse of similar data format. Adapting existing HIP viewers
 * for RIP pictures shouldn't be problematic too; Only what has to be done is
 * format recognition, displaying of various image height and setting colors
 * (for Gr.10) from the header (see below). <br/>
 * <br/>
 * 
 * <u>How to make a RIP picture?</u><br/>
 * 
 * It can be quite difficult. There is a converter for Amiga and PC. This is not
 * a real converter from any graphics format (GIF, IFF or Jpeg) to RIP. To make
 * a RIP picture, you will have to use programs not only dedicated to RIP. The
 * best for Amiga is Personal Paint and for PC - Display. It's quite a large
 * job. One picture has to be divided into two others. Next: size modification,
 * color palette reduction (one of these two pictures has to be 16 grey scales
 * what can be done with almost all programs and the other picture has to be in
 * 9 colors, what most programs for the Amiga and PC simply cannot do). The
 * effect can be very satisfactory, but it might also be completely bad, which
 * happens quite often (none of the pics is perfect). You can then always create
 * a HIP picture, which is better in this case (grey scale is easier to do) but
 * you will be limited to 200 lines. Well, life is brutal... <br/>
 * <br/>
 * <u>Header description:</u><br/>
 * In 'Description' filed text with quotes (") is pure ASCII string.
 * 
 * <pre>
 *  Offset | Len | Description
 * --------------------------------------------
 *       0 |  3  | "RIP" - RIP image identifier
 *       3 |  4  | version:
 *         |     |         "1.x " - standard RIP
 *         |     |         "2.0 " - Multi RIP
 *       7 |  1  | graphics mode:
 *         |     |         $20 - RIP or HIP
 *         |     |         $30 - Multi RIP, palette at the end of file
 *         |     |         $0f - Graphics  8
 *         |     |         $4f - Graphics  9
 *         |     |         $8f - Graphics 10
 *         |     |         $cf - Graphics 11
 *         |     |         $0e - Graphics 15
 *       8 |  2  | compression method:
 *         |     |         0, 0 - no compress
 *         |     |         0, 1 - RIPPCK
 *      10 |  2  | header length in bytes, MSB/LSB !!!
 *      12 |  1  | not used
 *      13 |  1  | image width in pixels (max. 80)     *see below
 *      14 |  1  | not used
 *      15 |  1  | image height in lines (max. 238)
 *      16 |  1  | display option (?), standard set to $20
 *      17 |  1  | author note length in bytes (max. 256)
 *      18 |  2  | "T:" - text identifier
 *      20 |  n  | author note
 *    20+n |  1  | number of colors (fixed = 9 from Gr.10)
 *    21+n |  3  | "CM:" - color map identifier
 *    24+n |  9  | color values                        **see below
 *    33+n |  3  | "PCK" - Multi RIP packed file only, means packed
 *         |     |         images data
 * </pre>
 * 
 * Directly after header images data are stored. First Gr.10, next Gr.9.
 * 
 * HIP and RIP are 80 pixels wide pictures with shifted 9-color plan (Gr.10)
 * relativelly to 16-grey shaded plan (Gr.9) for half pixel right. Thus, they
 * appear as 160x200, but one pic has only 80x200;
 * 
 * Number of colors is fixed to 9 now but it may change. So, take number of
 * colors from 20+n byte of header (n - length of author note) for safety. (RIP
 * mode 48 has more colors !!)
 * 
 * Displaying RIP "1.x" picture routine is similar to HIP routine. The only
 * difference is to set color registers with values put behind "CM:" while Gr.10
 * line.
 * 
 * Information above is enough for writing own procedure showing RIP 1.x
 * picture. For now I don't have any info about Multi RIP especially for
 * compression algorithm and color palette. The only thing I know for sure is
 * the palette should be changed every 2 scanlines. Maybe someone else knows a
 * little bit more about Multi RIP (or RIP mode 48 with many more colors than
 * mode 32)...
 * 
 * Note: This implementation is based on FAIL 1.0.1 and allows 239 lines tough
 * the specification above states 238 as the maximum.
 */
// TODO Verify against FAIL 1.0.2, check if the spurious grey spot and bugs are
// gone
public class LinearBitMapRIPConverter extends LinearBitMapConverter {

	private final static class RIPFile {
		public static final byte RIP = 0x20;
		public static final byte MULTI_RIP = 0x30;

		private int graphicsMode;
		private int width;
		private int height;
		private int[] palette;
		private byte[] unpackedImage;

		private RIPFile(int graphicsMode, int width, int height, int[] palette, byte[] unpacked_image) {
			this.graphicsMode = graphicsMode;
			this.width = width;
			this.height = height;
			this.palette = palette;
			this.unpackedImage = unpacked_image;

		}

		public static RIPFile createInstance(byte[] bytes) {
			if (bytes == null) {
				throw new IllegalArgumentException("Parameter 'bytes' must not be null.");
			}
			if (bytes.length < 23) {
				return null;
			}
			int graphicsMode = bytes[7];
			int headerLength = (bytes[11] & 0xff) + 256 * (bytes[12] & 0xff);
			int width = bytes[13] & 0xff;
			int height = bytes[15] & 0xff;
			int textLength = bytes[17] & 0xff;
			if (bytes.length < 20 + textLength) {
				return null;
			}
			int paletteLength = bytes[20 + textLength] & 0xff;
			int dataLength = bytes.length - headerLength;

			if (bytes[0] != 'R' || bytes[1] != 'I' || bytes[2] != 'P' || width > 80 || height > 239 || textLength > 152
					|| bytes[18] != 'T' || bytes[19] != ':' || paletteLength != 9 || bytes[21 + textLength] != 'C'
					|| bytes[22 + textLength] != 'M' || bytes[23 + textLength] != ':') {
				return null;
			}

			byte[] unpackedImage = new byte[24576];

			// Check compression mode.
			switch (bytes[9]) {
			case 0:
				// No compression.
				if (dataLength > unpackedImage.length) {
					return null;
				}
				System.arraycopy(bytes, headerLength, unpackedImage, 0, dataLength);
				break;

			// Compression
			case 1:
				if (!ShannonFano.unpack(bytes, headerLength, dataLength, unpackedImage)) {
					return null;
				}
				break;

			default:
				return null;
			}

			// Check graphic mode
			switch (graphicsMode) {
			case RIP:
				break;
			case MULTI_RIP:
				int frame_len = (width / 2) * height;
				for (int y = 0; y < height; y++) {
					for (int x = 0; x < 8; x++) {
						int ix = 2 * frame_len + y * 8 + x;
						if (y > 0 && unpackedImage[ix] == 0)
							unpackedImage[ix] = unpackedImage[ix - 8];
					}
				}
				break;
			default:
				return null;
			}

			int[] palette = new int[paletteLength];
			for (int i = 0; i < paletteLength; i++) {
				palette[i] = bytes[24 + textLength + i] & 0xff;
			}
			RIPFile result = new RIPFile(graphicsMode, width, height, palette, unpackedImage);
			return result;
		}

		public int getGraphicsMode() {
			return graphicsMode;
		}

		public int getWidth() {
			return width;
		}

		public int getHeight() {
			return height;
		}

		public int[] getPalette() {
			return palette;
		}

		public byte[] getUnpackedImage() {
			return unpackedImage;
		}

	}

	private static final class ShannonFano {

		private static void unpack_cnibl(byte data[], int dataOffset, int size, int output[]) {
			int x = dataOffset;
			int y = 0;
			while (y < size) {
				int a = data[x++] & 0xff;
				output[y++] = a >>> 4;
				output[y++] = a & 0x0f;
			}
		}

		private static void unpack_sort(byte data[], int dataOffset, int size, int tre01[], int tre02[]) {
			int[] pom = new int[16];
			int y;
			int x;
			int md_ = 0;
			int md = 0;

			unpack_cnibl(data, dataOffset, size, tre02);

			for (y = 0; y < size; y++) {
				pom[tre02[y]]++;
			}

			x = 0;
			do {
				y = 0;
				do {
					if (x == tre02[y]) {
						tre01[md_++] = y;
					}
					y++;
				} while (y < size);
				x++;
			} while (x < 16);

			x = 0;
			do {
				y = pom[x];
				while (y != 0) {
					tre02[md++] = x;
					y--;
				}
				x++;
			} while (x < 16);
		}

		private static void unpack_fano(byte data[], int dataOffset, int size, int tre01[], int tre02[], int l0[],
				int h0[], int l1[], int h1[], int lhOffset) {
			int p;
			int err;
			int l;
			int nxt;
			int y;

			unpack_sort(data, dataOffset, size, tre01, tre02);

			clearMemory(l0, lhOffset, size);
			clearMemory(l1, lhOffset, size);
			clearMemory(h0, lhOffset, size);
			clearMemory(h1, lhOffset, size);

			p = 0;
			err = 0;
			l = 0;
			nxt = 0;
			y = 0;
			do {
				if (tre02[y] != 0) {
					int x;
					int tmp;
					int val;
					int a;
					p += err;
					x = tre02[y];
					if (x != l) {
						l = x;
						err = 0x10000 >>> x;
					}
					tmp = p;
					val = tre01[y];
					x = tre02[y];
					a = 0;
					for (;;) {
						int z = lhOffset + a;
						x--;
						tmp <<= 1;
						if (tmp < 0x10000) {
							if (x == 0) {
								a = val;
								l0[z] = a;
								break;
							}
							a = h0[z];
							if (a == 0) {
								a = ++nxt;
								h0[z] = a;
							}
						} else {
							tmp &= 0xFFFF;
							if (x == 0) {
								a = val;
								l1[z] = a;
								break;
							}
							a = h1[z];
							if (a == 0) {
								a = ++nxt;
								h1[z] = a;
							}
						}
					}
				}
				y++;
			} while (y < size);
		}

		private static void clearMemory(int[] array, int offset, int size) {
			for (int i = 0; i < size; i++) {
				array[offset + i] = 0;
			}

		}

		public static boolean unpack(byte data[], int dataOffset, int dataLength, byte unpackedData[]) {
			if (data == null) {
				throw new IllegalArgumentException("Parameter 'data' must not be null.");
			}
			if (dataOffset < 0) {
				throw new IllegalArgumentException(
						"Parameter 'dataOffset' must not be negative, specified value is " + dataOffset + ".");
			}
			if (unpackedData == null) {
				throw new IllegalArgumentException("Parameter 'unpackedData' must not be null.");
			}
			int[] adl0 = new int[576];
			int[] adh0 = new int[576];
			int[] adl1 = new int[576];
			int[] adh1 = new int[576];

			int[] tre01 = new int[256];
			int[] tre02 = new int[256];

			int unpacked_len;
			int sx, sxend;
			int cx, dx;
			int lic, csh, c;

			// "PCK" header (16 bytes) + 288 bytes shannon-fano
			if (dataLength < 16 + 288 || data[dataOffset + 0] != 'P' || data[dataOffset + 1] != 'C'
					|| data[dataOffset + 2] != 'K') {
				return false;
			}

			unpacked_len = (data[dataOffset + 4] & 0xff) + 256 * (data[dataOffset + 5] & 0xff) - 33;
			if (unpacked_len > 0x5EFE) {
				return false;
			}

			// Buggy pictures?!
			if (unpacked_len == 16811) {
				unpacked_len = 16800;
			}

			unpack_fano(data, dataOffset + 16, 64, tre01, tre02, adl0, adh0, adl1, adh1, 0);
			unpack_fano(data, dataOffset + 16 + 32, 256, tre01, tre02, adl0, adh0, adl1, adh1, 64);
			unpack_fano(data, dataOffset + 16 + 160, 256, tre01, tre02, adl0, adh0, adl1, adh1, 320);

			sx = dataOffset + 16 + 288;
			sxend = dataOffset + dataLength + 1;

			dx = 0;
			lic = -1;
			csh = 0;

			do {
				// GBIT();
				if (--lic < 0) {
					if (sx >= sxend) {
						return false;
					}
					csh = data[sx++] & 0xff;
					lic = 7;
				}
				c = (csh & (1 << lic));

				if (c == 0) {
					int a = 0;
					for (;;) {
						int y = a;

						// GBIT();
						if (--lic < 0) {
							if (sx >= sxend) {
								return false;
							}
							csh = data[sx++] & 0xff;
							lic = 7;
						}
						c = (csh & (1 << lic));

						if (c == 0) {
							if ((a = adh0[320 + y]) == 0) {
								unpackedData[dx] = (byte) adl0[320 + y];
								break;
							}
						} else {
							if ((a = adh1[320 + y]) == 0) {
								unpackedData[dx] = (byte) adl1[320 + y];
								break;
							}
						}
					}
					++dx;
				} else {
					int a = 0;
					for (;;) {
						int y = a;

						// GBIT();
						if (--lic < 0) {
							if (sx >= sxend) {
								return false;
							}
							csh = data[sx++] & 0xff;
							lic = 7;
						}
						c = (csh & (1 << lic));

						if (c == 0) {
							if ((a = adh0[64 + y]) == 0) {
								a = adl0[64 + y];
								break;
							}
						} else {
							if ((a = adh1[64 + y]) == 0) {
								a = adl1[64 + y];
								break;
							}
						}
					}
					cx = dx - (a + 2);
					a = 0;
					for (;;) {
						int y = a;

						// GBIT();
						if (--lic < 0) {
							if (sx >= sxend) {
								return false;
							}
							csh = data[sx++] & 0xff;
							lic = 7;
						}
						c = (csh & (1 << lic));

						if (c == 0) {
							if ((a = adh0[y]) == 0) {
								a = adl0[y];
								break;
							}
						} else {
							if ((a = adh1[y]) == 0) {
								a = adl1[y];
								break;
							}
						}
					}

					if (cx > 0) {
						System.arraycopy(unpackedData, cx, unpackedData, dx, a + 2);
					}
					dx += a + 2;
				}
			} while (dx < unpacked_len);

			return true;
		}
	}

	public LinearBitMapRIPConverter() {

	}

	@Override
	public boolean canConvertToImage(byte[] bytes) {
		if (bytes == null) {
			throw new IllegalArgumentException("Parameter 'bytes' must not be null.");
		}

		RIPFile ripFile = RIPFile.createInstance(bytes);
		if (ripFile == null) {
			return false;
		}
		return true;
	}

	@Override
	public void convertToImageSizeAndPalette(FilesConverterData data, byte[] bytes) {
		if (data == null) {
			throw new IllegalArgumentException("Parameter 'data' must not be null.");
		}
		if (bytes == null) {
			throw new IllegalArgumentException("Parameter 'bytes' must not be null.");
		}

		RIPFile ripFile = RIPFile.createInstance(bytes);
		if (ripFile == null) {
			throw new IllegalStateException("canConvertToImage() not called");
		}

		int columns;
		int rows;
		columns = (ripFile.getWidth() + 1) / 2;
		rows = ripFile.getHeight();

		setImageSizeAndPalette(data, columns, rows, Palette.TRUE_COLOR, null);
	}

	@Override
	public void convertToImageDataSize(FilesConverterData data) {
		data.setImageDataWidth(data.getParameters().getColumns() * 4 + 1);
		data.setImageDataHeight(data.getParameters().getRows());
	}

	@Override
	public boolean convertToImageData(FilesConverterData data) {
		if (data == null) {
			throw new IllegalArgumentException("Parameter 'data' must not be null.");
		}

		int rows = data.getParameters().getRows();
		int columns = data.getParameters().getColumns();
		PaletteMapper paletteMapper = new Atari8BitPaletteMapper();

		// Compute palette.
		RIPFile ripFile = RIPFile.createInstance(data.getSourceFileBytes(BIT_MAP_FILE));
		if (ripFile == null) {
			return false;
		}

		byte[] unpackedImage = ripFile.getUnpackedImage();
		int[] graphics10Colors;
		switch (ripFile.getGraphicsMode()) {
		case RIPFile.RIP:
			graphics10Colors = ripFile.getPalette();
			break;
		case RIPFile.MULTI_RIP:
			graphics10Colors = new int[ripFile.getPalette().length];
			break;
		default:
			throw new IllegalStateException("Unsupported graphics mode " + ripFile.getGraphicsMode() + ".");

		}

		// Compute the offsets in the file.
		int offset9, offset10;
		int frameSize = rows * columns;
		offset10 = 0;
		offset9 = offset10 + frameSize;

		int[] buffer1 = new int[columns * 4 + 1];
		int[] buffer2 = new int[columns * 4 + 1];
		for (int y1 = 0; y1 < rows; y1++) {

			// MultiRIP mode?
			if (ripFile.getGraphicsMode() == RIPFile.MULTI_RIP) {
				int offset = frameSize * 2 + ((y1 >>> 1) << 3);
				for (int i = 0; i < graphics10Colors.length - 1; i++) {
					graphics10Colors[i + 1] = unpackedImage[offset + i] & 0xff;
				}
			}

			for (int x1 = 0; x1 < columns; x1++) {
				if (offset9 >= unpackedImage.length) {
					return true;
				}
				int byte9 = unpackedImage[offset9++];
				if (offset10 >= unpackedImage.length) {
					return true;
				}
				int byte10 = unpackedImage[offset10++];

				// Byte 1 is the GTIA 9 byte, take the values as brightness
				// values
				int color1 = (byte9 & mask_4bit[0]) >>> shift_4bit[0];
				int color2 = (byte9 & mask_4bit[1]) >>> shift_4bit[1];

				// Byte 2 is the GTIA 10 byte, take the values from the GTIA 10
				// palette
				int color3 = (byte10 & mask_4bit[0]) >>> shift_4bit[0];
				int color4 = (byte10 & mask_4bit[1]) >>> shift_4bit[1];
				color3 = graphics10Colors[Atari8BitUtility.GRAPHICS_10_REGISTERS[color3]];
				color4 = graphics10Colors[Atari8BitUtility.GRAPHICS_10_REGISTERS[color4]];

				// Put the color values in the row buffer, shifted by 1 pixel
				int x = x1 << 2;
				buffer1[x + 0] = color1;
				buffer1[x + 1] = color1;
				buffer1[x + 2] = color2;
				buffer1[x + 3] = color2;

				buffer2[x + 1] = color3;
				buffer2[x + 2] = color3;
				buffer2[x + 3] = color4;
				buffer2[x + 4] = color4;
			}

			// Merge the two buffers into combined color values.
			for (int x = 0; x < buffer1.length; x++) {
				int atariColor = RBGUtility.combineRGBColor(paletteMapper.getRGBColor(buffer1[x]),
						paletteMapper.getRGBColor(buffer2[x]));
				data.setDirectPixel(x, y1, atariColor);
			}
		}
		return true;

	}
}
