/*
 * @author Valentin Simonov / http://va.lent.in/
 */

#if TOUCHSCRIPT_TUIO
#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_IOS || UNITY_ANDROID
using System;
using System.Collections.Generic;
using TouchScript.Pointers;
using TouchScript.Utils;
using TUIOsharp;
using TUIOsharp.DataProcessors;
using TUIOsharp.Entities;
using UnityEngine;

namespace TouchScript.InputSources 
{
    /// <summary>
    /// Processes TUIO 1.1 input.
    /// </summary>
    [AddComponentMenu("TouchScript/Input Sources/TUIO Input")]
    [HelpURL("http://touchscript.github.io/docs/html/T_TouchScript_InputSources_TuioInput.htm")]
    public sealed class TuioInput : InputSource
    {
        #region Constants

        /// <summary>
        /// Type of TUIO input object.
        /// </summary>
        [Flags]
        public enum InputType
        {
            /// <summary>
            /// Pointer.
            /// </summary>
            Cursors = 1 << 0,

            /// <summary>
            /// Shape.
            /// </summary>
            Blobs = 1 << 1,

            /// <summary>
            /// Tagged object.
            /// </summary>
            Objects = 1 << 2
        }

        #endregion

        #region Public properties

        /// <summary>
        /// Port to listen to.
        /// </summary>
        public int TuioPort
        {
            get { return tuioPort; }
            set
            {
                if (tuioPort == value) return;
                tuioPort = value;
                connect();
            }
        }

        /// <summary>
        /// What input types should the input source listen to.
        /// </summary>
        public InputType SupportedInputs
        {
            get { return supportedInputs; }
            set
            {
                if (supportedInputs == value) return;
                supportedInputs = value;
                updateInputs();
            }
        }

        #endregion

        #region Private variables

        [SerializeField]
        private int tuioPort = 3333;

        [SerializeField]
        private InputType supportedInputs = InputType.Cursors | InputType.Blobs | InputType.Objects;

        private TuioServer server;
        private CursorProcessor cursorProcessor;
        private ObjectProcessor objectProcessor;
        private BlobProcessor blobProcessor;

        private Dictionary<TuioCursor, TouchPointer> cursorToInternalId = new Dictionary<TuioCursor, TouchPointer>();
        private Dictionary<TuioBlob, ObjectPointer> blobToInternalId = new Dictionary<TuioBlob, ObjectPointer>();
        private Dictionary<TuioObject, ObjectPointer> objectToInternalId = new Dictionary<TuioObject, ObjectPointer>();
        private int screenWidth;
        private int screenHeight;

        private ObjectPool<TouchPointer> touchPool;
        private ObjectPool<ObjectPointer> objectPool;

        #endregion

        #region Constructor

        public TuioInput()
        {
            touchPool = new ObjectPool<TouchPointer>(20, () => new TouchPointer(this), null, (t) => t.INTERNAL_Reset());
            objectPool = new ObjectPool<ObjectPointer>(10, () => new ObjectPointer(this), null, (t) => t.INTERNAL_Reset());
        }

        #endregion

        #region Public methods

        /// <inheritdoc />
        public override void UpdateInput()
        {
            base.UpdateInput();
            screenWidth = Screen.width;
            screenHeight = Screen.height;
        }

        /// <inheritdoc />
        public override bool CancelPointer(Pointer pointer, bool shouldReturn)
        {
            base.CancelPointer(pointer, shouldReturn);
            lock (this)
            {
                if (pointer.Type == Pointer.PointerType.Touch)
                {
                    TuioCursor cursor = null;
                    foreach (var touchPoint in cursorToInternalId)
                    {
                        if (touchPoint.Value.Id == pointer.Id)
                        {
                            cursor = touchPoint.Key;
                            break;
                        }
                    }
                    if (cursor != null)
                    {
                        cancelPointer(pointer);
                        if (shouldReturn)
                        {
                            cursorToInternalId[cursor] = internalReturnTouch(pointer as TouchPointer);
                        }
                        else
                        {
                            cursorToInternalId.Remove(cursor);
                        }
                        return true;
                    }
                    return false;
                }

                TuioObject obj = null;
                foreach (var touchPoint in objectToInternalId)
                {
                    if (touchPoint.Value.Id == pointer.Id)
                    {
                        obj = touchPoint.Key;
                        break;
                    }
                }
                if (obj != null)
                {
                    cancelPointer(pointer);
                    if (shouldReturn)
                    {
                        objectToInternalId[obj] = internalReturnObject(pointer as ObjectPointer, pointer.Position);
                    }
                    else
                    {
                        objectToInternalId.Remove(obj);
                    }
                    return true;
                }

                TuioBlob blob = null;
                foreach (var touchPoint in blobToInternalId)
                {
                    if (touchPoint.Value.Id == pointer.Id)
                    {
                        blob = touchPoint.Key;
                        break;
                    }
                }
                if (blob != null)
                {
                    cancelPointer(pointer);
                    if (shouldReturn)
                    {
                        blobToInternalId[blob] = internalReturnObject(pointer as ObjectPointer, pointer.Position);
                    }
                    else
                    {
                        blobToInternalId.Remove(blob);
                    }
                    return true;
                }

                return false;
            }
        }

        #endregion

        #region Internal methods

        /// <inheritdoc />
        public override void INTERNAL_DiscardPointer(Pointer pointer)
        {
            if (pointer.Type == Pointer.PointerType.Touch)
            {
                touchPool.Release(pointer as TouchPointer);
            } else if (pointer.Type == Pointer.PointerType.Object)
            {
                objectPool.Release(pointer as ObjectPointer);
            }
        }

        #endregion

        #region Unity

        /// <inheritdoc />
        protected override void OnEnable()
        {
            base.OnEnable();

            screenWidth = Screen.width;
            screenHeight = Screen.height;

            cursorProcessor = new CursorProcessor();
            cursorProcessor.CursorAdded += OnCursorAdded;
            cursorProcessor.CursorUpdated += OnCursorUpdated;
            cursorProcessor.CursorRemoved += OnCursorRemoved;

            blobProcessor = new BlobProcessor();
            blobProcessor.BlobAdded += OnBlobAdded;
            blobProcessor.BlobUpdated += OnBlobUpdated;
            blobProcessor.BlobRemoved += OnBlobRemoved;

            objectProcessor = new ObjectProcessor();
            objectProcessor.ObjectAdded += OnObjectAdded;
            objectProcessor.ObjectUpdated += OnObjectUpdated;
            objectProcessor.ObjectRemoved += OnObjectRemoved;

            connect();
        }

        /// <inheritdoc />
        protected override void OnDisable()
        {
            disconnect();
            base.OnDisable();
        }

        #endregion

        #region Private functions

        private TouchPointer internalAddTouch(Vector2 position)
        {
            var pointer = touchPool.Get();
            pointer.Position = remapCoordinates(position);
            pointer.Buttons |= Pointer.PointerButtonState.FirstButtonDown | Pointer.PointerButtonState.FirstButtonPressed;
            addPointer(pointer);
            pressPointer(pointer);
            return pointer;
        }

        private TouchPointer internalReturnTouch(TouchPointer pointer)
        {
            var newPointer = touchPool.Get();
            newPointer.CopyFrom(pointer);
            pointer.Buttons |= Pointer.PointerButtonState.FirstButtonDown | Pointer.PointerButtonState.FirstButtonPressed;
            newPointer.Flags |= Pointer.FLAG_RETURNED;
            addPointer(newPointer);
            pressPointer(newPointer);
            return newPointer;
        }

        private ObjectPointer internalAddObject(Vector2 position)
        {
            var pointer = objectPool.Get();
            pointer.Position = remapCoordinates(position);
            pointer.Buttons |= Pointer.PointerButtonState.FirstButtonDown | Pointer.PointerButtonState.FirstButtonPressed;
            addPointer(pointer);
            pressPointer(pointer);
            return pointer;
        }

        private ObjectPointer internalReturnObject(ObjectPointer pointer, Vector2 position)
        {
            var newPointer = objectPool.Get();
            newPointer.CopyFrom(pointer);
            pointer.Buttons |= Pointer.PointerButtonState.FirstButtonDown | Pointer.PointerButtonState.FirstButtonPressed;
            newPointer.Flags |= Pointer.FLAG_RETURNED;
            addPointer(newPointer);
            pressPointer(newPointer);
            return newPointer;
        }

        private void connect()
        {
            if (!Application.isPlaying) return;
            if (server != null) disconnect();

            server = new TuioServer(TuioPort);
            server.Connect();
            updateInputs();
        }

        private void disconnect()
        {
            if (server != null)
            {
                server.RemoveAllDataProcessors();
                server.Disconnect();
                server = null;
            }

            foreach (var i in cursorToInternalId) cancelPointer(i.Value);
            foreach (var i in blobToInternalId) cancelPointer(i.Value);
            foreach (var i in objectToInternalId) cancelPointer(i.Value);
            cursorToInternalId.Clear();
            blobToInternalId.Clear();
            objectToInternalId.Clear();
        }

        private void updateInputs()
        {
            if (server == null) return;

            if ((supportedInputs & InputType.Cursors) != 0) server.AddDataProcessor(cursorProcessor);
            else server.RemoveDataProcessor(cursorProcessor);
            if ((supportedInputs & InputType.Blobs) != 0) server.AddDataProcessor(blobProcessor);
            else server.RemoveDataProcessor(blobProcessor);
            if ((supportedInputs & InputType.Objects) != 0) server.AddDataProcessor(objectProcessor);
            else server.RemoveDataProcessor(objectProcessor);
        }

        private void updateBlobProperties(ObjectPointer obj, TuioBlob target)
        {
            obj.Width = target.Width;
            obj.Height = target.Height;
            obj.Angle = target.Angle;
        }

        private void updateObjectProperties(ObjectPointer obj, TuioObject target)
        {
            obj.ObjectId = target.ClassId;
            obj.Angle = target.Angle;
        }

        #endregion

        #region Event handlers

        private void OnCursorAdded(object sender, TuioCursorEventArgs e)
        {
            var entity = e.Cursor;
            lock (this)
            {
                var x = entity.X * screenWidth;
                var y = (1 - entity.Y) * screenHeight;
                cursorToInternalId.Add(entity, internalAddTouch(new Vector2(x, y)));
            }
        }

        private void OnCursorUpdated(object sender, TuioCursorEventArgs e)
        {
            var entity = e.Cursor;
            lock (this)
            {
                TouchPointer touch;
                if (!cursorToInternalId.TryGetValue(entity, out touch)) return;

                var x = entity.X * screenWidth;
                var y = (1 - entity.Y) * screenHeight;

                touch.Position = remapCoordinates(new Vector2(x, y));
                updatePointer(touch);
            }
        }

        private void OnCursorRemoved(object sender, TuioCursorEventArgs e)
        {
            var entity = e.Cursor;
            lock (this)
            {
                TouchPointer touch;
                if (!cursorToInternalId.TryGetValue(entity, out touch)) return;

                cursorToInternalId.Remove(entity);
                releasePointer(touch);
                removePointer(touch);
            }
        }

        private void OnBlobAdded(object sender, TuioBlobEventArgs e)
        {
            var entity = e.Blob;
            lock (this)
            {
                var x = entity.X * screenWidth;
                var y = (1 - entity.Y) * screenHeight;
                var touch = internalAddObject(new Vector2(x, y));
                updateBlobProperties(touch, entity);
                blobToInternalId.Add(entity, touch);
            }
        }

        private void OnBlobUpdated(object sender, TuioBlobEventArgs e)
        {
            var entity = e.Blob;
            lock (this)
            {
                ObjectPointer touch;
                if (!blobToInternalId.TryGetValue(entity, out touch)) return;

                var x = entity.X * screenWidth;
                var y = (1 - entity.Y) * screenHeight;

                touch.Position = remapCoordinates(new Vector2(x, y));
                updateBlobProperties(touch, entity);
                updatePointer(touch);
            }
        }

        private void OnBlobRemoved(object sender, TuioBlobEventArgs e)
        {
            var entity = e.Blob;
            lock (this)
            {
                ObjectPointer touch;
                if (!blobToInternalId.TryGetValue(entity, out touch)) return;

                blobToInternalId.Remove(entity);
                releasePointer(touch);
                removePointer(touch);
            }
        }

        private void OnObjectAdded(object sender, TuioObjectEventArgs e)
        {
            var entity = e.Object;
            lock (this)
            {
                var x = entity.X * screenWidth;
                var y = (1 - entity.Y) * screenHeight;
                var touch = internalAddObject(new Vector2(x, y));
                updateObjectProperties(touch, entity);
                objectToInternalId.Add(entity, touch);
            }
        }

        private void OnObjectUpdated(object sender, TuioObjectEventArgs e)
        {
            var entity = e.Object;
            lock (this)
            {
                ObjectPointer touch;
                if (!objectToInternalId.TryGetValue(entity, out touch)) return;

                var x = entity.X * screenWidth;
                var y = (1 - entity.Y) * screenHeight;

                touch.Position = remapCoordinates(new Vector2(x, y));
                updateObjectProperties(touch, entity);
                updatePointer(touch);
            }
        }

        private void OnObjectRemoved(object sender, TuioObjectEventArgs e)
        {
            var entity = e.Object;
            lock (this)
            {
                ObjectPointer touch;
                if (!objectToInternalId.TryGetValue(entity, out touch)) return;

                objectToInternalId.Remove(entity);
                releasePointer(touch);
                removePointer(touch);
            }
        }

        #endregion
    }

}

#endif
 #endif