#region Copyright & License Information
/*
 * Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
 * This file is part of OpenRA, which is free software. It is made
 * available to you under the terms of the GNU General Public License
 * as published by the Free Software Foundation. For more information,
 * see COPYING.
 */
#endregion

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;

namespace OpenRA.FileFormats
{
	public static class PngLoader
	{
		public static Bitmap Load(string filename)
		{
			using (var s = File.OpenRead(filename))
				return Load(s);
		}

		public static Bitmap Load(Stream s)
		{
			using (var br = new BinaryReader(s))
			{
				var signature = new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 };
				foreach (var b in signature)
					if (br.ReadByte() != b)
						throw new InvalidDataException("PNG Signature is bogus");

				Bitmap bitmap = null;
				Color[] palette = null;
				var data = new List<byte>();

				for (;;)
				{
					var length = IPAddress.NetworkToHostOrder(br.ReadInt32());
					var type = Encoding.UTF8.GetString(br.ReadBytes(4));
					var content = br.ReadBytes(length);
					/*var crc = */br.ReadInt32();

					using (var ms = new MemoryStream(content))
					using (var cr = new BinaryReader(ms))
						switch (type)
						{
							case "IHDR":
								{
									var width = IPAddress.NetworkToHostOrder(cr.ReadInt32());
									var height = IPAddress.NetworkToHostOrder(cr.ReadInt32());
									var bitDepth = cr.ReadByte();
									var colorType = (PngColorType)cr.ReadByte();
									var compression = cr.ReadByte();
									/*var filter = */cr.ReadByte();
									var interlace = cr.ReadByte();

									if (compression != 0) throw new InvalidDataException("Compression method not supported");
									if (interlace != 0) throw new InvalidDataException("Interlacing not supported");

									bitmap = new Bitmap(width, height, MakePixelFormat(bitDepth, colorType));
								}

								break;

							case "PLTE":
								{
									palette = new Color[256];
									for (var i = 0; i < 256; i++)
									{
										var r = cr.ReadByte(); var g = cr.ReadByte(); var b = cr.ReadByte();
										palette[i] = Color.FromArgb(r, g, b);
									}
								}

								break;

							case "tRNS":
								{
									for (var i = 0; i < length; i++)
										palette[i] = Color.FromArgb(cr.ReadByte(), palette[i]);
								}

								break;

							case "IDAT":
								{
									data.AddRange(content);
								}

								break;

							case "IEND":
								{
									var bits = bitmap.LockBits(bitmap.Bounds(),
										ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);

									using (var ns = new MemoryStream(data.ToArray()))
									{
										// 'zlib' flags bytes; confuses the DeflateStream.
										/*var flags = (byte)*/ns.ReadByte();
										/*var moreFlags = (byte)*/ns.ReadByte();

										using (var ds = new DeflateStream(ns, CompressionMode.Decompress))
										using (var dr = new BinaryReader(ds))
										{
											var prevLine = new byte[bitmap.Width];	// all zero
											for (var y = 0; y < bitmap.Height; y++)
											{
												var filter = (PngFilter)dr.ReadByte();
												var line = dr.ReadBytes(bitmap.Width);

												for (var i = 0; i < bitmap.Width; i++)
													line[i] = i > 0
														? UnapplyFilter(filter, line[i], line[i - 1], prevLine[i], prevLine[i - 1])
														: UnapplyFilter(filter, line[i], 0, prevLine[i], 0);

												Marshal.Copy(line, 0, new IntPtr(bits.Scan0.ToInt64() + y * bits.Stride), line.Length);
												prevLine = line;
											}
										}
									}

									bitmap.UnlockBits(bits);

									using (var temp = new Bitmap(1, 1, PixelFormat.Format8bppIndexed))
									{
										var cp = temp.Palette;
										for (var i = 0; i < 256; i++)
											cp.Entries[i] = palette[i];		// finalize the palette.
										bitmap.Palette = cp;
										return bitmap;
									}
								}
						}
				}
			}
		}

		static byte UnapplyFilter(PngFilter f, byte x, byte a, byte b, byte c)
		{
			switch (f)
			{
				case PngFilter.None: return x;
				case PngFilter.Sub: return (byte)(x + a);
				case PngFilter.Up: return (byte)(x + b);
				case PngFilter.Average: return (byte)(x + (a + b) / 2);
				case PngFilter.Paeth: return (byte)(x + Paeth(a, b, c));
				default:
					throw new InvalidOperationException("Unsupported Filter");
			}
		}

		static byte Paeth(byte a, byte b, byte c)
		{
			var p = a + b - c;
			var pa = Math.Abs(p - a);
			var pb = Math.Abs(p - b);
			var pc = Math.Abs(p - c);

			return (pa <= pb && pa <= pc) ? a :
				(pb <= pc) ? b : c;
		}

		[Flags]
		enum PngColorType { Indexed = 1, Color = 2, Alpha = 4 }
		enum PngFilter { None, Sub, Up, Average, Paeth }

		static PixelFormat MakePixelFormat(byte bitDepth, PngColorType colorType)
		{
			if (bitDepth == 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
				return PixelFormat.Format8bppIndexed;

			throw new InvalidDataException("Unknown pixelformat");
		}
	}
}
