using System;
using System.IO;
using UnityEngine;
using System.Collections.Generic;

namespace uGIF
{
	public class GIFEncoder
	{

		public bool useGlobalColorTable = false;
		public Color32? transparent = null;
		public int repeat = -1;
		public int dispose = -1; // disposal code (-1 = use default)
		public int quality = 10; // default sample interval for quantizer

		public float FPS {
			set {
				delay = Mathf.RoundToInt (100f / value);
			}
		}

		public void AddFrame (Image im)
		{
			if (im == null)
				throw new ArgumentNullException ("im");
			if (!started)
				throw new InvalidOperationException ("Start() must be called before AddFrame()");

			if (firstFrame) {
				width = im.width;
				height = im.height;
			}

			pixels = im.pixels;
			RemapPixels (); // build color table & map pixels
			pixels = null;
			if (firstFrame) {
				WriteLSD (); // logical screen descriptior
				WritePalette (); // global color table
				if (repeat >= 0) {
					// use NS app extension to indicate reps
					WriteNetscapeExt ();
				}
			}

			WriteGraphicCtrlExt (); // write graphic control extension
			WriteImageDesc (); // image descriptor
			if (!firstFrame && !useGlobalColorTable) {
				WritePalette (); // local color table
			}
			WritePixels (); // encode and write pixel data
			firstFrame = false;
		}

		public void Finish ()
		{
			if (!started)
				throw new InvalidOperationException ("Start() must be called before Finish()");

			started = false;

			ms.WriteByte (0x3b); // gif trailer
			ms.Flush ();

			// reset for subsequent use
			transIndex = 0;
			pixels = null;
			indexedPixels = null;
			prevIndexedPixels = null;
			colorTab = null;
			firstFrame = true;
			nq = null;
		}
	
		public void Start (MemoryStream os)
		{
			if (os == null)
				throw new ArgumentNullException ("os");
			ms = os;
			started = true;
			WriteString ("GIF89a"); // header
		}

		void RemapPixels ()
		{
			int len = pixels.Length;
			indexedPixels = new byte[len];

			if (firstFrame || !useGlobalColorTable) {
				// initialize quantizer
				nq = new NeuQuant (pixels, len, quality);
				colorTab = nq.Process (); // create reduced palette
			}

			for (int i = 0; i < len; i++) {
				int index = nq.Map (pixels [i].r & 0xff, pixels [i].g & 0xff, pixels [i].b & 0xff);
				usedEntry [index] = true;
				indexedPixels [i] = (byte)index;

				if (dispose == 1 && prevIndexedPixels != null) {
					if (indexedPixels [i] == prevIndexedPixels [i]) {
						indexedPixels [i] = (byte)transIndex;
					} else {
						prevIndexedPixels [i] = (byte)index;
					}
				}
			}
			colorDepth = 8;
			palSize = 7;
			// get closest match to transparent color if specified
			if (transparent.HasValue) {
				var c = transparent.Value;
				//transIndex = FindClosest(transparent);
				transIndex = nq.Map (c.b, c.g, c.r);
			}

			if (dispose == 1 && prevIndexedPixels == null)
				prevIndexedPixels = indexedPixels.Clone () as byte[];
		}
	
		int FindClosest (Color32 c)
		{
			if (colorTab == null)
				return -1;
			int r = c.r;
			int g = c.g;
			int b = c.b;
			int minpos = 0;
			int dmin = 256 * 256 * 256;
			int len = colorTab.Length;
			for (int i = 0; i < len;) {
				int dr = r - (colorTab [i++] & 0xff);
				int dg = g - (colorTab [i++] & 0xff);
				int db = b - (colorTab [i] & 0xff);
				int d = dr * dr + dg * dg + db * db;
				int index = i / 3;
				if (usedEntry [index] && (d < dmin)) {
					dmin = d;
					minpos = index;
				}
				i++;
			}
			return minpos;
		}

		void WriteGraphicCtrlExt ()
		{
			ms.WriteByte (0x21); // extension introducer
			ms.WriteByte (0xf9); // GCE label
			ms.WriteByte (4); // data block size
			int transp, disp;
			if (transparent == null) {
				transp = 0;
				disp = 0; // dispose = no action
			} else {
				transp = 1;
				disp = 2; // force clear if using transparent color
			}
			if (dispose >= 0) {
				disp = dispose & 7; // user override
			}
			disp <<= 2;

			// packed fields
			ms.WriteByte (Convert.ToByte (0 | // 1:3 reserved
				disp | // 4:6 disposal
				0 | // 7   user input - 0 = none
				transp)); // 8   transparency flag

			WriteShort (delay); // delay x 1/100 sec
			ms.WriteByte (Convert.ToByte (transIndex)); // transparent color index
			ms.WriteByte (0); // block terminator
		}
	
		void WriteImageDesc ()
		{
			ms.WriteByte (0x2c); // image separator
			WriteShort (0); // image position x,y = 0,0
			WriteShort (0);
			WriteShort (width); // image size
			WriteShort (height);
			// no LCT  - GCT is used for first (or only) frame
			ms.WriteByte (0);

		}
	
		void WriteLSD ()
		{
			// logical screen size
			WriteShort (width);
			WriteShort (height);
			// packed fields
			ms.WriteByte (Convert.ToByte (0x80 | // 1   : global color table flag = 1 (gct used)
				0x70 | // 2-4 : color resolution = 7
				0x00 | // 5   : gct sort flag = 0
				palSize)); // 6-8 : gct size

			ms.WriteByte (0); // background color index
			ms.WriteByte (0); // pixel aspect ratio - assume 1:1
		}

		void WriteNetscapeExt ()
		{
			ms.WriteByte (0x21); // extension introducer
			ms.WriteByte (0xff); // app extension label
			ms.WriteByte (11); // block size
			WriteString ("NETSCAPE" + "2.0"); // app id + auth code
			ms.WriteByte (3); // sub-block size
			ms.WriteByte (1); // loop sub-block id
			WriteShort (repeat); // loop count (extra iterations, 0=repeat forever)
			ms.WriteByte (0); // block terminator
		}
	
		void WritePalette ()
		{
			ms.Write (colorTab, 0, colorTab.Length);
			int n = (3 * 256) - colorTab.Length;
			for (int i = 0; i < n; i++) {
				ms.WriteByte (0);
			}
		}
	
		void WritePixels ()
		{
			LZWEncoder encoder = new LZWEncoder (width, height, indexedPixels, colorDepth);
			encoder.Encode (ms);
		}
	
		void WriteShort (int value)
		{
			ms.WriteByte (Convert.ToByte (value & 0xff));
			ms.WriteByte (Convert.ToByte ((value >> 8) & 0xff));
		}
	
		void WriteString (String s)
		{
			char[] chars = s.ToCharArray ();
			for (int i = 0; i < chars.Length; i++) {
				ms.WriteByte ((byte)chars [i]);
			}
		}

		int delay = 0;
		int width;
		int height;
		int transIndex;
		bool started = false;
		MemoryStream  ms;
		Color32[] pixels;
		byte[] indexedPixels;
		byte[] prevIndexedPixels;
		int colorDepth;
		byte[] colorTab;
		bool[] usedEntry = new bool[256]; // active palette entries
		int palSize = 7; // color table size (bits-1)
		bool firstFrame = true;
		NeuQuant nq;
	}

}