#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.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using OpenRA.FileSystem;

namespace OpenRA.Graphics
{
	public sealed class Sheet : IDisposable
	{
		readonly object textureLock = new object();
		bool dirty;
		bool releaseBufferOnCommit;
		ITexture texture;
		byte[] data;

		public readonly Size Size;
		public byte[] GetData()
		{
			CreateBuffer();
			return data;
		}

		public bool Buffered { get { return data != null || texture == null; } }

		public Sheet(Size size)
		{
			Size = size;
		}

		public Sheet(ITexture texture)
		{
			this.texture = texture;
			Size = texture.Size;
		}

		public Sheet(string filename)
		{
			using (var stream = GlobalFileSystem.Open(filename))
			using (var bitmap = (Bitmap)Image.FromStream(stream))
			{
				Size = bitmap.Size;

				var dataStride = 4 * Size.Width;
				data = new byte[dataStride * Size.Height];

				var bd = bitmap.LockBits(bitmap.Bounds(),
					ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
				for (var y = 0; y < Size.Height; y++)
					Marshal.Copy(IntPtr.Add(bd.Scan0, y * bd.Stride), data, y * dataStride, dataStride);
				bitmap.UnlockBits(bd);
			}

			ReleaseBuffer();
		}

		public ITexture GetTexture()
		{
			// This is only called from the main thread but 'dirty'
			// is set from other threads too via CommitData().
			GenerateTexture();
			return texture;
		}

		void GenerateTexture()
		{
			lock (textureLock)
			{
				if (texture == null)
				{
					texture = Game.Renderer.Device.CreateTexture();
					dirty = true;
				}

				if (data != null)
				{
					if (dirty)
					{
						texture.SetData(data, Size.Width, Size.Height);
						dirty = false;
						if (releaseBufferOnCommit)
							data = null;
					}
				}
			}
		}

		public Bitmap AsBitmap()
		{
			var d = GetData();
			var dataStride = 4 * Size.Width;
			var bitmap = new Bitmap(Size.Width, Size.Height);

			var bd = bitmap.LockBits(bitmap.Bounds(),
				ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
			for (var y = 0; y < Size.Height; y++)
				Marshal.Copy(d, y * dataStride, IntPtr.Add(bd.Scan0, y * bd.Stride), dataStride);
			bitmap.UnlockBits(bd);

			return bitmap;
		}

		public Bitmap AsBitmap(TextureChannel channel, IPalette pal)
		{
			var d = GetData();
			var dataStride = 4 * Size.Width;
			var bitmap = new Bitmap(Size.Width, Size.Height);
			var channelOffset = (int)channel;

			var bd = bitmap.LockBits(bitmap.Bounds(),
				ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
			unsafe
			{
				var colors = (uint*)bd.Scan0;
				for (var y = 0; y < Size.Height; y++)
				{
					var dataRowIndex = y * dataStride + channelOffset;
					var bdRowIndex = y * bd.Stride / 4;
					for (var x = 0; x < Size.Width; x++)
					{
						var paletteIndex = d[dataRowIndex + 4 * x];
						colors[bdRowIndex + x] = pal[paletteIndex];
					}
				}
			}

			bitmap.UnlockBits(bd);

			return bitmap;
		}

		public void CreateBuffer()
		{
			lock (textureLock)
			{
				if (data != null)
					return;
				if (texture == null)
					data = new byte[4 * Size.Width * Size.Height];
				else
					data = texture.GetData();
				releaseBufferOnCommit = false;
			}
		}

		public void CommitData()
		{
			CommitData(false);
		}

		public void ReleaseBuffer()
		{
			CommitData(true);
		}

		void CommitData(bool releaseBuffer)
		{
			lock (textureLock)
			{
				if (!Buffered)
					throw new InvalidOperationException(
						"This sheet is unbuffered. You cannot call CommitData on an unbuffered sheet. " +
						"If you need to completely replace the texture data you should set data into the texture directly. " +
						"If you need to make only small changes to the texture data consider creating a buffered sheet instead.");

				dirty = true;
				if (releaseBuffer)
					releaseBufferOnCommit = true;
			}
		}

		public void Dispose()
		{
			if (texture != null)
				texture.Dispose();
		}
	}
}
