using FSpot.Imaging;
using SemWeb;
using System;
using System.IO;

#if ENABLE_NUNIT
using NUnit.Framework;
#endif

namespace FSpot.Pnm {
	public class PnmFile : ImageFile, StatementSource {

                // false seems a safe default
                public bool Distinct {
                        get { return false; }
                }

		public PnmFile (Uri uri) : base (uri) 
		{
		}

		public PnmFile (string path) : base (path) 
		{
		}

		public class Header {
			public string Magic;
			public int Width;
			public int Height;
			public ushort Max;
			
			public Header (Stream stream)
			{
				Magic = GetString (stream);
				Width = int.Parse (GetString (stream));
				Height = int.Parse (GetString (stream));
				Max = ushort.Parse (GetString (stream));
			}

			public bool IsDeep {
				get {
					return Max > 256;
				}
			}

			public void Dump ()
			{
				System.Console.WriteLine ("Loading ({0} - {1},{2} - {3})", 
							  Magic, Width, Height, Max);
			}
		}

		public void Select (StatementSink sink)
		{
			using (Stream stream = Open ()) {
				Header header = new Header (stream);
				MetadataStore.AddLiteral (sink, "tiff:ImageWidth", header.Width.ToString ());
				MetadataStore.AddLiteral (sink, "tiff:ImageLength", header.Height.ToString ());
				string bits = header.IsDeep ? "16" : "8";
				MetadataStore.Add (sink, "tiff:BitsPerSample", "rdf:Seq", new string [] { bits, bits, bits });
			}
		}
		
		public override Stream PixbufStream ()
		{
			Stream stream = Open ();
			Header header = new Header (stream);
			if (header.IsDeep)
				return null;

			stream.Position = 0;
			return stream;
		}

		static char EatComment (Stream stream)
		{
			char c;
			do {
				c = (char)stream.ReadByte ();
				
			} while (c != '\n' && c != '\n');
			
			return c;
		}

		static string GetString (Stream stream)
		{
			System.Text.StringBuilder builder = new System.Text.StringBuilder ();

			char c;
			do {
				c = (char)stream.ReadByte ();
				if (c == '#')
					c = EatComment (stream);

			} while (char.IsWhiteSpace (c));
			
			while (! char.IsWhiteSpace (c)) {
				builder.Append (c);
				c = (char)stream.ReadByte ();				
			}
			
			return builder.ToString ();
		}

		public static ushort [] ReadShort (Stream stream, int width, int height, int channels)
		{
			int length = width * height * channels;
			ushort [] data = new ushort [length];
			byte [] tmp = new byte [2];

			for (int i = 0; i < length; i++)
			{
				stream.Read (tmp, 0, tmp.Length);
				data [i] = BitConverter.ToUInt16 (tmp, 0, false);
			}
			return data;
		}

		static Gdk.Pixbuf LoadRGB16 (Stream stream, int width, int height)
		{
			Gdk.Pixbuf pixbuf = new Gdk.Pixbuf (Gdk.Colorspace.Rgb, false, 8, width, height);
			unsafe {
				byte *pixels = (byte *)pixbuf.Pixels;
				int length = width * 6;
				byte [] buffer = new byte [length];
				
				for (int row = 0; row < height; row++) {
					stream.Read (buffer, 0, buffer.Length);
					for (int i = 0; i < width * 3; i++) {
						pixels [i] = (byte) (BitConverter.ToUInt16 (buffer, i * 2, false) >> 8);
					}
					pixels += pixbuf.Rowstride;
				}
			}
			return pixbuf;
		}

		static Gdk.Pixbuf LoadRGB8 (Stream stream, int width, int height)
		{
			Gdk.Pixbuf pixbuf = new Gdk.Pixbuf (Gdk.Colorspace.Rgb, false, 8, width, height);
			unsafe {
				byte *pixels = (byte *)pixbuf.Pixels;
				byte [] buffer = new byte [width * 3];
				
				for (int i = 0; i < height; i++) {
					stream.Read (buffer, 0, buffer.Length);
					    
					System.Runtime.InteropServices.Marshal.Copy (buffer, 0, 
										     (System.IntPtr)pixels, buffer.Length);
					
					pixels += pixbuf.Rowstride; 
				}
			}
			return pixbuf;
		}

		static PixelBuffer LoadBufferRGB16 (Stream stream, int width, int height)
		{
			PixelBuffer pix = new UInt16Buffer (width, height);
			int count = width * 3;
			byte [] buffer = new byte [count * 2];

			for (int row = 0; row < height; row++) {
				int len = 0;
				while (len < buffer.Length) {
					int read = stream.Read (buffer, len, buffer.Length - len);
					if (read < 0)
						break;
					len += read;
				}

				pix.Fill16 (row, 0, buffer, 0, count, false);
			}

			return pix;
		}

		static PixelBuffer LoadBufferRGB8 (Stream stream, int width, int height)
		{
			PixelBuffer pix = new UInt16Buffer (width, height);
			int length = width * 3;
			byte [] buffer = new byte [length];
			
			for (int row = 0; row < height; row++) {
				stream.Read (buffer, 0, buffer.Length);
				pix.Fill8 (row, 0, buffer, 0, buffer.Length);
			}

			return pix;
		}

		public static FSpot.Imaging.PixelBuffer LoadBuffer (Stream stream)
		{

			Header header = new Header (stream);
			header.Dump (); 

			switch (header.Magic) {
			case "P6":
				if (header.IsDeep)
					return LoadBufferRGB16 (stream, header.Width, header.Height);
				else
					return LoadBufferRGB8 (stream, header.Width, header.Height);
			default:
				throw new System.Exception (System.String.Format ("unknown pnm type {0}", header.Magic));
			}			
		}

		public override Gdk.Pixbuf Load ()
		{
			try {
				using (Stream stream = Open ()) {
					Gdk.Pixbuf pixbuf = PnmFile.Load (stream);
					return pixbuf;
				}
			} catch (System.Exception e) {
				System.Console.WriteLine (e.ToString ());
			}
			return null;
		}

		public override Gdk.Pixbuf Load (int width, int height)
		{
			return PixbufUtils.ScaleToMaxSize (this.Load (), width, height);
		}

		public override void Save (Gdk.Pixbuf pixbuf, System.IO.Stream stream)
		{
			if (pixbuf.HasAlpha)
				throw new NotImplementedException ();

			// FIXME this should be part of the header class
			string header = String.Format ("P6\n"
						       + "#Software: {0} {1}\n"
						       + "{2} {3}  #Width and Height\n"
						       + "255\n", 
						       FSpot.Defines.PACKAGE,
						       FSpot.Defines.VERSION,
						       pixbuf.Width, 
						       pixbuf.Height);
						       
			byte [] header_bytes = System.Text.Encoding.UTF8.GetBytes (header);
			stream.Write (header_bytes, 0, header.Length);
										 
			unsafe {
				byte * src_pixels = (byte *) pixbuf.Pixels;
				int src_stride = pixbuf.Rowstride;
				int count = pixbuf.Width * pixbuf.NChannels;
				int height = pixbuf.Height;

				for (int y = 0; y < height; y++) {
					for (int x = 0; x < count; x++) {
						stream.WriteByte (* (src_pixels + x));
					}
					src_pixels += src_stride;
				}
			}
		}

		public static Gdk.Pixbuf Load (Stream stream)
		{
			Header header = new Header (stream);
			header.Dump ();

			switch (header.Magic) {
			case "P6":
				if (header.IsDeep) {
#if SKIP_BUFFER					
					return LoadRGB16 (stream, header.Width, header.Height);
#else
					stream.Position = 0;
					FSpot.Imaging.PixelBuffer image = FSpot.Pnm.PnmFile.LoadBuffer (stream);
					Gdk.Pixbuf result = image.ToPixbuf (Cms.Profile.CreateStandardRgb ());
					return result;
#endif
				} else
					return LoadRGB8 (stream, header.Width, header.Height);
			default:
				throw new System.Exception (System.String.Format ("unknown pnm type {0}", header.Magic));
			}			
		}
	}

#if ENABLE_NUNIT
	[TestFixture]
	public class Tests {
		[Test]
		public void SaveLoad ()
		{
			using (Gdk.Pixbuf pixbuf = new Gdk.Pixbuf (null, "f-spot-32.png")) {
				Gdk.Pixbuf source = pixbuf;
				if (pixbuf.HasAlpha)
					source = PixbufUtils.Flatten (pixbuf);

				string path = ImageFile.TempPath ("test.ppm");
				PnmFile pnm = new PnmFile (path);
				using (Stream stream = File.OpenWrite (path)) {
					pnm.Save (source, stream);
				}

				pnm = new PnmFile (path);

				using (Gdk.Pixbuf saved = pnm.Load ()) {
					Assert.IsNotNull (saved);
					Assert.AreEqual (saved.Width, source.Width);
					Assert.AreEqual (saved.Height, source.Height);
				}
				
				if (source != pixbuf)
					source.Dispose ();

				File.Delete (path);
			}
		}
	}	
#endif
}
