using System;
using System.IO;
using System.Collections;

namespace Orciid.Media.Util
{
	/// <summary>
	/// IPTC data manager
	/// </summary>
	/// <remarks>
	/// This class allows retrieving and adding IPTC data to the header of JPEG files
	/// </remarks>
	public class IPTCData
	{
		static private Hashtable iptccodes;
		static private Hashtable iptclistcodes;

		static IPTCData()
		{
			iptccodes = new Hashtable();
//			iptccodes.Add(0, "record version");		# skip -- binary data
			iptccodes.Add(5, "object name");
			iptccodes.Add(7, "edit status");
			iptccodes.Add(8, "editorial update");
			iptccodes.Add(10, "urgency");
			iptccodes.Add(12, "subject reference");
			iptccodes.Add(15, "category");
//			iptccodes.Add(20, "supplemental category");	# in listdatasets (see below)
			iptccodes.Add(22, "fixture identifier");
//			iptccodes.Add(25, "keywords");				# in listdatasets
			iptccodes.Add(26, "content location code");
			iptccodes.Add(27, "content location name");
			iptccodes.Add(30, "release date");
			iptccodes.Add(35, "release time");
			iptccodes.Add(37, "expiration date");
			iptccodes.Add(38, "expiration time");
			iptccodes.Add(40, "special instructions");
			iptccodes.Add(42, "action advised");
			iptccodes.Add(45, "reference service");
			iptccodes.Add(47, "reference date");
			iptccodes.Add(50, "reference number");
			iptccodes.Add(55, "date created");
			iptccodes.Add(60, "time created");
			iptccodes.Add(62, "digital creation date");
			iptccodes.Add(63, "digital creation time");
			iptccodes.Add(65, "originating program");
			iptccodes.Add(70, "program version");
			iptccodes.Add(75, "object cycle");
			iptccodes.Add(80, "by-line");
			iptccodes.Add(85, "by-line title");
			iptccodes.Add(90, "city");
			iptccodes.Add(92, "sub-location");
			iptccodes.Add(95, "province/state");
			iptccodes.Add(100, "country/primary location code");
			iptccodes.Add(101, "country/primary location name");
			iptccodes.Add(103, "original transmission reference");
			iptccodes.Add(105, "headline");
			iptccodes.Add(110, "credit");
			iptccodes.Add(115, "source");
			iptccodes.Add(116, "copyright notice");
			iptccodes.Add(118, "contact");
			iptccodes.Add(120, "caption/abstract");
			iptccodes.Add(122, "writer/editor");
//			iptccodes.Add(125, "rasterized caption"); # unsupported (binary data)
			iptccodes.Add(130, "image type");
			iptccodes.Add(131, "image orientation");
			iptccodes.Add(135, "language identifier");
			iptccodes.Add(200, "custom1"); // These are NOT STANDARD, but are used by
			iptccodes.Add(201, "custom2"); // Fotostation. Use at your own risk. They're
			iptccodes.Add(202, "custom3"); // here in case you need to store some special
			iptccodes.Add(203, "custom4"); // stuff, but note that other programs won't 
			iptccodes.Add(204, "custom5"); // recognize them and may blow them away if 
			iptccodes.Add(205, "custom6"); // you open and re-save the file. (Except with
			iptccodes.Add(206, "custom7"); // Fotostation, of course.)
			iptccodes.Add(207, "custom8");
			iptccodes.Add(208, "custom9");
			iptccodes.Add(209, "custom10");
			iptccodes.Add(210, "custom11");
			iptccodes.Add(211, "custom12");
			iptccodes.Add(212, "custom13");
			iptccodes.Add(213, "custom14");
			iptccodes.Add(214, "custom15");
			iptccodes.Add(215, "custom16");
			iptccodes.Add(216, "custom17");
			iptccodes.Add(217, "custom18");
			iptccodes.Add(218, "custom19");
			iptccodes.Add(219, "custom20");

			iptclistcodes = new Hashtable();
			iptclistcodes.Add(20, "supplemental category");
			iptclistcodes.Add(25, "keywords");
		}

		private bool IsJPEGStream()
		{
			try
			{
				originalstream.Position = 0;
				byte ff = reader.ReadByte();
				byte soi = reader.ReadByte();
				if (ff != 0xFF || soi != 0xD8)
					return false;
				ff = reader.ReadByte();
				reader.ReadByte();
				if (ff != 0xFF)
					return false;
				return true;
			}
			catch
			{
				return false;
			}
			finally
			{
				originalstream.Position = 0;
			}
		}

		private ushort SwapBytes(ushort s)
		{
			return (ushort)(((s & 0xFF00) >> 8) + ((s & 0x00FF) << 8));
		}

		private uint SwapBytes(uint i)
		{
			return (uint)(((i & 0xFF000000) >> 24) + ((i & 0x00FF0000) >> 8) +
				((i & 0x0000FF00) << 8) + ((i & 0x000000FF) << 24));
		}

		private Stream originalstream;
		private bool isjpegstream;
		private Hashtable attributes = new Hashtable();
		private ArrayList keywords = new ArrayList();
		private ArrayList supplementalcategories = new ArrayList();
		private BinaryReader reader;
			
		/// <summary>
		/// IPTC entry
		/// </summary>
		/// <remarks>
		/// Retrieves an IPTC entry by IPTC code
		/// </remarks>
		/// <value>
		/// The value of the IPTC entry
		/// </value>
		/// <param name="index">The IPTC code of the IPTC entry to read or write</param>
		public string this[int index]
		{
			get
			{
				if (iptccodes.ContainsKey(index))
					return (string)attributes[iptccodes[index]];
				else
					throw new ArgumentException("Invalid IPTC code", "index");
			}
			set
			{
				if (iptccodes.ContainsKey(index))
				{
					attributes[iptccodes[index]] = 
						(value == null ? "" : 
						(value.Length <= 0xFFFF ? value : value.Substring(0, 0xFFFF)));
				}
				else
					throw new ArgumentException("Invalid IPTC code", "index");
			}
		}

		/// <summary>
		/// IPTC entry
		/// </summary>
		/// <remarks>
		/// Retrieves an IPTC entry by IPTC field name
		/// </remarks>
		/// <value>
		/// The value of the IPTC entry
		/// </value>
		/// <param name="index">The IPTC field name of the IPTC entry to read or write</param>
		public string this[string index]
		{
			get
			{
				if (iptccodes.ContainsValue(index))
					return (string)attributes[index];
				else
					throw new ArgumentException("Invalid IPTC field name", "index");
			}
			set
			{
				if (iptccodes.ContainsValue(index))
					attributes[index] = 						
						(value == null ? "" : 
						(value.Length <= 0xFFFF ? value : value.Substring(0, 0xFFFF)));

				else
					throw new ArgumentException("Invalid IPTC field name", "index");
			}
		}

		/// <summary>
		/// Names of existing IPTC entries
		/// </summary>
		/// <remarks>
		/// This method returns an array of names of IPTC entries that are currently set
		/// </remarks>
		/// <returns>An array of names of IPTC entries that are currently set</returns>
		public string[] Keys()
		{
			ArrayList keys = new ArrayList();
			foreach (string v in iptccodes.Values)
				if (attributes.ContainsKey(v))
					keys.Add(v);
			return (string[])keys.ToArray(typeof(string));
		}

		/// <summary>
		/// Clear entries
		/// </summary>
		/// <remarks>
		/// This method removes all currently set entries
		/// </remarks>
		public void ClearAttributes()
		{
			attributes.Clear();
		}
		
		/// <summary>
		/// Keywords
		/// </summary>
		/// <remarks>
		/// Returns all currently set keywords
		/// </remarks>
		/// <value>
		/// An array of currently set keywords
		/// </value>
		public string[] Keywords
		{
			get
			{
				return (string[])keywords.ToArray(typeof(string));
			}
		}

		/// <summary>
		/// Clear keywords
		/// </summary>
		/// <remarks>
		/// Removes all currently set keywords
		/// </remarks>
		public void ClearKeywords()
		{
			keywords.Clear();
		}

		/// <summary>
		/// Add keywords
		/// </summary>
		/// <remarks>
		/// Adds keywords entries to the IPTC header
		/// </remarks>
		/// <param name="values">Keywords to add</param>
		public void AddKeywords(params string[] values)
		{
			foreach (string s in values)
				if (s != null)
					keywords.Add(s.Length <= 0xFFFF ? s : s.Substring(0, 0xFFFF));
		}

		/// <summary>
		/// Supplemental categories
		/// </summary>
		/// <remarks>
		/// Returns all currently set supplemental categories
		/// </remarks>
		/// <value>
		/// An array of currently set supplemental categories
		/// </value>
		public string[] SupplementalCategories
		{
			get
			{
				return (string[])supplementalcategories.ToArray(typeof(string));
			}
		}

		/// <summary>
		/// Clear supplemental categories
		/// </summary>
		/// <remarks>
		/// Removes all currently set supplemental categories
		/// </remarks>
		public void ClearSupplementalCategories()
		{
			supplementalcategories.Clear();
		}

		/// <summary>
		/// Add supplemental categories
		/// </summary>
		/// <remarks>
		/// Adds supplemental categories entries to the IPTC header
		/// </remarks>
		/// <param name="values">Supplemental categories to add</param>
		public void AddSupplementalCategories(params string[] values)
		{
			foreach (string s in values)
				if (s != null)
					supplementalcategories.Add(s.Length <= 0xFFFF ? s : s.Substring(0, 0xFFFF));
		}

		private void AddListValue(byte dataset, string val)
		{
			if (dataset == 20)
				AddSupplementalCategories(val);
			else if (dataset == 25)
				AddKeywords(val);
		}
			
		/// <summary>
		/// Access IPTC header in JPEG file
		/// </summary>
		/// <remarks>
		/// This method reads any existing information from the IPTC header of the JPEG file
		/// </remarks>
		/// <param name="jpeg">A stream containing a JPEG file</param>
		public IPTCData(Stream jpeg)
		{
			if (!jpeg.CanSeek || !jpeg.CanRead)
				throw new FormatException("Stream does not support reading and/or seeking");
			originalstream = jpeg;
			reader = new BinaryReader(originalstream, System.Text.Encoding.BigEndianUnicode);
			isjpegstream = IsJPEGStream();
			bool result = false;
			if (isjpegstream)
				result = JPEGScan();
			else
				result = BlindScan();
			if (result)
				CollectIIMInfo();
		}

		/// <summary>
		/// Save changes to IPTC header
		/// </summary>
		/// <remarks>
		/// This method saves any changes to the IPTC header to the JPEG file
		/// </remarks>
		/// <param name="jpegout">The stream to write the new JPEG file to</param>
		public void Save(Stream jpegout /*, bool discardAppParts */)
		{
			if (!isjpegstream)
				throw new FormatException("Cannot save non-JPEG streams");
			byte[] start;
			byte[] end;
			byte[] adobe;
			bool hasExifData;
			BinaryWriter writer = new BinaryWriter(jpegout);
			JPEGCollectFileParts(out start, out end, out adobe, out hasExifData /*, discardAppParts */);
/*			if (discardAppParts)
				adobe = null;
*/			writer.Write(start, 0, 2);
			if (!hasExifData)
			{
				// add blank EXIF block
				writer.Write(
					new byte[] {             
								   0xFF, 0xE1, 0x00, 0x16, 0x45, 0x78, 0x69, 0x66, 
								   0x00, 0x00, 0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 
								   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
							   });
			}
			writer.Write(PhotoshopIIMBlock(adobe, PackedIIMData()));
			writer.Write(start, 2, start.Length - 2);
			writer.Write(end);
			writer.Flush();
		}

		private bool JPEGScan()
		{
			byte ff = reader.ReadByte();
			byte soi = reader.ReadByte();
			if (ff != 0xFF || soi != 0xD8)
				throw new FormatException("Invalid stream");
			byte marker = JPEGNextMarker();
			while (marker != 0xED)
			{
				if (marker == 0)
					throw new Exception("Marker scan failed");
				if (marker == 0xD9)
					throw new Exception("Marker scan hit end of image marker");
				if (marker == 0xDA)
					return false;
//					throw new Exception("Marker scan hit start of image data");
				JPEGSkipVariable();
				marker = JPEGNextMarker();
			}
			return BlindScan();
		}

		private byte JPEGNextMarker()
		{
			try
			{
				byte b = reader.ReadByte();
				while (b != 0xFF)
				{
					b = reader.ReadByte();
				}
				do
				{
					b = reader.ReadByte();
				} while (b == 0xFF);
				return b;
			}
			catch
			{
				return 0;
			}
		}

		private byte[] JPEGSkipVariable()
		{
			ushort length = SwapBytes(reader.ReadUInt16());
			if (length < 2)
				throw new FormatException("Invalid JPEG marker length");
			length -= 2;
			return reader.ReadBytes(length);
		}

		private bool BlindScan()
		{
			const int MAX = 8192;
			int offset = 0;
			while (offset <= MAX)
			{
				byte temp = reader.ReadByte();
				if (temp == 0x1C)
				{
					byte record = reader.ReadByte();
					byte dataset = reader.ReadByte();
					if (record == 2 && dataset == 0)
					{
						reader.BaseStream.Position -= 3;
						return true;
					}
					else
					{
						reader.BaseStream.Position -= 2;
					}
				}
				offset++;
			}
			return false;
			// throw new FormatException("Could not find IIM record");
		}

		private void CollectIIMInfo()
		{
			while (true)
			{
				byte tag = reader.ReadByte();
				byte record = reader.ReadByte();
				byte dataset = reader.ReadByte();
				ushort length = SwapBytes(reader.ReadUInt16());
				if (tag != 0x1C || record != 2)
					return;
				byte[] val = reader.ReadBytes(length);
				string sval = System.Text.ASCIIEncoding.ASCII.GetString(val);
				if (iptclistcodes.ContainsKey((int)dataset))
					AddListValue(dataset, sval);
				else if (iptccodes.ContainsKey((int)dataset))
					this[dataset] = sval;
			}
		}

		private void JPEGCollectFileParts(out byte[] start, out byte[] end, out byte[] adobe,
			out bool hasExifData /*, bool discardAppParts */)
		{
			originalstream.Position = 0;
			hasExifData = false;
			byte ff = reader.ReadByte();
			byte soi = reader.ReadByte();
			if (ff != 0xFF || soi != 0xD8)
				throw new FormatException("JPEGCollectFileParts: invalid start of file");
			BinaryWriter wstart = new BinaryWriter(new MemoryStream());
			BinaryWriter wend = new BinaryWriter(new MemoryStream());
			BinaryWriter wadobe = new BinaryWriter(new MemoryStream());

			bool passedadobepart = false;

			wstart.Write((byte)0xFF);
			wstart.Write((byte)0xD8);
/*			if (discardAppParts)
			{
				wstart.Write((byte)0xFF);
				wstart.Write((byte)0xE0); 
				wstart.Write(SwapBytes((ushort)16)); // length (including these 2 bytes)
				wstart.Write(System.Text.ASCIIEncoding.ASCII.GetBytes("JFIF")); // format
				wstart.Write((byte)1);
				wstart.Write((byte)2); // call it version 1.2 (current JFIF)
				for (int i = 0; i < 8; i++)
					wstart.Write((byte)0); // zero everything else
			}
*/			byte marker = JPEGNextMarker();
			while (true)
			{
				if (marker == 0)
					break;
					// throw new FormatException("Marker scan failed");
				if (marker == 0xD9 || marker == 0xDA)
				{
					wend.Write((byte)0xFF);
					wend.Write(marker);
					break;
				}
				byte[] partdata = JPEGSkipVariable();
/*				if (discardAppParts && marker >= 0xE0 && marker <= 0xEF)
				{
					// nothing
				}
				else */ 
				if (marker == 0xED)
				{
					wadobe.Write(CollectAdobeParts(partdata));
					passedadobepart = true;
//					break;
				}
				else
				{
					if (!passedadobepart)
					{
						wstart.Write((byte)0xFF);
						wstart.Write(marker);
						wstart.Write(SwapBytes((ushort)(partdata.Length + 2)));
						wstart.Write(partdata);
					}
					else
					{
						wend.Write((byte)0xFF);
						wend.Write(marker);
						wend.Write(SwapBytes((ushort)(partdata.Length + 2)));
						wend.Write(partdata);
					}
					if (marker == 0xE1)
					{
						hasExifData = true;
					}
				}
				marker = JPEGNextMarker();
			}
			byte[] buffer = new byte[16384];
			int read;
			do 
			{
				read = originalstream.Read(buffer, 0, 16384);
				if (read > 0)
					wend.Write(buffer, 0, read);
			} while (read > 0);
			start = ((MemoryStream)wstart.BaseStream).ToArray();
			end = ((MemoryStream)wend.BaseStream).ToArray();
			adobe = ((MemoryStream)wadobe.BaseStream).ToArray();
		}

		private byte[] CollectAdobeParts(byte[] data)
		{
			BinaryWriter result = new BinaryWriter(new MemoryStream());
			BinaryReader r = new BinaryReader(new MemoryStream(data, false));
			r.ReadBytes("Photoshop 3.0 ".Length);
			while (r.BaseStream.Position < r.BaseStream.Length)
			{
				uint ostype = SwapBytes(r.ReadUInt32());
				byte id1 = r.ReadByte();
				byte id2 = r.ReadByte();
				byte stringlen = r.ReadByte();
				byte[] mystring = r.ReadBytes(stringlen);
				if (stringlen == 0 || stringlen % 2 != 0)
					r.ReadByte();
				uint size = SwapBytes(r.ReadUInt32());
				byte[] var = r.ReadBytes((int)size);
				if (size % 2 != 0)
					r.ReadByte();
				if (id1 != 4 || id2 != 4)
				{
					result.Write(SwapBytes(ostype));
					result.Write(id1);
					result.Write(id2);
					result.Write(stringlen);
					result.Write(mystring);
					if (stringlen == 0 || stringlen % 2 != 0)
						result.Write((byte)0);
					result.Write(SwapBytes(size));
					result.Write(var);
					if (size % 2 != 0 || result.BaseStream.Length % 2 != 0)
						result.Write((byte)0);
				}
			}
			return ((MemoryStream)result.BaseStream).ToArray();
		}

		private byte[] PackedIIMData()
		{
			BinaryWriter result = new BinaryWriter(new MemoryStream());
			result.Write((byte)0x1C);
			result.Write((byte)2);
			result.Write((byte)0);
			result.Write(SwapBytes((ushort)2));
			result.Write(SwapBytes((ushort)2));
			foreach (int code in iptccodes.Keys)
				if (attributes.ContainsKey(iptccodes[code]))
				{
					byte[] val = System.Text.ASCIIEncoding.ASCII.GetBytes((string)attributes[iptccodes[code]]);
					result.Write((byte)0x1C);
					result.Write((byte)0x02);
					result.Write((byte)code);
					result.Write(SwapBytes((ushort)val.Length));
					result.Write(val);
				}
			foreach (string s in supplementalcategories)
			{
				byte[] val = System.Text.ASCIIEncoding.ASCII.GetBytes(s);
				result.Write((byte)0x1C);
				result.Write((byte)0x02);
				result.Write((byte)20);
				result.Write(SwapBytes((ushort)val.Length));
				result.Write(val);
			}
			foreach (string s in keywords)
			{
				byte[] val = System.Text.ASCIIEncoding.ASCII.GetBytes(s);
				result.Write((byte)0x1C);
				result.Write((byte)0x02);
				result.Write((byte)25);
				result.Write(SwapBytes((ushort)val.Length));
				result.Write(val);
			}
			return ((MemoryStream)result.BaseStream).ToArray();
		}

		private byte[] PhotoshopIIMBlock(byte[] otherparts, byte[] data)
		{
			BinaryWriter result = new BinaryWriter(new MemoryStream());
			result.Write((byte)0xFF);
			result.Write((byte)0xED);
			result.Write(SwapBytes((ushort)0)); // dummy length, will be overwritten later
			result.Write(System.Text.ASCIIEncoding.ASCII.GetBytes("Photoshop 3.0"));
			result.Write((byte)0);
			result.Write(System.Text.ASCIIEncoding.ASCII.GetBytes("8BIM"));
			result.Write((byte)0x04);
			result.Write((byte)0x04);
			result.Write((byte)0);
			result.Write((byte)0);
			result.Write(SwapBytes((uint)data.Length));
			result.Write(data);
			if (data.Length % 2 != 0)
				result.Write((byte)0);
			if (otherparts != null)
				result.Write(otherparts);
			ushort length = (ushort)(result.BaseStream.Length - 2);
			result.BaseStream.Position = 2;
			result.Write(SwapBytes(length));
			return ((MemoryStream)result.BaseStream).ToArray();
		}
	}
}
