using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Kinect;
using Microsoft.Kinect.Toolkit;
using Microsoft.Kinect.Toolkit.Interaction;

namespace KinPortal {
	public class HandsTracker : IInteractionClient {
		public double InteractionRegionWidth {get; set;}
		public double InteractionRegionHeight {get; set;}
		private const int InvalidTrackingId = 0;
		private readonly KinectSensorChooser sensorChooser = new KinectSensorChooser();
		private readonly HashSet<int> trackedUsers = new HashSet<int>();

		private InteractionStream interactionStream;
        private KinectSensor kinectSensor;
        private Skeleton[] skeletons;
        private UserInfo[] userInfos;

		public HandsState LeftHand {get; private set;}
		public HandsState RightHand {get; private set;}

		public HandsTracker(double screenWidth, double screenHeight) {
			InteractionRegionWidth = screenWidth;
			InteractionRegionHeight = screenHeight;
			LeftHand = new HandsState();
			RightHand = new HandsState();
		}

		public void Run() {
			this.sensorChooser.KinectChanged += this.OnSensorChanged;
			this.sensorChooser.Start();
		}
        
		#region PressAndGripAdjustment
		public InteractionInfo GetInteractionInfoAtLocation(int skeletonTrackingId, InteractionHandType handType, double x, double y) {
			return new InteractionInfo();
		}
		#endregion PressAndGripAdjustment
        

		#region Configuration
		private void InitializeInteractions(KinectSensor sensor) {
			this.skeletons = new Skeleton[sensor.SkeletonStream.FrameSkeletonArrayLength];
			this.userInfos = new UserInfo[InteractionFrame.UserInfoArrayLength];

			sensor.DepthFrameReady += this.SensorDepthFrameReady;
			sensor.SkeletonFrameReady += this.SensorSkeletonFrameReady;
			
			this.interactionStream = new InteractionStream(sensor, this);
			this.interactionStream.InteractionFrameReady += this.InteractionFrameReady;
		}

		private void UninitializeInteractions(KinectSensor sensor) {
			sensor.DepthFrameReady -= this.SensorDepthFrameReady;
			sensor.SkeletonFrameReady -= this.SensorSkeletonFrameReady;
			
			this.skeletons = null;
			this.userInfos = null;
			
			this.interactionStream.InteractionFrameReady -= this.InteractionFrameReady;
			this.interactionStream.Dispose();
			this.interactionStream = null;
		}

		private void OnSensorChanged(object sender, KinectChangedEventArgs args) {
			if (args.OldSensor != null) {
				this.UninitializeInteractions(args.OldSensor);
				try {
					args.OldSensor.SkeletonStream.AppChoosesSkeletons = false;
					args.OldSensor.DepthStream.Range = DepthRange.Default;
					args.OldSensor.SkeletonStream.EnableTrackingInNearRange = false;
					args.OldSensor.DepthStream.Disable();
					args.OldSensor.SkeletonStream.Disable();
				} catch (InvalidOperationException) {
					// KinectSensor might enter an invalid state while enabling/disabling streams or stream features.
					// E.g.: sensor might be abruptly unplugged.
				}
			}

			this.kinectSensor = null;

			if (args.NewSensor != null) {
				try {
					// InteractionStream needs 640x480 depth data stream
					args.NewSensor.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);
					args.NewSensor.SkeletonStream.Enable();

                    try {
						// Interactions work better in near range
						args.NewSensor.DepthStream.Range = DepthRange.Near;
						args.NewSensor.SkeletonStream.EnableTrackingInNearRange = true;
					} catch (InvalidOperationException) {
						// Non Kinect for Windows devices do not support Near mode, so reset back to default mode.
						args.NewSensor.DepthStream.Range = DepthRange.Default;
						args.NewSensor.SkeletonStream.EnableTrackingInNearRange = false;
					}
				} catch (InvalidOperationException) {
					// KinectSensor might enter an invalid state while enabling/disabling streams or stream features.
					// E.g.: sensor might be abruptly unplugged.
				}

				this.kinectSensor = args.NewSensor;

				this.InitializeInteractions(args.NewSensor);
			}
		}

		#endregion Configuration

        #region Processing
		private void SensorDepthFrameReady(object sender, DepthImageFrameReadyEventArgs depthImageFrameReadyEventArgs) {
			if (this.kinectSensor != sender) {
				return;
			}

			using (DepthImageFrame depthFrame = depthImageFrameReadyEventArgs.OpenDepthImageFrame()) {
				if (null != depthFrame) {
					try {
						// Hand data to Interaction framework to be processed
						this.interactionStream.ProcessDepth(depthFrame.GetRawPixelData(), depthFrame.Timestamp);
					} catch (InvalidOperationException) {
						// DepthFrame functions may throw when the sensor gets
						// into a bad state.  Ignore the frame in that case.
					}
				}
			}
		}

		private void SensorSkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs skeletonFrameReadyEventArgs) {
			if (this.kinectSensor != sender) {
				return;
			}

			using (SkeletonFrame skeletonFrame = skeletonFrameReadyEventArgs.OpenSkeletonFrame()) {
				if (null != skeletonFrame) {
					try {
						skeletonFrame.CopySkeletonDataTo(this.skeletons);
						var accelerometerReading = this.kinectSensor.AccelerometerGetCurrentReading();
						this.interactionStream.ProcessSkeleton(this.skeletons, accelerometerReading, skeletonFrame.Timestamp);
					} catch (InvalidOperationException) {
						// SkeletonFrame functions may throw when the sensor gets
						// into a bad state.  Ignore the frame in that case.
					}
				}
			}
		}

		private void InteractionFrameReady(object sender, InteractionFrameReadyEventArgs e) {
		    // Check for a null userInfos since we may still get posted events
		    // from the stream after we have unregistered our event handler and
		    // deleted our buffers.
		    if (this.userInfos == null) {
		        return;
		    }

		    UserInfo[] localUserInfos = null;
		    long timestamp = 0;
			
			using (InteractionFrame interactionFrame = e.OpenInteractionFrame()) {
			    if (interactionFrame != null) {
			        interactionFrame.CopyInteractionDataTo(this.userInfos);
			        timestamp = interactionFrame.Timestamp;
			        localUserInfos = this.userInfos;
			    }
			}

			if (localUserInfos != null) {
				var currentUserSet = new HashSet<int>();
				var usersToRemove = new HashSet<int>();

				foreach (var info in localUserInfos) {
				    if (info.SkeletonTrackingId == InvalidTrackingId) {
				        // Only look at user information corresponding to valid users
				        continue;
				    }
					
					/*if (!this.trackedUsers.Contains(info.SkeletonTrackingId)) {
					    Console.WriteLine("New user '{0}' entered scene at time {1}", info.SkeletonTrackingId, timestamp);
					}*/

					currentUserSet.Add(info.SkeletonTrackingId);
					this.trackedUsers.Add(info.SkeletonTrackingId);
					
					// Perform hit testing and look for Grip and GripRelease events
					foreach (var handPointer in info.HandPointers) { 
						if (handPointer.HandType.ToString() == "Right") {
							RightHand.Pos = new Point(
								(int) ((handPointer.X / 2) * InteractionRegionWidth),
								(int) ((handPointer.Y / 2) * InteractionRegionHeight)
							);
							RightHand.Depth = handPointer.RawZ;
							RightHand.PrevIsClosed = RightHand.IsClosed;
							
							if(handPointer.HandEventType != InteractionHandEventType.None) {
								if (handPointer.HandEventType.ToString() == "Grip")
									RightHand.IsClosed = true;
								else
									RightHand.IsClosed = false;
							}
						} else {
							LeftHand.Pos = new Point(
								(int) ((handPointer.X / 2) * InteractionRegionWidth),
								(int) ((handPointer.Y / 2) * InteractionRegionHeight)
							);
							LeftHand.Depth = handPointer.RawZ;
							LeftHand.PrevIsClosed = LeftHand.IsClosed;

							if (handPointer.HandEventType != InteractionHandEventType.None) {
								if (handPointer.HandEventType.ToString() == "Grip")
									LeftHand.IsClosed = true;
								else
									LeftHand.IsClosed = false;
							}
						} 
					}
				}

				foreach (var id in this.trackedUsers) {
				    if (!currentUserSet.Contains(id)) {
				        usersToRemove.Add(id);
				    }
				}

				foreach (var id in usersToRemove) {
				    this.trackedUsers.Remove(id);
				}
			}
		}
		
		#endregion Processing
	}
}
