using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.Kinect;
using Microsoft.Speech.Recognition;
using System.Windows.Threading;
using System.Threading.Tasks;
using System.Threading;

namespace Kinect_CameraControl
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {

        # region Private State

        /// <summary>
        /// Rate at which the game thread is being updated.
        /// </summary>
        private const int TimerResolution = 500;  // ms

        /// <summary>
        /// The Kinect.
        /// </summary>
        private KinectSensor sensor;

        /// <summary>
        /// Speech recognizer: Identifies voice commands.
        /// </summary>
        private SpeechRecognizer mySpeechRecognizer;

        /// <summary>
        /// Timer to manage elevation update tasks. 
        /// </summary>
        private readonly DispatcherTimer debounce = new DispatcherTimer { IsEnabled = false, Interval = TimeSpan.FromMilliseconds(200) };

        /// <summary>
        /// Current elevation.
        /// </summary>
        private int elevation;

        /// <summary>
        /// Flag. Helps the management of the elevation update tasks.
        /// </summary>
        private bool backgroundUpdateInProgress = false;

        /// <summary>
        /// Counter. Helps the management of the opacity of label 1.
        /// </summary>
        private int labelAnimationCount = 0;

        /// <summary>
        /// Players.
        /// </summary>
        private readonly Dictionary<int, Player> players = new Dictionary<int, Player>();

        /// <summary>
        /// Current skeleton data for all the players.
        /// </summary>
        private Skeleton[] skeletonData;

        /// <summary>
        /// Flag. Controls when the game thread should stop.
        /// </summary>
        private bool runningGameThread;

        /// <summary>
        /// Rate control. This counter is used to control the rate at which the depth view is updated.
        /// </summary>
        int countDepth = 0;

        /// <summary>
        /// Flag. Controls if a command from the skeleton is being updated.
        /// </summary>
        bool skeletonEventUpdate = false;

        /// <summary>
        /// Help window.
        /// </summary>
        private HelpWindow helpWindow;

        #endregion Private State

        #region ctor + Window Events

        /// <summary>
        /// Initializes the window.
        /// </summary>
        public MainWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// This method is called when the window is loaded.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            //Sets the callback to identify a new Kinect sensor.
            kinectSensorChooser1.KinectSensorChanged += new DependencyPropertyChangedEventHandler(kinectSensorChooser1_KinectSensorChanged);

            //Sets the callback for the debounce clock tick.
            this.debounce.Tick += this.DebounceElapsed;
            
            //Starts the game thread.
            var myGameThread = new Thread(this.GameThread);
            myGameThread.Start();
        }

        /// <summary>
        /// This method is called when the application is being closed.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            //Stop the game thread.
            this.runningGameThread = false;

            //Stop and erase the sensor.
            sensor = null;
            kinectSkeletonViewer1.Kinect = null;
            StopKinect(kinectSensorChooser1.Kinect);

            //Close the help window if open.
            if (helpWindow != null)
                helpWindow.Close();
        }

        /// <summary>
        /// Help button. Open/Close the help window.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void image1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (helpWindow == null)
            {
                helpWindow = new HelpWindow();
                helpWindow.Top = this.Top;
                helpWindow.Left = this.Left + this.Width + 10;
                helpWindow.setSensor(sensor);
                helpWindow.Show();
            }
            else
            {
                helpWindow.Close();
                helpWindow = null;
            }
        }

        # endregion ctor + Window Events

        #region Kinect discovery + setup


        /// <summary>
        /// This callback is invoked each time a new Kinect sensor is identified.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void kinectSensorChooser1_KinectSensorChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            //Stop last sensor.
            KinectSensor oldSensor = (KinectSensor)e.OldValue;
            StopKinect(oldSensor);

            //Enable depth, skeleton and color stream.
            KinectSensor newSensor = (KinectSensor)e.NewValue;
            sensor = newSensor;
            newSensor.DepthStream.Enable();
            newSensor.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);
            newSensor.SkeletonStream.Enable();

            //Add callbacks for depth frame and skeleton frame.
            newSensor.SkeletonFrameReady += new EventHandler<SkeletonFrameReadyEventArgs>(newSensor_SkeletonFrameReady);
            newSensor.AllFramesReady += new EventHandler<AllFramesReadyEventArgs>(newSensor_AllFramesReady);

            //Add the Kinect to the skeleton viewer.
            kinectSkeletonViewer1.Kinect = newSensor;

            //Start the stream nad reset the elevation angle.
            try
            {
                newSensor.Start();
                elevation = 20;
                ElevationAngleChanged();
            }
            catch (System.IO.IOException)
            { 
                kinectSensorChooser1.AppConflictOccurred();
                return;
            }

            //Start the speech recognizer
            this.mySpeechRecognizer = SpeechRecognizer.Create();
            this.mySpeechRecognizer.SaidSomething += this.RecognizerSaidSomething;
            this.mySpeechRecognizer.Start(newSensor.AudioSource);

            //Update the sensor of the help window.
            if (helpWindow != null)
                helpWindow.setSensor(newSensor);
        }

        /// <summary>
        /// Stops the Kinect services.
        /// </summary>
        /// <param name="sensor"></param>
        void StopKinect(KinectSensor sensor)
        {
            if (sensor != null)
            {
                sensor.Stop();
                sensor.AudioSource.Stop();
                this.mySpeechRecognizer.Stop();
                this.mySpeechRecognizer.SaidSomething -= this.RecognizerSaidSomething;
                this.mySpeechRecognizer.Dispose();
                this.mySpeechRecognizer = null;

                if (helpWindow != null)
                    helpWindow.setSensor(null);
            }
        }

        #endregion Kinect discovery + setup

        #region Kinect Skeleton processing

        /// <summary>
        /// Callback called every skeleton update.
        /// </summary>
        /// <param name="sender">object that sent the event</param>
        /// <param name="e">Skeletons information</param>
        void newSensor_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
        {
            using (SkeletonFrame skeletonFrame = e.OpenSkeletonFrame())
            {
                if (skeletonFrame != null)
                {
                    int skeletonSlot = 0;

                    if ((this.skeletonData == null) || (this.skeletonData.Length != skeletonFrame.SkeletonArrayLength))
                    {
                        this.skeletonData = new Skeleton[skeletonFrame.SkeletonArrayLength];
                    }

                    skeletonFrame.CopySkeletonDataTo(this.skeletonData);

                    foreach (Skeleton skeleton in this.skeletonData)
                    {
                        if (SkeletonTrackingState.Tracked == skeleton.TrackingState)
                        {
                            //Identify if it is a new player.
                            Player player;
                            if (this.players.ContainsKey(skeletonSlot))
                            {
                                player = this.players[skeletonSlot];
                            }
                            else
                            {
                                player = new Player(skeletonSlot);
                                this.players.Add(skeletonSlot, player);
                            }

                            // Update player's bone and joint positions
                            if (skeleton.Joints.Count > 0)
                            {
                                player.LastUpdated = DateTime.Now;
                                player.IsAlive = true;

                                double position1 = skeleton.Joints[JointType.Head].Position.Z;
                                double position2 = skeleton.Joints[JointType.HandRight].Position.Z;
                                double position3 = skeleton.Joints[JointType.HandLeft].Position.Z;
                                double position4 = skeleton.Joints[JointType.FootRight].Position.Z;
                                double position5 = skeleton.Joints[JointType.FootLeft].Position.Z;
                                double position6 = skeleton.Joints[JointType.KneeLeft].Position.Z;
                                double position7 = skeleton.Joints[JointType.KneeRight].Position.Z;

                                double minPosition = Math.Min(position1, position2);
                                minPosition = Math.Min(minPosition, position3);
                                minPosition = Math.Min(minPosition, position4);
                                minPosition = Math.Min(minPosition, position5);
                                minPosition = Math.Min(minPosition, position6);
                                minPosition = Math.Min(minPosition, position7);
                                double maxPosition = Math.Max(position1, position2);
                                maxPosition = Math.Max(maxPosition, position3);
                                maxPosition = Math.Max(maxPosition, position4);
                                maxPosition = Math.Max(maxPosition, position5);
                                maxPosition = Math.Max(maxPosition, position6);
                                maxPosition = Math.Max(maxPosition, position7);

                                //Set maximum and minimum Z position of the player
                                double scale = 1000;
                                player.playerMinZ = minPosition * scale - 100;
                                player.playerMaxZ = maxPosition * scale + 100;

                                double hand1 = skeleton.Joints[JointType.HandRight].Position.Y;
                                double hand2 = skeleton.Joints[JointType.HandLeft].Position.Y;
                                double shoulder = skeleton.Joints[JointType.ShoulderCenter].Position.Y;
                                double hip = skeleton.Joints[JointType.HipCenter].Position.Y;
                                double knee1 = skeleton.Joints[JointType.KneeLeft].Position.Y;
                                double knee2 = skeleton.Joints[JointType.KneeRight].Position.Y;
                                double foot1 = skeleton.Joints[JointType.FootRight].Position.Y;
                                double foot2 = skeleton.Joints[JointType.FootLeft].Position.Y;
                                double reference = 0.7 * Math.Min(knee1, knee2) + 0.3 * hip;

                                //Identify gestures
                                if (!skeletonEventUpdate)
                                {
                                    if (hand1 >= shoulder && hand2 >= shoulder)
                                    {
                                        label1.Content = "Hands up!";
                                        label1.Opacity = 1;
                                        elevation = Math.Min(27, elevation + 2);
                                        ElevationAngleChanged();
                                        skeletonUpdateEvent();
                                    }
                                    else if (hand1 <= reference && hand2 <= reference)
                                    {
                                        label1.Content = "Hands down!";
                                        label1.Opacity = 1;
                                        elevation = Math.Max(-27, elevation - 2);
                                        ElevationAngleChanged(); 
                                        skeletonUpdateEvent();
                                    }
                                    else if (foot1 >= reference)
                                    {
                                        label1.Content = "Right foot up!";
                                        label1.Opacity = 1;
                                        player.changeColorUp();
                                        skeletonUpdateEvent();
                                    }
                                    else if (foot2 >= reference)
                                    {
                                        label1.Content = "Left foot up!";
                                        label1.Opacity = 1;
                                        player.changeColorDown();
                                        skeletonUpdateEvent();
                                    }
                                }
                            }

                        }

                        skeletonSlot++;
                    }
                }
            }
        }

        /// <summary>
        /// This event starts when a gesture is identified.
        /// </summary>
        public void skeletonUpdateEvent()
        {
            skeletonEventUpdate = true;
            Thread th = new Thread(skeletonUpdateHandler);
            th.Start();
        }

        /// <summary>
        /// Enables gestures again after one second.
        /// </summary>
        public void skeletonUpdateHandler()
        {
            Thread.Sleep(1000);
            skeletonEventUpdate = false;
        }

        #endregion Kinect Skeleton processing

        # region Depth View

        /// <summary>
        /// This callback is called each time a depth frame is ready.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void newSensor_AllFramesReady(object sender, AllFramesReadyEventArgs e)
        {
            //Count depth controls the updating rate.
            if (countDepth == 0)
            {
                //The using command is necesary to process the depthFrame, without this command the information on depthFrame will be lost at the first query.
                using (DepthImageFrame depthFrame = e.OpenDepthImageFrame())
                {
                    if (depthFrame == null)
                    { return; }

                    Byte[] pixels = GenerateColoredBytes(depthFrame);
                    Int32 stride = depthFrame.Width * 4;

                    depthimage.Source = BitmapSource.Create(depthFrame.Width, depthFrame.Height, 96, 96, PixelFormats.Bgr32, null, pixels, stride);
                }
                countDepth = 5;
            }
            else
            {
                countDepth--;
            }
        }

        /// <summary>
        /// Colored data (pixels) depends on the position of the player respect to the sensor.
        /// </summary>
        /// <param name="depthFrame"></param>
        /// <returns></returns>
        private Byte[] GenerateColoredBytes(DepthImageFrame depthFrame)
        {
            short[] rawDepthData = new short[depthFrame.PixelDataLength];

            depthFrame.CopyPixelDataTo(rawDepthData);

            Byte[] pixels = new Byte[depthFrame.Height * depthFrame.Width * 4];

            const Int32 BlueIndex = 0;
            const Int32 GreenIndex = 1;
            const Int32 RedIndex = 2;

            for (Int32 depthIndex = 0, colorIndex = 0; depthIndex < rawDepthData.Length && colorIndex < pixels.Length; depthIndex++, colorIndex += 4)
            {
                Int32 depth = rawDepthData[depthIndex] >> DepthImageFrame.PlayerIndexBitmaskWidth;

                foreach (var player in this.players)
                {
                    //Only paint if the player is "alive".
                    if (player.Value.IsAlive)
                    {
                        double MinPaintDistanceZ = player.Value.playerMinZ;
                        double MaxPaintDistanceZ = player.Value.playerMaxZ;
                        if (depth - MinPaintDistanceZ >= 0 && MaxPaintDistanceZ - depth >= 0)
                        {
                            pixels[colorIndex + BlueIndex] = player.Value.mixB[player.Value.colorId];
                            pixels[colorIndex + GreenIndex] = player.Value.mixG[player.Value.colorId];
                            pixels[colorIndex + RedIndex] = player.Value.mixR[player.Value.colorId];
                        }
                    }
                }
            }
            return pixels;
        }

        #endregion Depth View

        # region Speech Recognizer

        /// <summary>
        /// Callback called when the recognizer identifies a command.
        /// </summary>
        /// <param name="sender">object that sent the event</param>
        /// <param name="e">parameters of a "SaidSomethingEvent"</param>
        private void RecognizerSaidSomething(object sender, SpeechRecognizer.SaidSomethingEventArgs e)
        {
            label1.Content = e.Verb;
            label1.Opacity = 1;
            switch (e.Verb)
            {
                case SpeechRecognizer.Verbs.Up:
                    elevation = Math.Min(27, elevation + 10);
                    ElevationAngleChanged();
                    break;
                case SpeechRecognizer.Verbs.Down:
                    elevation = Math.Max(-27, elevation - 10);
                    ElevationAngleChanged();
                    break;
                case SpeechRecognizer.Verbs.Middle:
                    elevation = 0;
                    ElevationAngleChanged();
                    break;
                case SpeechRecognizer.Verbs.GoodBye:
                    Application.Current.Shutdown();
                    break;
                case SpeechRecognizer.Verbs.ShowSkeleton:
                    showSkeletonView(true);
                    break;
                case SpeechRecognizer.Verbs.HideSkeleton:
                    showSkeletonView(false);
                    break;
            }
        }

        #endregion Speech Recognizer

        # region Elevation Angle

        /// <summary>
        /// This method is called when a command to change the elevation is a call.
        /// </summary>
        private void ElevationAngleChanged()
        {
            this.debounce.Stop();
            this.debounce.Start();
        }


        /// <summary>
        /// Updates the elevation angle. 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void DebounceElapsed(object sender, EventArgs e)
        {
            // The time has elapsed.  We may start it again later.
            this.debounce.Stop();
            int angleToSet = elevation;

            // Is there an update in progress?
            if (this.backgroundUpdateInProgress)
            {
                // Try again in a few moments.
                this.debounce.Start();
            }
            else
            {
                this.backgroundUpdateInProgress = true;

                Task.Factory.StartNew(
                    () =>
                    {
                        try
                        {
                            // Check for not null and running
                            if ((this.sensor != null) && this.sensor.IsRunning)
                            {
                                // We must wait at least 1 second, and call no more frequently than 15 times every 20 seconds
                                // So, we wait at least 1350ms afterwards before we set backgroundUpdateInProgress to false.
                                this.sensor.ElevationAngle = angleToSet;
                                Thread.Sleep(1350);
                            }
                        }
                        finally
                        {
                            this.backgroundUpdateInProgress = false;
                        }
                    }).ContinueWith(
                            results =>
                            {
                                // This can happen if the Kinect transitions from Running to not running
                                // after the check above but before setting the ElevationAngle.
                                if (results.IsFaulted)
                                {
                                    var exception = results.Exception;
                                }
                            });
            }
        }
        #endregion Elevation Angle

        #region Game Threading

        /// <summary>
        /// Game thread.
        /// </summary>
        private void GameThread()
        {
            this.runningGameThread = true;

            while (this.runningGameThread)
            {
                Thread.Sleep(TimerResolution);
                //Check for player who are not alive.
                this.CheckPlayers();
                this.Dispatcher.Invoke(DispatcherPriority.Send, new Action<int>(this.HandleGameTimer), 0);
            }
        }

        /// <summary>
        /// Animates the command label.
        /// </summary>
        /// <param name="param"></param>
        private void HandleGameTimer(int param)
        {
            if (label1.Opacity == 1 && labelAnimationCount <= 0)
            {
                labelAnimationCount = 2;
            }
            if (!backgroundUpdateInProgress && label1.Opacity > 0)
            {
                labelAnimationCount--;
                if (labelAnimationCount <= 0)
                {
                    label1.Opacity -= 0.25;
                }
            }
        }

        /// <summary>
        /// Check which players hasn't been updated for too much time.
        /// </summary>
        private void CheckPlayers()
        {
            foreach (var player in this.players)
            {
                if (player.Value.IsAlive && DateTime.Now.Subtract(player.Value.LastUpdated).TotalMilliseconds > 200)
                    player.Value.IsAlive = false;
            }
        }

        #endregion

        # region Show/Hide skeleton view

        /// <summary>
        /// This method is called each time that the checkbox is selected.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void checkBox1_Checked(object sender, RoutedEventArgs e)
        {
            kinectSkeletonViewer1.Kinect = sensor;
        }

        /// <summary>
        /// This method is called each time that the checkbox is not selected.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void checkBox1_Unchecked(object sender, RoutedEventArgs e)
        {
            kinectSkeletonViewer1.Kinect = null;
        }

        /// <summary>
        /// Updates the skeleton view.
        /// </summary>
        /// <param name="show"></param>
        private void showSkeletonView(bool show)
        {
            if (checkBox1.IsChecked != show)
            {
                checkBox1.IsChecked = show;
                if (!show)
                    kinectSkeletonViewer1.Kinect = null;
                else
                    kinectSkeletonViewer1.Kinect = sensor;
            }
        }

        #endregion Show/Hide skeleton view

    }
}
