// This test case is taken from https://blogs.msdn.microsoft.com/lukeh/2007/10/01/taking-linq-to-objects-to-extremes-a-fully-linqified-raytracer/

using System.Linq;
using System;
using System.Collections.Generic;

namespace RayTracer
{
	public class RayTracer
	{
		static void Main()
		{
			const int width = 50;
			const int height = 50;

			RayTracer rayTracer = new RayTracer(width, height, (int x, int y, Color color) => {
				Console.Write("{0},{1}:{2};", x, y, color);
			});
			rayTracer.Render(rayTracer.DefaultScene);
		}

		private int screenWidth;
		private int screenHeight;
		private const int MaxDepth = 5;

		public Action<int, int, Color> setPixel;

		public RayTracer(int screenWidth, int screenHeight, Action<int, int, Color> setPixel)
		{
			this.screenWidth = screenWidth;
			this.screenHeight = screenHeight;
			this.setPixel = setPixel;
		}

		private class Wrap<T>
		{
			public readonly Func<Wrap<T>, T> It;
			public Wrap(Func<Wrap<T>, T> it) { It = it; }
		}

		public static Func<T, U> Y<T, U>(Func<Func<T, U>, Func<T, U>> f)
		{
			Func<Wrap<Func<T, U>>, Func<T, U>> g = wx => f(wx.It(wx));
			return g(new Wrap<Func<T, U>>(wx => f(y => wx.It(wx)(y))));
		}

		class TraceRayArgs
		{
			public readonly Ray Ray;
			public readonly Scene Scene;
			public readonly int Depth;

			public TraceRayArgs(Ray ray, Scene scene, int depth) { Ray = ray; Scene = scene; Depth = depth; }
		}

		internal void Render(Scene scene)
		{
			var pixelsQuery =
				from y in Enumerable.Range(0, screenHeight)
				let recenterY = -(y - (screenHeight / 2.0)) / (2.0 * screenHeight)
				select from x in Enumerable.Range(0, screenWidth)
					   let recenterX = (x - (screenWidth / 2.0)) / (2.0 * screenWidth)
					   let point =
						   Vector.Norm(Vector.Plus(scene.Camera.Forward,
												   Vector.Plus(Vector.Times(recenterX, scene.Camera.Right),
															   Vector.Times(recenterY, scene.Camera.Up))))
					   let ray = new Ray() { Start = scene.Camera.Pos, Dir = point }
					   let computeTraceRay = (Func<Func<TraceRayArgs, Color>, Func<TraceRayArgs, Color>>)
						(f => traceRayArgs =>
						 (from isect in
							  from thing in traceRayArgs.Scene.Things
							  select thing.Intersect(traceRayArgs.Ray)
						  where isect != null
						  orderby isect.Dist
						  let d = isect.Ray.Dir
						  let pos = Vector.Plus(Vector.Times(isect.Dist, isect.Ray.Dir), isect.Ray.Start)
						  let normal = isect.Thing.Normal(pos)
						  let reflectDir = Vector.Minus(d, Vector.Times(2 * Vector.Dot(normal, d), normal))
						  let naturalColors =
							  from light in traceRayArgs.Scene.Lights
							  let ldis = Vector.Minus(light.Pos, pos)
							  let livec = Vector.Norm(ldis)
							  let testRay = new Ray() { Start = pos, Dir = livec }
							  let testIsects = from inter in
												   from thing in traceRayArgs.Scene.Things
												   select thing.Intersect(testRay)
											   where inter != null
											   orderby inter.Dist
											   select inter
							  let testIsect = testIsects.FirstOrDefault()
							  let neatIsect = testIsect == null ? 0 : testIsect.Dist
							  let isInShadow = !((neatIsect > Vector.Mag(ldis)) || (neatIsect == 0))
							  where !isInShadow
							  let illum = Vector.Dot(livec, normal)
							  let lcolor = illum > 0 ? Color.Times(illum, light.Color) : Color.Make(0, 0, 0)
							  let specular = Vector.Dot(livec, Vector.Norm(reflectDir))
							  let scolor = specular > 0
											 ? Color.Times(Math.Pow(specular, isect.Thing.Surface.Roughness),
														   light.Color)
											 : Color.Make(0, 0, 0)
							  select Color.Plus(Color.Times(isect.Thing.Surface.Diffuse(pos), lcolor),
												Color.Times(isect.Thing.Surface.Specular(pos), scolor))
						  let reflectPos = Vector.Plus(pos, Vector.Times(.001, reflectDir))
						  let reflectColor = traceRayArgs.Depth >= MaxDepth
											  ? Color.Make(.5, .5, .5)
											  : Color.Times(isect.Thing.Surface.Reflect(reflectPos),
															f(new TraceRayArgs(new Ray() {
																Start = reflectPos,
																Dir = reflectDir
															},
																			   traceRayArgs.Scene,
																			   traceRayArgs.Depth + 1)))
						  select naturalColors.Aggregate(reflectColor,
														 (color, natColor) => Color.Plus(color, natColor))
						 ).DefaultIfEmpty(Color.Background).First())
					   let traceRay = Y(computeTraceRay)
					   select new { X = x, Y = y, Color = traceRay(new TraceRayArgs(ray, scene, 0)) };

			foreach (var row in pixelsQuery)
				foreach (var pixel in row)
					setPixel(pixel.X, pixel.Y, pixel.Color);
		}

		internal readonly Scene DefaultScene =
			new Scene() {
				Things = new SceneObject[] {
								new Plane() {
									Norm = Vector.Make(0,1,0),
									Offset = 0,
									Surface = Surfaces.CheckerBoard
								},
								new Sphere() {
									Center = Vector.Make(0,1,0),
									Radius = 1,
									Surface = Surfaces.Shiny
								},
								new Sphere() {
									Center = Vector.Make(-1,.5,1.5),
									Radius = .5,
									Surface = Surfaces.Shiny
								}},
				Lights = new Light[] {
								new Light() {
									Pos = Vector.Make(-2,2.5,0),
									Color = Color.Make(.49,.07,.07)
								},
								new Light() {
									Pos = Vector.Make(1.5,2.5,1.5),
									Color = Color.Make(.07,.07,.49)
								},
								new Light() {
									Pos = Vector.Make(1.5,2.5,-1.5),
									Color = Color.Make(.07,.49,.071)
								},
								new Light() {
									Pos = Vector.Make(0,3.5,0),
									Color = Color.Make(.21,.21,.35)
								}},
				Camera = Camera.Create(Vector.Make(3, 2, 4), Vector.Make(-1, .5, 0))
			};
	}

	static class Surfaces
	{
		// Only works with X-Z plane.
		public static readonly Surface CheckerBoard =
			new Surface() {
				Diffuse = pos => ((Math.Floor(pos.Z) + Math.Floor(pos.X)) % 2 != 0)
									? Color.Make(1, 1, 1)
									: Color.Make(0, 0, 0),
				Specular = pos => Color.Make(1, 1, 1),
				Reflect = pos => ((Math.Floor(pos.Z) + Math.Floor(pos.X)) % 2 != 0)
									? .1
									: .7,
				Roughness = 150
			};


		public static readonly Surface Shiny =
			new Surface() {
				Diffuse = pos => Color.Make(1, 1, 1),
				Specular = pos => Color.Make(.5, .5, .5),
				Reflect = pos => .6,
				Roughness = 50
			};
	}

	class Vector
	{
		public readonly double X;
		public readonly double Y;
		public readonly double Z;

		public Vector(double x, double y, double z) { X = x; Y = y; Z = z; }

		public static Vector Make(double x, double y, double z) { return new Vector(x, y, z); }
		public static Vector Times(double n, Vector v)
		{
			return new Vector(v.X * n, v.Y * n, v.Z * n);
		}
		public static Vector Minus(Vector v1, Vector v2)
		{
			return new Vector(v1.X - v2.X, v1.Y - v2.Y, v1.Z - v2.Z);
		}
		public static Vector Plus(Vector v1, Vector v2)
		{
			return new Vector(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
		}
		public static double Dot(Vector v1, Vector v2)
		{
			return (v1.X * v2.X) + (v1.Y * v2.Y) + (v1.Z * v2.Z);
		}
		public static double Mag(Vector v) { return Math.Sqrt(Dot(v, v)); }
		public static Vector Norm(Vector v)
		{
			double mag = Mag(v);
			double div = mag == 0 ? double.PositiveInfinity : 1 / mag;
			return Times(div, v);
		}
		public static Vector Cross(Vector v1, Vector v2)
		{
			return new Vector(((v1.Y * v2.Z) - (v1.Z * v2.Y)),
							  ((v1.Z * v2.X) - (v1.X * v2.Z)),
							  ((v1.X * v2.Y) - (v1.Y * v2.X)));
		}
		public static bool Equals(Vector v1, Vector v2)
		{
			return (v1.X == v2.X) && (v1.Y == v2.Y) && (v1.Z == v2.Z);
		}
	}

	public class Color
	{
		public double R;
		public double G;
		public double B;

		public Color(double r, double g, double b) { R = r; G = g; B = b; }

		public static Color Make(double r, double g, double b) { return new Color(r, g, b); }

		public static Color Times(double n, Color v)
		{
			return new Color(n * v.R, n * v.G, n * v.B);
		}
		public static Color Times(Color v1, Color v2)
		{
			return new Color(v1.R * v2.R, v1.G * v2.G, v1.B * v2.B);
		}

		public static Color Plus(Color v1, Color v2)
		{
			return new Color(v1.R + v2.R, v1.G + v2.G, v1.B + v2.B);
		}
		public static Color Minus(Color v1, Color v2)
		{
			return new Color(v1.R - v2.R, v1.G - v2.G, v1.B - v2.B);
		}

		public static readonly Color Background = Make(0, 0, 0);
		public static readonly Color DefaultColor = Make(0, 0, 0);

		private double Legalize(double d)
		{
			return d > 1 ? 1 : d;
		}

		public override string ToString()
		{
			return string.Format("[{0},{1},{2}]", R, G, B);
		}
	}

	class Ray
	{
		public Vector Start;
		public Vector Dir;
	}

	class ISect
	{
		public SceneObject Thing;
		public Ray Ray;
		public double Dist;
	}

	class Surface
	{
		public Func<Vector, Color> Diffuse;
		public Func<Vector, Color> Specular;
		public Func<Vector, double> Reflect;
		public double Roughness;
	}

	class Camera
	{
		public Vector Pos;
		public Vector Forward;
		public Vector Up;
		public Vector Right;

		public static Camera Create(Vector pos, Vector lookAt)
		{
			Vector forward = Vector.Norm(Vector.Minus(lookAt, pos));
			Vector down = new Vector(0, -1, 0);
			Vector right = Vector.Times(1.5, Vector.Norm(Vector.Cross(forward, down)));
			Vector up = Vector.Times(1.5, Vector.Norm(Vector.Cross(forward, right)));

			return new Camera() { Pos = pos, Forward = forward, Up = up, Right = right };
		}
	}

	class Light
	{
		public Vector Pos;
		public Color Color;
	}

	abstract class SceneObject
	{
		public Surface Surface;
		public abstract ISect Intersect(Ray ray);
		public abstract Vector Normal(Vector pos);
	}

	class Sphere : SceneObject
	{
		public Vector Center;
		public double Radius;

		public override ISect Intersect(Ray ray)
		{
			Vector eo = Vector.Minus(Center, ray.Start);
			double v = Vector.Dot(eo, ray.Dir);
			double dist;
			if (v < 0) {
				dist = 0;
			} else {
				double disc = Math.Pow(Radius, 2) - (Vector.Dot(eo, eo) - Math.Pow(v, 2));
				dist = disc < 0 ? 0 : v - Math.Sqrt(disc);
			}
			if (dist == 0) return null;
			return new ISect() {
				Thing = this,
				Ray = ray,
				Dist = dist
			};
		}

		public override Vector Normal(Vector pos)
		{
			return Vector.Norm(Vector.Minus(pos, Center));
		}
	}

	class Plane : SceneObject
	{
		public Vector Norm;
		public double Offset;

		public override ISect Intersect(Ray ray)
		{
			double denom = Vector.Dot(Norm, ray.Dir);
			if (denom > 0) return null;
			return new ISect() {
				Thing = this,
				Ray = ray,
				Dist = (Vector.Dot(Norm, ray.Start) + Offset) / (-denom)
			};
		}

		public override Vector Normal(Vector pos)
		{
			return Norm;
		}
	}

	class Scene
	{
		public SceneObject[] Things;
		public Light[] Lights;
		public Camera Camera;

		public IEnumerable<ISect> Intersect(Ray r)
		{
			return from thing in Things
				   select thing.Intersect(r);
		}
	}
}