using BBIWARG.Input.InputHandling;
using BBIWARG.Input.InputProviding;
using BBIWARG.Recognition.FingerRecognition;
using BBIWARG.Recognition.PalmRecognition;
using BBIWARG.Utility;
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace BBIWARG.Output.GlassesOutput
{
    /// <summary>
    /// A Windows Form which displays a full screen window to be shown on augmented reality glasses.
    /// </summary>
    public partial class GlassesWindow : Form
    {
        /// <summary>
        /// true iff the calibration image has the latest data
        /// </summary>
        private bool calibrationImageUpToDate;

        /// <summary>
        /// position of the current calibration point in the output plane
        /// </summary>
        private Vector2D currentCalibrationPoint;

        /// <summary>
        /// id of the current frame
        /// </summary>
        private int currentFrameID;

        /// <summary>
        /// true iff the window is showing the latest data
        /// </summary>
        private bool guiUpToDate;

        /// <summary>
        /// the image shown on the glasses
        /// </summary>
        private OutputImage image;

        /// <summary>
        /// the input handler
        /// </summary>
        private InputHandler inputHandler;

        /// <summary>
        /// the input provider
        /// </summary>
        private IInputProvider inputProvider;

        /// <summary>
        /// size of the input images
        /// </summary>
        private ImageSize inputSize;

        /// <summary>
        /// size of the image show on the glasses
        /// </summary>
        private ImageSize outputSize;

        /// <summary>
        /// projection from the input image plane to a plane which lies in front of the wearer of the glasses (output plane)
        /// </summary>
        private Projection2DTo2D projection;

        /// <summary>
        /// random number generator
        /// </summary>
        private Random rand;

        /// <summary>
        /// timer to periodically update the window
        /// </summary>
        private System.Windows.Forms.Timer timer;

        /// <summary>
        /// Creates a GlassesWindow.
        /// </summary>
        /// <param name="inputProvider">input provider</param>
        /// <param name="inputHandler">input handler</param>
        /// <param name="name">title of the window</param>
        /// <param name="screen">the screen this window is shown on</param>
        /// <param name="updateInterval">the update interval for the window in milliseconds</param>
        public GlassesWindow(IInputProvider inputProvider, InputHandler inputHandler, String name, Screen screen, int updateInterval)
        {
            InitializeComponent();

            this.inputProvider = inputProvider;
            this.inputHandler = inputHandler;
            this.inputSize = inputHandler.ImageSize;
            this.outputSize = new ImageSize(screen.Bounds.Width, screen.Bounds.Height);
            guiUpToDate = false;
            calibrationImageUpToDate = false;

            rand = new Random();
            currentCalibrationPoint = getRandomOutputPoint();
            projection = new Projection2DTo2D(inputSize, outputSize, Parameters.GlassesWindowNumCalibrationPoints);

            Name = name;
            Text = name;

            timer = new System.Windows.Forms.Timer();
            timer.Interval = updateInterval;
            timer.Tick += update;
            timer.Start();

            KeyPreview = true;

            // fullscreen
            FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
            Location = screen.Bounds.Location;
            Size = screen.Bounds.Size;
        }

        /// <summary>
        /// Stops the input provider when closing the window.
        /// </summary>
        /// <param name="e">event arguments</param>
        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);
            inputProvider.stop();
        }

        /// <summary>
        /// Returns a random point in the glasses image.
        /// </summary>
        /// <returns>a random point in the output image</returns>
        private Vector2D getRandomOutputPoint()
        {
            return outputSize.getAbsolutePoint(new Vector2D((float)rand.NextDouble(), (float)rand.NextDouble()));
        }

        /// <summary>
        /// Handle key down events by showing the next point or finishing or resetting the calibration.
        /// </summary>
        /// <param name="sender">event sender</param>
        /// <param name="e">event arguments</param>
        private void GlassesWindow_OnKeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.K)
            {
                FrameData frameData = inputHandler.FrameData;
                if (frameData != null)
                {
                    lock (frameData)
                    {
                        if (frameData.TrackedFingers.Count == 1)
                        {
                            Vector2D pointOutput = currentCalibrationPoint;
                            Vector2D pointInput = frameData.TrackedFingers[0].TipPoint;

                            projection.addCalibrationPoints(pointInput, pointOutput);

                            currentCalibrationPoint = getRandomOutputPoint();
                        }
                    }
                }
            }
            else if (e.KeyCode == Keys.R)
            {
                projection.reset();
                currentCalibrationPoint = getRandomOutputPoint();
            }
        }

        /// <summary>
        /// Updates the window.
        /// </summary>
        /// <param name="sender">the event sender</param>
        /// <param name="e">the event arguments</param>
        private void update(object sender, EventArgs e)
        {
            Utility.Timer.start("GlassesWindow.update");

            if (!inputProvider.IsActive)
                Close();

            if (projection.IsCalibrated)
            {
                FrameData frameData = inputHandler.FrameData;
                if (frameData != null)
                {
                    lock (frameData)
                    {
                        if (currentFrameID != frameData.FrameID)
                        {
                            currentFrameID = frameData.FrameID;
                            Utility.Timer.start("GlassesWindow.update::updateImage");
                            updateImage(frameData);
                            Utility.Timer.stop("GlassesWindow.update::updateImage");
                        }
                    }
                }
            }
            else
            {
                if (!calibrationImageUpToDate)
                    updateCalibrationImage();
            }

            if (!guiUpToDate)
            {
                Utility.Timer.start("GlassesWindow.update::updateGUI");
                updateGUI();
                Utility.Timer.stop("GlassesWindow.update::updateGUI");
            }

            Utility.Timer.stop("GlassesWindow.update");
        }

        /// <summary>
        /// Updates the calibration image.
        /// </summary>
        private void updateCalibrationImage()
        {
            guiUpToDate = false;

            if (image != null)
                image.Dispose();

            image = new OutputImage(outputSize);
            image.fillCircle(currentCalibrationPoint, 25, Color.Orange);
        }

        /// <summary>
        /// Updates the GUI elements.
        /// </summary>
        private void updateGUI()
        {
            // update image boxes
            imageBox.Image = image;

            guiUpToDate = true;
        }

        /// <summary>
        /// Updates the glasses image.
        /// </summary>
        /// <param name="frameData">data for the new frame</param>
        private void updateImage(FrameData frameData)
        {
            guiUpToDate = false;

            if (image != null)
                image.Dispose();

            image = new OutputImage(outputSize);
            foreach (Palm palm in frameData.TrackedPalms)
            {
                Quadrangle quadInput = palm.Quad;
                Vector2D a = projection.projectPoint(quadInput.TopLeft);
                Vector2D b = projection.projectPoint(quadInput.TopRight);
                Vector2D c = projection.projectPoint(quadInput.BottomRight);
                Vector2D d = projection.projectPoint(quadInput.BottomLeft);
                Quadrangle quadOutput = new Quadrangle(a, b, c, d);
                image.drawQuadrangleGrid(quadOutput, Color.Yellow, Color.Orange, 3, 4);
            }

            foreach (Finger finger in frameData.TrackedFingers)
            {
                Vector2D tipProjected = projection.projectPoint(finger.TipPoint);
                image.fillCircle(tipProjected, 10, Color.Yellow);
            }
        }
    }
}