/*
Copyright (c) 2018, John Lewin
 */

using System;
using System.Diagnostics;
using System.Linq;
using MatterHackers.Agg;
using MatterHackers.Agg.Image;
using MatterHackers.PolygonMesh;
using MatterHackers.VectorMath;

namespace MatterHackers.MatterControl.Plugins.Lithophane
{
	public static class Lithophane
	{
		class PixelInfo
		{
			public IVertex Top { get; set; }
			public IVertex Bottom { get; set; }
		}

		public static Mesh Generate(IImageData resizedImage, double maxZ, double nozzleWidth, double pixelsPerMM, bool invert, IProgress<ProgressStatus> reporter)
		{
			// TODO: Move this to a user supplied value
			double baseThickness = nozzleWidth;     // base thickness (in mm)
			double zRange = maxZ - baseThickness;

			// Dimensions of image
			var width = resizedImage.Width;
			var height = resizedImage.Height;

			var zScale = zRange / 255;

			var pixelData = resizedImage.Pixels;

			Stopwatch stopwatch = Stopwatch.StartNew();

			var mesh = new Mesh();

			//var rescale = (double)onPlateWidth / imageData.Width;
			var rescale = 1;

			var progressStatus = new ProgressStatus();

			// Build an array of PixelInfo objects from each pixel
			// Collapse from 4 bytes per pixel to one - makes subsequent processing more logical and has minimal cost
			var pixels = pixelData.Where((x, i) => i % 4 == 0)

				// Interpolate the pixel color to zheight
				.Select(b => baseThickness + (invert ? 255 - b : b) * zScale)

				// Create a Vector3 for each pixel at the computed x/y/z
				.Select((z, i) => mesh.CreateVertex(new Vector3(
					i % width * rescale,
					(i - i % width) / width * rescale * -1,
					z)))

				// Create a mirrored vector for the pixel at z0 and return with top/bottom paired together
				.Select(vec => new PixelInfo()
				{
					Top = vec,
					Bottom = mesh.CreateVertex(new Vector3(
						vec.Position.X,
						vec.Position.Y,
						0))
				}).ToArray();

			Console.WriteLine("ElapsedTime - PixelInfo Linq Generation: {0}", stopwatch.ElapsedMilliseconds);
			stopwatch.Restart();

			// Select pixels along image edges
			var backRow = pixels.Take(width).Reverse().ToArray();
			var frontRow = pixels.Skip((height - 1) * width).Take(width).ToArray();
			var leftRow = pixels.Where((x, i) => i % width == 0).ToArray();
			var rightRow = pixels.Where((x, i) => (i + 1) % width == 0).Reverse().ToArray();

			int k,
				nextJ,
				nextK;

			var notificationInterval = 100;

			var workCount = (resizedImage.Width - 1) * (resizedImage.Height - 1) +
							(height - 1) +
							(width - 1);

			double workIndex = 0;

			// Vertical faces: process each row and column, creating the top and bottom faces as appropriate
			for (int i = 0; i < resizedImage.Height - 1; ++i)
			{
				var startAt = i * width;

				// Process each column
				for (int j = startAt; j < startAt + resizedImage.Width - 1; ++j)
				{
					k = j + 1;
					nextJ = j + resizedImage.Width;
					nextK = nextJ + 1;

					// Create north, then south face
					mesh.CreateFace(new IVertex[] { pixels[k].Top, pixels[j].Top, pixels[nextJ].Top, pixels[nextK].Top });
					mesh.CreateFace(new IVertex[] { pixels[j].Bottom, pixels[k].Bottom, pixels[nextK].Bottom, pixels[nextJ].Bottom });
					workIndex++;

					if (workIndex % notificationInterval == 0)
					{
						progressStatus.Progress0To1 = workIndex / workCount;
						reporter.Report(progressStatus);
					}
				}
			}

			// Side faces: East/West
			for (int j = 0; j < height - 1; ++j)
			{
				//Next row
				k = j + 1;

				// Create east, then west face
				mesh.CreateFace(new IVertex[] { leftRow[k].Top, leftRow[j].Top, leftRow[j].Bottom, leftRow[k].Bottom });
				mesh.CreateFace(new IVertex[] { rightRow[k].Top, rightRow[j].Top, rightRow[j].Bottom, rightRow[k].Bottom });
				workIndex++;

				if (workIndex % notificationInterval == 0)
				{
					progressStatus.Progress0To1 = workIndex / workCount;
					reporter.Report(progressStatus);
				}
			}

			// Side faces: North/South
			for (int j = 0; j < width - 1; ++j)
			{
				// Next row
				k = j + 1;

				// Create north, then south face
				mesh.CreateFace(new IVertex[] { frontRow[k].Top, frontRow[j].Top, frontRow[j].Bottom, frontRow[k].Bottom });
				mesh.CreateFace(new IVertex[] { backRow[k].Top, backRow[j].Top, backRow[j].Bottom, backRow[k].Bottom });
				workIndex++;

				if (workIndex % notificationInterval == 0)
				{
					progressStatus.Progress0To1 = workIndex / workCount;
					reporter.Report(progressStatus);
				}
			}

			Console.WriteLine("ElapsedTime - Face Generation: {0}", stopwatch.ElapsedMilliseconds);

			return mesh;
		}

		public interface IImageData
		{
			byte[] Pixels { get; }

			int Width { get; }
			int Height { get; }
		}

		public class ImageBufferImageData : IImageData
		{
			ImageBuffer resizedImage;

			public ImageBufferImageData(ImageBuffer image, double pixelWidth)
			{
				resizedImage = this.ToResizedGrayscale(image, pixelWidth);
				resizedImage.FlipY();
			}

			public int Width => resizedImage.Width;
			public int Height => resizedImage.Height;

			private ImageBuffer ToResizedGrayscale(ImageBuffer image, double onPlateWidth = 0)
			{
				var ratio = onPlateWidth / image.Width;

				var resizedImage = image.CreateScaledImage(ratio);

				var grayImage = resizedImage.ToGrayscale();

				// Render grayscale pixels onto resized image with larger pixel format needed by caller
				resizedImage.NewGraphics2D().Render(grayImage, 0, 0);

				return resizedImage;
			}
			
			public byte[] Pixels => resizedImage.GetBuffer();
		}
	}
}
