/*
Copyright 2019 Dr. Thomas Benjamin Senior, Michael Zoeller 

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

namespace EyeSkills
{
    /// <summary>
    /// A gesture controller for BlueTooth devices. This adds extra input behaviour to standard controllers.
    /// It supports short and long clicks, and continuous hold, in the style "EyeSkills Up - Short" etc.
    /// </summary>
    public class EyeSkillsInput : MonoBehaviour
    {
        // 2 recurring phases : Update A, Coroutine A, Update B, Corouting B
        // These map to : CLEARED & Capturing presses, UNPREDICTABLE & Categorising/Storing presses in active, AVAILABLE & serving esInputs, UNPREDICTABLE & clearing esInputs
        // If we always know which phase we are in, we can write simpler and more robust code
        // UPDATE : We actually need to capture in the B Phase so we don't miss events, which implicitly pushes them into availability for the A phase.

        private List<string> interestingButtons = new List<string>();

        /// <summary>
        /// Down buttons.  The id of the button, and the time it was initially pressed (so we can judge how long it has been pressed when release, or we decide to categorise it as a "hold")
        /// </summary>
        private Dictionary<string, float> downButtons = new Dictionary<string, float>();

        /// <summary>
        /// Up buttons.  We also record the time at which we detected up buttons in UpdateA, so the CoroutineA has what it needs to categorise.
        /// </summary>
        private Dictionary<string, float> upButtons = new Dictionary<string, float>();

        /// <summary>
        /// A list of the buttons being held, so we ignore the many events generated by them being held down 
        /// </summary>
        private List<string> buttonsBeingHeld = new List<string>();

        /// <summary>
        /// The active buttons.  These are buttons we were able to categorise as being "short"/"long" or "hold" buttons.
        /// </summary>
        private List<string> activeButtons = new List<string>();

        // Lets just flip some booleans for now.
        bool phaseA = true;

        // Our short/long click thresholds. Anthing held longer than longThreshold is a "hold"
        public float shortThreshold = 0.4f, longThreshold = 3f;

        /// <summary>
        /// Whether or not it is safe to serve press data via the API 
        /// </summary>
        bool _serving = false;

        /// <summary>
        /// API : A read only version which can be queried externally to see if we are in a "serving" phase
        /// </summary>
        /// <value><c>true</c> if serving; otherwise, <c>false</c>.</value>
        protected bool serving
        {
            set { _serving = value; }
        }

        //singleton like access pattern
        public static EyeSkillsInput instance = null;

        void Awake()
        {
            if (instance == null)
                instance = this;
            else if (instance != this)
                Destroy(gameObject);
        }

        /// <summary>
        /// Make our list of interesting buttons which we monitor
        /// </summary>
        void ListInterestingButtons()
        {
            interestingButtons.Add("EyeSkills Up");
            interestingButtons.Add("EyeSkills Down");
            interestingButtons.Add("EyeSkills Left");
            interestingButtons.Add("EyeSkills Right");
            interestingButtons.Add("EyeSkills Confirm");
            interestingButtons.Add("EyeSkills Cancel");
        }

        /// <summary>
        /// Each phase in the pipeline is composed of an update followed by a post-update (coroutine) execution phase.
        /// The EsInput manager is a two phase process. The end of each phase takes place in a coroutine.
        /// Here we flip our bits.
        /// </summary>
        void ChangePhase()
        {
            if (phaseA == true)
            {
                phaseA = false;
            }
            else
            {
                phaseA = true;
            }
        }

        // The job of capture is to see which key has been pressed during this update phase
        // The difference with the old version is : Capture used to run everyframe, and then spin off a coroutine to watch for the UP
        // Now, capture is only being called once every two frames. Is that what's changed? Can we merge Capture from PhaseB into PhaseA? Will that help?
        void Capture()
        {
            float now = Time.time;

            //List<string> downButtonKeys = new List<string>(downButtons.Keys);

            // We could avoid unnecessary processing if Unity provided both Input.GetAnyKeyDown and Input.GetAnyKeyUp, but the Up variant doesn't exist :-(
            foreach (string k in interestingButtons)
            {
                if (Input.GetButtonDown(k))
                {
                    // We store the details of the press for our coroutine to categorize.
                    // We allow multi-touch out of the box in this implementation
                    if (!downButtons.ContainsKey(k) && !buttonsBeingHeld.Contains(k))
                    {
                        // Filter out buttons we are actively holding down
                        downButtons.Add(k, now); //Without checking if it's already added, we will push on the timestamp forever.
                    }
                }

                if (Input.GetButtonUp(k)) //A key can't be down and up at the same time?
                {
                    // We store the details of the press for our coroutine to categorize.
                    // We allow "multi-touch" out of the box in this implementation
                    // An "up" event can only happen in one frame
                    upButtons.Add(k, now);
                }
            }
        }

        /// <summary>
        /// Categorise the button presses from the capture (UpdateA) phase.  If we have an "up" where we have a matching "down" we can categorise it as a short/long press.
        /// If we have a "down" without an "up", we still need to check we haven't held it long enough for it to become a "hold" press.
        /// If we can categorise the press, it is removed from both "up" and "down" lists, and placed on the "active" list so it is available for scripts in the UpdateB phase, and can then be cleared in CoroutineB phase
        /// so that we don't start accumulating old events during our A phase.
        /// </summary>
        void Categorise()
        {
            // Take a time sample so we can treat all cases from the same moment in time
            float now = Time.time;

            // Check which buttons are being released
            List<string> upButtonKeys = new List<string>(upButtons.Keys);
            foreach (string k in upButtonKeys)
            {
                if (downButtons.ContainsKey(k))
                {
                    // To detect an up, there must have been a corresponding down.
                    float downTime = (now - downButtons[k]);

                    // if we detected an up for this button, and the threshold for long clearly wasn't exceeded, but it has been there for longer than short, it must be a "long" press
                    if (downTime > longThreshold)
                    {
                        activeButtons.Add(k + "-holdEnd");
                        buttonsBeingHeld.Remove(k); //We only release the hold when the button is truly released
                        downButtons.Remove(k);
                        upButtons.Remove(k);
                    }
                    else if ((downTime > shortThreshold) && (downTime <= longThreshold))
                    {
                        activeButtons.Add(k + "-long");
                        downButtons.Remove(k);
                        upButtons.Remove(k);
                    }
                    else if ((downTime <= shortThreshold))
                    {
                        activeButtons.Add(k + "-short");
                        downButtons.Remove(k);
                        upButtons.Remove(k);
                    }

                    // This is actually a case in its own right. We can't put removal code after the branching statements
                }
                else
                {
                    upButtons.Remove(k);
                }
            }

            // Now check which are still being pressed down (more efficient to do this after removing UPs!)
            List<string> downButtonKeys = new List<string>(downButtons.Keys);
            foreach (string k in downButtonKeys)
            {
                if (!buttonsBeingHeld.Contains(k))
                {
                    float downTime = (now - downButtons[k]);

                    // first we check to see if the time the key has been held down exceeds our "hold" threshold. In which case, it is a hold event.
                    if (downTime > longThreshold)
                    {
                        // We have detected a hold, so we create an appropriate event 
                        activeButtons.Add(k + "-holdStart");
                        buttonsBeingHeld.Add(k);
                        // TODO: Is the capture phase sorting this already?
                        // TODO: We need to add this HOLD to a list to ignore it and avoid sending a stream of HOLD events. We might want to actually signal this to the Capture phase, to ignore those buttons
                    }
                }
            }
        }

        /// <summary>
        /// Because it is dangerous to consume any information about outstanding button presses, we only allow this to occur in this phase.
        /// We do this by setting a flag which determines whether the API then returns data or not.
        /// </summary>
        void StartServing()
        {
            _serving = true;
        }

        void StopServing()
        {
            _serving = false;
        }

        void Serve()
        {
            StartServing();
        }

        /// <summary>
        /// Remove active buttons and prevent the API from returning any unreliable data.
        /// </summary>
        void Clear()
        {
            StopServing();
            activeButtons.Clear();
        }

        /// <summary>
        /// Flush all previous key presses. Useful for restarting if a script using the esInput class has been paused (e.g. we never receive "closure" on key ups causing stale data)
        /// </summary>
        public void Flush()
        {
            //Get back to a fresh cycle with clean data
            activeButtons.Clear();
            downButtons.Clear();
            upButtons.Clear();
            buttonsBeingHeld.Clear();
            StopServing();
            phaseA = true;
        }

        /*
         * ===== Phase Control =====
         */

        void Start()
        {
            DontDestroyOnLoad(this);

            ListInterestingButtons();

            // TODO: performance hog?
            StartCoroutine(PostUpdateProcessor());

            // TODO: We should now have a coordinated cycle of Update/PostUpdateProcessor, where each knows whether they are in phase A (collection + categorisation) or B (usage and clearance)
        }


        void Update()
        {
            if (phaseA)
                //Update A Phase : Capture events
                Capture();
            else
                //Update B Phase : Serve events
                Capture();
        }


        IEnumerator PostUpdateProcessor()
        {
            // The first time we are called, just do nothing. We then know we will be executed after the first Update.
            yield return 0;

            // We run forever to match up with the update phases
            while (true)
            {
                if (phaseA)
                {
                    // Coroutine A Phase : Categorise
                    Categorise();
                    Serve();
                }
                else
                {
                    // Coroutine B Phase : Clear
                    Clear();
                }

                ChangePhase();
                yield return 0;
            }
        }

        /*
         * ===== External API =====
         */

        /// <summary>
        /// Checks the button is active.
        /// </summary>
        /// <returns><c>true</c>, if button is active was checked, <c>false</c> otherwise.</returns>
        /// <param name="id">Identifier.</param>
        private bool checkButtonIsActive(string id)
        {
            if (_serving)
            {
                return activeButtons.Contains(id);
            }

            return false; // If we aren't serving, then return nothing.
        }

        /// <summary>
        /// Gets the short button press.
        /// </summary>
        /// <returns><c>true</c>, if short button press was gotten, <c>false</c> otherwise.</returns>
        /// <param name="buttonID">Button identifier.</param>
        public bool GetShortButtonPress(string buttonID)
        {
            return checkButtonIsActive(buttonID + "-short");
        }

        /// <summary>
        /// Gets the long button press.
        /// </summary>
        /// <returns><c>true</c>, if long button press was gotten, <c>false</c> otherwise.</returns>
        /// <param name="buttonID">Button identifier.</param>
        public bool GetLongButtonPress(string buttonID)
        {
            return checkButtonIsActive(buttonID + "-long");
        }

        /// <summary>
        /// Gets the held button press.  If there is a button being held it returns then number of milliseconds it has been held for, otherwise, it returns -1
        /// </summary>
        /// <returns>The held button press.</returns>
        /// <param name="buttonID">Button identifier.</param>
        public bool GetHeldButtonPress(string buttonID)
        {
            return checkButtonIsActive(buttonID + "-holdStart");
        }
    }
}