using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FreeImageAPI;

namespace AtlasGeneratorLib
{
	public class AtlasGenerator
	{
		public AtlasGenerator()
		{
			LogFunc = s => Console.WriteLine(s);

			GetFileStreamFunc = s =>
			{
				Process.Start("p4", "edit " + s)?.WaitForExit();
				return new FileStream(s, FileMode.Create);
			};
		}

		private void EnsureDirExists(string _sDirectory)
		{
			if (!Directory.Exists(_sDirectory))
			{
				LogFunc("Could not find target directory: " + _sDirectory + "\n" + "Creating directory: " +
				        _sDirectory);

				Directory.CreateDirectory(_sDirectory);
			}
		}

		public IEnumerable<string> CreateAtlases(string _sRootPath, string _sTargetPath, uint _iAtlasSize,
		                                         uint _iMargin, uint _iAlignment)
		{
			var parallelOptions = new ParallelOptions
			{
				/* MaxDegreeOfParallelism = 1  */
			};

			int iStart = System.Environment.TickCount;

			EnsureDirExists(_sTargetPath);

			BlockingCollection<string> lImagePaths = new BlockingCollection<string>();

			if (Directory.Exists(_sRootPath))
			{
				Parallel.ForEach(Directory.EnumerateFiles(_sRootPath, "*.png", SearchOption.AllDirectories),
				                 parallelOptions, sFile =>
				                 {
					                 lImagePaths.Add(sFile);
				                 });
				Parallel.ForEach(Directory.EnumerateFiles(_sRootPath, "*.jpg", SearchOption.AllDirectories),
				                 parallelOptions, sFile =>
				                 {
					                 lImagePaths.Add(sFile);
				                 });
			}

			BlockingCollection<ImageInfo> lImages = new BlockingCollection<ImageInfo>();
			int maxImageSize = 0;

			Parallel.ForEach(lImagePaths, parallelOptions, sPath =>
			{
				var size = ImageHelper.GetDimensions(sPath);
				lImages.Add(new ImageInfo(sPath, size.Width, size.Height));
				maxImageSize = Math.Max(maxImageSize, Math.Max(size.Width, size.Height));
			});

			int iEndReading = System.Environment.TickCount;
			LogFunc(string.Format("Files read in {0}ms", iEndReading - iStart));
			int iStartBuildMeta = System.Environment.TickCount;

			// pack the images onto as little atlasses as possible
			// if only 1 atlas is used try to find the smallest one that works, but keep all atlasses at the same size
			List<Atlas> buildAtlases = null;
			uint atlasSize = Math.Min(32, _iAtlasSize);

			do
			{
				atlasSize *= 2;

				if (atlasSize >= maxImageSize || atlasSize >= _iAtlasSize)
				{
					buildAtlases =
						SimpleBestFitDecreasing.BuildAtlases(lImages.ToList(), atlasSize, _iMargin, _iAlignment);
				}
			} while ((buildAtlases == null || buildAtlases.Count > 1) && atlasSize < _iAtlasSize);

			int iEndBuildMeta = System.Environment.TickCount;
			LogFunc(string.Format("Metadata generated in {0}ms", iEndBuildMeta - iStartBuildMeta));
			int iStartBuildBitmap = System.Environment.TickCount;

			SaveAtlases(buildAtlases, _sTargetPath);

			int iEndBuildBitmap = System.Environment.TickCount;

			LogFunc(string.Format("Bitmap generated in {0}ms", iEndBuildBitmap - iStartBuildBitmap));

			int iStartSafeBitmap = System.Environment.TickCount;

			var result = SaveMetadata(buildAtlases, _sRootPath, _sTargetPath);

			int iEnd = System.Environment.TickCount;
			LogFunc(string.Format("Written Files to system in {0}ms", iEnd - iStartSafeBitmap));
			LogFunc(string.Format("Generate in {0}ms", iEnd - iStart));

			return result;
		}

		public Action<string> LogFunc
		{
			get;
			set;
		}

		public Func<string, Stream> GetFileStreamFunc
		{
			get;
			set;
		}

		private IEnumerable<string> SaveMetadata(List<Atlas> _lAtlases, string _sRootPath, string _sTargetPath)
		{
			var files = new List<string>();
			var sBrushPath = Path.Combine(_sTargetPath, "atlas.json");
			_sRootPath = Path.GetFullPath(_sRootPath);

			using (var stream = GetFileStreamFunc(sBrushPath))
			{
				using (var brushsheet = new StreamWriter(stream))
				{
					brushsheet.WriteLine("<Brushes>");

					for (int i = 0; i < _lAtlases.Count; i++)
					{
						for (int j = 0; j < _lAtlases[i].m_lInsertedImages.Count; j++)
						{
							var image = _lAtlases[i].m_lInsertedImages[j];

							string imagePath = Path.GetFullPath(image.m_Info.m_sPath).Substring(_sRootPath.Length + 1);
							imagePath = imagePath.Replace("\\", "/").Replace(".png", "");
							imagePath = imagePath.Replace("\\", "/").Replace(".jpg", "");

							brushsheet.WriteLine("\t<Brush Name=\"" + imagePath + "\" Width=\"" + image.m_Space.x +
							                 "\" Height=\"" + image.m_Space.y + "\" X=\"" + image.m_Pos.x +
							                 "\" Y=\"" + image.m_Pos.y + "\" Id=\"" + i + "\"></Brush>");
						}
					}

					brushsheet.WriteLine("</Brushes>");
				}
			}

			files.Add(sBrushPath);

			return files;
		}

		private unsafe void SaveAtlases(List<Atlas> _lAtlases, string _sTargetAtlasFolder)
		{
			Parallel.ForEach(_lAtlases, atlas =>
			{
				using (var atlasBitmap = new Bitmap((int) atlas.m_iSize, (int) atlas.m_iSize))
				{
					var pixelFormat = atlasBitmap.PixelFormat;

					var destData = atlasBitmap.LockBits(new Rectangle(0, 0, (int) atlas.m_iSize, (int) atlas.m_iSize),
					                                    ImageLockMode.WriteOnly, pixelFormat);

					Parallel.ForEach(atlas.m_lInsertedImages, image =>
					{
						using (var stream =
							new FileStream(image.m_Info.m_sPath, FileMode.Open, FileAccess.Read, FileShare.Read))
						{
							FIBITMAP bmp;

							try
							{
								bmp = FreeImage.LoadFromStream(stream);
								var converted = FreeImage.ConvertTo32Bits(bmp);
								FreeImage.Unload(bmp);
								bmp = converted;
							}
							catch (Exception)
							{
								Console.Error.WriteLine("Error while loading '" + image.m_Info.m_sPath + "'");

								throw;
							}

							var srcBits = (byte*) FreeImage.GetBits(bmp);
							var iHeight = FreeImage.GetHeight(bmp);
							var iWidth = FreeImage.GetWidth(bmp);

							Parallel.For(0, iHeight, y =>
							{
								var destPtr = (int*) ((byte*) destData.Scan0 +
								                      (image.m_Pos.y + iHeight - 1 - y) * destData.Stride +
								                      image.m_Pos.x * 4);

								var srcPtr = (int*) (srcBits + y * iWidth * 4);
								var srcPtrEnd = srcPtr + iWidth;

								do
								{
									*destPtr++ = *srcPtr++;
								} while (srcPtr != srcPtrEnd);
							});

							FreeImage.Unload(bmp);
						}
					});

					atlasBitmap.UnlockBits(destData);

					string sName = Path.Combine(_sTargetAtlasFolder, "atlas" + _lAtlases.IndexOf(atlas) + ".png");

					using (var stream = GetFileStreamFunc(sName))
					{
						atlasBitmap.Save(stream, ImageFormat.Png);
					}
				}
			});
		}
	}
}
