using UnityEngine;
using com.rfilkov.kinect;
namespace com.rfilkov.components
{
    /// 
    /// Body slice enum.
    /// 
    public enum BodySlice
    {
        HEIGHT = 0,
        WIDTH = 1,
        TORSO_1 = 2,
        TORSO_2 = 3,
        TORSO_3 = 4,
        TORSO_4 = 5,
        TORSO_5 = 6,
        COUNT = 7
    }
    /// 
    /// Data structure used by the body slicer.
    /// 
    public struct BodySliceData
    {
        public BodySlice sliceType;
        public bool isSliceValid;
        public float diameter;
        public int depthPointsLength;
        public int colorPointsLength;
        //	public ushort[] depths;
        public Vector2 startDepthPoint;
        public Vector2 endDepthPoint;
        public Vector2 startColorPoint;
        public Vector2 endColorPoint;
        public Vector3 startKinectPoint;
        public Vector3 endKinectPoint;
    }
    /// 
    /// Body slicer is component that estimates the user height from the depth data, as well as several other body sizes.
    /// 
    public class BodySlicer : MonoBehaviour
    {
        [Tooltip("Depth sensor index - 0 is the 1st one, 1 - the 2nd one, etc.")]
        public int sensorIndex = 0;
        [Tooltip("Index of the player, tracked by this component. 0 means the 1st player, 1 - the 2nd one, 2 - the 3rd one, etc.")]
        public int playerIndex = 0;
        [Tooltip("Camera used to estimate the overlay positions of 3D-objects over the background. By default it is the main camera.")]
        public Camera foregroundCamera;
        [Tooltip("Whether the sensor is in portrait mode or not.")]
        public bool portraitMode = false;
        [Tooltip("Whether the body height should estimated or not.")]
        public bool estimateBodyHeight = true;
        [Tooltip("Whether the body width should estimated or not.")]
        public bool estimateBodyWidth = false;
        [Tooltip("Whether the body slices should estimated or not.")]
        public bool estimateBodySlices = false;
        [Tooltip("Whether the slicing should be done on all updates, or only after the user calibration.")]
        public bool continuousSlicing = false;
        [Tooltip("Whether the detected body slices should be displayed on the screen.")]
        public bool displayBodySlices = false;
        private ulong calibratedUserId;
        private byte userBodyIndex;
        // The singleton instance of BodySlicer
        //private static BodySlicer instance = null;
        private KinectManager kinectManager;
        private KinectInterop.SensorData sensorData;
        private ulong lastDepthFrameTime;
        // body slice data
        private BodySliceData[] bodySlices = new BodySliceData[(int)BodySlice.COUNT];
        // depth image resolution
        private int depthImageWidth;
        private int depthImageHeight;
        // depth scale
        private Vector3 depthScale = Vector3.one;
        // screen rectangle taken by the foreground image (in pixels)
        private Rect foregroundImgRect;
        ///// 
        ///// Gets the singleton BodySlicer instance.
        ///// 
        ///// The singleton BodySlicer instance.
        //public static BodySlicer Instance
        //{
        //    get
        //    {
        //        return instance;
        //    }
        //}
        /// 
        /// Gets the height of the user.
        /// 
        /// The user height.
        public float getUserHeight()
        {
            return getSliceWidth(BodySlice.HEIGHT);
        }
        /// 
        /// Gets the slice width.
        /// 
        /// The slice width.
        /// Slice.
        public float getSliceWidth(BodySlice slice)
        {
            int iSlice = (int)slice;
            if (bodySlices[iSlice].isSliceValid)
            {
                return bodySlices[iSlice].diameter;
            }
            return 0f;
        }
        /// 
        /// Gets the body slice count.
        /// 
        /// The body slice count.
        public int getBodySliceCount()
        {
            return bodySlices != null ? bodySlices.Length : 0;
        }
        /// 
        /// Gets the body slice data.
        /// 
        /// The body slice data.
        /// Slice.
        public BodySliceData getBodySliceData(BodySlice slice)
        {
            return bodySlices[(int)slice];
        }
        /// 
        /// Gets the calibrated user ID.
        /// 
        /// The calibrated user ID.
        public ulong getCalibratedUserId()
        {
            return calibratedUserId;
        }
        /// 
        /// Gets the last frame time.
        /// 
        /// The last frame time.
        public ulong getLastFrameTime()
        {
            return lastDepthFrameTime;
        }
        ////////////////////////////////////////////////////////////////////////
        void Awake()
        {
            //instance = this;
        }
        void Start()
        {
            kinectManager = KinectManager.Instance;
            sensorData = kinectManager ? kinectManager.GetSensorData(sensorIndex) : null;
            if(kinectManager && kinectManager.IsInitialized())
            {
                depthImageWidth = kinectManager.GetDepthImageWidth(sensorIndex);
                depthImageHeight = kinectManager.GetDepthImageHeight(sensorIndex);
                depthScale = kinectManager.GetDepthImageScale(sensorIndex);
            }
            if (foregroundCamera == null)
            {
                // by default use the main camera
                foregroundCamera = Camera.main;
            }
        }
        void Update()
        {
            if (!kinectManager || !kinectManager.IsInitialized() || sensorData == null)
                return;
            // calculate the foreground rectangle
            foregroundImgRect = kinectManager.GetForegroundRectDepth(sensorIndex, foregroundCamera);
            // get required player
            ulong userId = kinectManager.GetUserIdByIndex(playerIndex);
            if (calibratedUserId == 0)
            {
                if (userId != 0)
                {
                    OnCalibrationSuccess(userId);
                }
            }
            else
            {
                if (calibratedUserId != userId)
                {
                    OnUserLost(calibratedUserId);
                }
                else if (continuousSlicing)
                {
                    EstimateBodySlices(calibratedUserId);
                }
            }
        }
        void OnRenderObject()
        {
            if (displayBodySlices)
            {
                DrawBodySlice(bodySlices[(int)BodySlice.HEIGHT]);
                DrawBodySlice(bodySlices[(int)BodySlice.TORSO_1]);
                DrawBodySlice(bodySlices[(int)BodySlice.TORSO_2]);
                DrawBodySlice(bodySlices[(int)BodySlice.TORSO_3]);
                DrawBodySlice(bodySlices[(int)BodySlice.TORSO_4]);
                DrawBodySlice(bodySlices[(int)BodySlice.TORSO_5]);
            }
        }
        // draws a body slice line
        private void DrawBodySlice(BodySliceData bodySlice)
        {
            if (bodySlice.isSliceValid && bodySlice.startDepthPoint != Vector2.zero && bodySlice.endDepthPoint != Vector2.zero)
            {
                float rectX = foregroundImgRect.xMin;
                float rectY = foregroundImgRect.yMin;
                float scaleX = foregroundImgRect.width / depthImageWidth;
                float scaleY = foregroundImgRect.height / depthImageHeight;
                float x1 = rectX + (depthScale.x >= 0f ? bodySlice.startDepthPoint.x : depthImageWidth - bodySlice.startDepthPoint.x) * scaleX;
                float y1 = rectY + (depthScale.y >= 0f ? bodySlice.startDepthPoint.y : depthImageHeight - bodySlice.startDepthPoint.y) * scaleY;
                float x2 = rectX + (depthScale.x >= 0f ? bodySlice.endDepthPoint.x : depthImageWidth - bodySlice.endDepthPoint.x) * scaleX;
                float y2 = rectY + (depthScale.y >= 0f ? bodySlice.endDepthPoint.y : depthImageHeight - bodySlice.endDepthPoint.y) * scaleY;
                KinectInterop.DrawLine((int)x1, (int)y1, (int)x2, (int)y2, 2f, Color.red);
            }
        }
        public void OnCalibrationSuccess(ulong userId)
        {
            calibratedUserId = userId;
            // estimate body slices
            EstimateBodySlices(calibratedUserId);
        }
        void OnUserLost(ulong userId)
        {
            calibratedUserId = 0;
            // invalidate the body slice data
            for (int i = 0; i < bodySlices.Length; i++)
            {
                bodySlices[i].isSliceValid = false;
            }
        }
        // estimates the body slice data for the given user
        public bool EstimateBodySlices(ulong userId)
        {
            if (userId <= 0)
                userId = calibratedUserId;
            if (!kinectManager || userId == 0)
                return false;
            userBodyIndex = (byte)kinectManager.GetBodyIndexByUserId(userId);
            if (userBodyIndex == 255)
                return false;
            bool bSliceSuccess = false;
            if (sensorData.bodyIndexImage != null && sensorData.depthImage != null &&
                sensorData.lastDepthFrameTime != lastDepthFrameTime)
            {
                lastDepthFrameTime = sensorData.lastDepthFrameTime;
                bSliceSuccess = true;
                Vector2 pointPelvis = kinectManager.MapSpacePointToDepthCoords(sensorIndex, kinectManager.GetJointKinectPosition(userId, (int)KinectInterop.JointType.Pelvis, false));
                if (estimateBodyHeight)
                {
                    bodySlices[(int)BodySlice.HEIGHT] = !portraitMode ? GetUserHeightParams(pointPelvis) : GetUserWidthParams(pointPelvis);
                }
                if (estimateBodyWidth)
                {
                    bodySlices[(int)BodySlice.WIDTH] = !portraitMode ? GetUserWidthParams(pointPelvis) : GetUserHeightParams(pointPelvis);
                }
                if (estimateBodySlices && kinectManager.IsJointTracked(userId, (int)KinectInterop.JointType.Pelvis) && kinectManager.IsJointTracked(userId, (int)KinectInterop.JointType.Neck))
                {
                    Vector2 point1 = pointPelvis;
                    Vector2 point2 = kinectManager.MapSpacePointToDepthCoords(sensorIndex, kinectManager.GetJointKinectPosition(userId, (int)KinectInterop.JointType.Neck, false));
                    Vector2 sliceDir = (point2 - point1) / 4f;
                    bool sliceOnX = !portraitMode ? true : false;
                    bool sliceOnY = !portraitMode ? false : true;
                    Vector2 vSlicePoint = point1;
                    bodySlices[(int)BodySlice.TORSO_1] = GetBodySliceParams(BodySlice.TORSO_1, vSlicePoint, sliceOnX, sliceOnY, -1);
                    vSlicePoint += sliceDir;
                    bodySlices[(int)BodySlice.TORSO_2] = GetBodySliceParams(BodySlice.TORSO_2, vSlicePoint, sliceOnX, sliceOnY, -1);
                    vSlicePoint += sliceDir;
                    bodySlices[(int)BodySlice.TORSO_3] = GetBodySliceParams(BodySlice.TORSO_3, vSlicePoint, sliceOnX, sliceOnY, -1);
                    vSlicePoint += sliceDir;
                    bodySlices[(int)BodySlice.TORSO_4] = GetBodySliceParams(BodySlice.TORSO_4, vSlicePoint, sliceOnX, sliceOnY, -1);
                    vSlicePoint = point2;
                    bodySlices[(int)BodySlice.TORSO_5] = GetBodySliceParams(BodySlice.TORSO_5, vSlicePoint, sliceOnX, sliceOnY, -1);
                }
            }
            return bSliceSuccess;
        }
        // creates body slice data for user height
        private BodySliceData GetUserHeightParams(Vector2 pointSpineBase)
        {
            int depthLength = sensorData.depthImage.Length;
            int depthWidth = sensorData.depthImageWidth;
            int depthHeight = sensorData.depthImageHeight;
            Vector2 posTop = new Vector2(0, depthHeight);
            for (int i = 0, x = 0, y = 0; i < depthLength; i++)
            {
                if (sensorData.bodyIndexImage[i] == userBodyIndex)
                {
                    //if (posTop.y > y)
                    posTop = new Vector2(x, y);
                    break;
                }
                x++;
                if (x >= depthWidth)
                {
                    x = 0;
                    y++;
                }
            }
            Vector2 posBottom = new Vector2(0, -1);
            for (int i = depthLength - 1, x = depthWidth - 1, y = depthHeight - 1; i >= 0; i--)
            {
                if (sensorData.bodyIndexImage[i] == userBodyIndex)
                {
                    //if (posBottom.y < y)
                    posBottom = new Vector2(x, y);
                    break;
                }
                x--;
                if (x < 0)
                {
                    x = depthWidth - 1;
                    y--;
                }
            }
            BodySliceData sliceData = new BodySliceData();
            sliceData.sliceType = BodySlice.HEIGHT;
            sliceData.isSliceValid = false;
            if (posBottom.y >= 0)
            {
                sliceData.startDepthPoint = posTop;
                sliceData.endDepthPoint = posBottom;
                sliceData.depthPointsLength = (int)posBottom.y - (int)posTop.y + 1;
                int index1 = (int)posTop.y * depthWidth + (int)posTop.x;
                ushort depth1 = sensorData.depthImage[index1];
                sliceData.startKinectPoint = kinectManager.MapDepthPointToSpaceCoords(sensorIndex, sliceData.startDepthPoint, depth1, true);
                int index2 = (int)posBottom.y * depthWidth + (int)posBottom.x;
                ushort depth2 = sensorData.depthImage[index2];
                sliceData.endKinectPoint = kinectManager.MapDepthPointToSpaceCoords(sensorIndex, sliceData.endDepthPoint, depth2, true);
                sliceData.startColorPoint = kinectManager.MapDepthPointToColorCoords(sensorIndex, sliceData.startDepthPoint, depth1);
                sliceData.endColorPoint = kinectManager.MapDepthPointToColorCoords(sensorIndex, sliceData.endDepthPoint, depth2);
                if (sliceData.startColorPoint.y < 0)
                    sliceData.startColorPoint.y = 0;
                if (sliceData.endColorPoint.y >= sensorData.colorImageHeight)
                    sliceData.endColorPoint.y = sensorData.colorImageHeight - 1;
                sliceData.colorPointsLength = (int)sliceData.endColorPoint.y - (int)sliceData.startColorPoint.y + 1;
                // correct x-positions of depth points
                sliceData.startDepthPoint.x = pointSpineBase.x;
                sliceData.endDepthPoint.x = pointSpineBase.x;
                sliceData.diameter = (sliceData.endKinectPoint - sliceData.startKinectPoint).magnitude;
                sliceData.isSliceValid = true;
            }
            return sliceData;
        }
        // creates body slice data for user width
        private BodySliceData GetUserWidthParams(Vector2 pointSpineBase)
        {
            int depthLength = sensorData.depthImage.Length;
            int depthWidth = sensorData.depthImageWidth;
            //int depthHeight = sensorData.depthImageHeight;
            Vector2 posLeft = new Vector2(depthWidth, 0);
            Vector2 posRight = new Vector2(-1, 0);
            for (int i = 0, x = 0, y = 0; i < depthLength; i++)
            {
                if (sensorData.bodyIndexImage[i] == userBodyIndex)
                {
                    if (posLeft.x > x)
                        posLeft = new Vector2(x, y);
                    if (posRight.x < x)
                        posRight = new Vector2(x, y);
                }
                x++;
                if (x >= depthWidth)
                {
                    x = 0;
                    y++;
                }
            }
            BodySliceData sliceData = new BodySliceData();
            sliceData.sliceType = BodySlice.WIDTH;
            sliceData.isSliceValid = false;
            if (posRight.x >= 0)
            {
                sliceData.startDepthPoint = posLeft;
                sliceData.endDepthPoint = posRight;
                sliceData.depthPointsLength = (int)posRight.x - (int)posLeft.x + 1;
                int index1 = (int)posLeft.y * depthWidth + (int)posLeft.x;
                ushort depth1 = sensorData.depthImage[index1];
                sliceData.startKinectPoint = kinectManager.MapDepthPointToSpaceCoords(sensorIndex, sliceData.startDepthPoint, depth1, true);
                int index2 = (int)posRight.y * depthWidth + (int)posRight.x;
                ushort depth2 = sensorData.depthImage[index2];
                sliceData.endKinectPoint = kinectManager.MapDepthPointToSpaceCoords(sensorIndex, sliceData.endDepthPoint, depth2, true);
                sliceData.startColorPoint = kinectManager.MapDepthPointToColorCoords(sensorIndex, sliceData.startDepthPoint, depth1);
                sliceData.endColorPoint = kinectManager.MapDepthPointToColorCoords(sensorIndex, sliceData.endDepthPoint, depth2);
                if (sliceData.startColorPoint.x < 0)
                    sliceData.startColorPoint.x = 0;
                if (sliceData.endColorPoint.x >= sensorData.colorImageWidth)
                    sliceData.endColorPoint.x = sensorData.colorImageWidth - 1;
                sliceData.colorPointsLength = (int)sliceData.endColorPoint.x - (int)sliceData.startColorPoint.x + 1;
                // correct y-positions of depth points
                sliceData.startDepthPoint.y = pointSpineBase.y;
                sliceData.endDepthPoint.y = pointSpineBase.y;
                sliceData.diameter = (sliceData.endKinectPoint - sliceData.startKinectPoint).magnitude;
                sliceData.isSliceValid = true;
            }
            return sliceData;
        }
        // creates body slice data for the given body slice
        private BodySliceData GetBodySliceParams(BodySlice sliceType, Vector2 middlePoint, bool bSliceOnX, bool bSliceOnY, int maxDepthLength)
        {
            BodySliceData sliceData = new BodySliceData();
            sliceData.sliceType = sliceType;
            sliceData.isSliceValid = false;
            sliceData.depthPointsLength = 0;
            if (!kinectManager || middlePoint == Vector2.zero)
                return sliceData;
            if (!bSliceOnX && !bSliceOnY)
                return sliceData;
            middlePoint.x = Mathf.FloorToInt(middlePoint.x + 0.5f);
            middlePoint.y = Mathf.FloorToInt(middlePoint.y + 0.5f);
            int depthWidth = sensorData.depthImageWidth;
            int depthHeight = sensorData.depthImageHeight;
            int indexMid = (int)middlePoint.y * depthWidth + (int)middlePoint.x;
            byte userIndex = sensorData.bodyIndexImage[indexMid];
            if (userIndex != userBodyIndex)
                return sliceData;
            sliceData.startDepthPoint = middlePoint;
            sliceData.endDepthPoint = middlePoint;
            int indexDiff1 = 0;
            int indexDiff2 = 0;
            if (bSliceOnX)
            {
                // min-max
                int minIndex = (int)middlePoint.y * depthWidth;
                int maxIndex = (int)(middlePoint.y + 1) * depthWidth;
                // horizontal left
                int stepIndex = -1;
                indexDiff1 = TrackSliceInDirection(indexMid, stepIndex, minIndex, maxIndex, userIndex);
                // horizontal right
                stepIndex = 1;
                indexDiff2 = TrackSliceInDirection(indexMid, stepIndex, minIndex, maxIndex, userIndex);
            }
            else if (bSliceOnY)
            {
                // min-max
                int minIndex = 0;
                int maxIndex = depthHeight * depthWidth;
                // vertical up
                int stepIndex = -depthWidth;
                indexDiff1 = TrackSliceInDirection(indexMid, stepIndex, minIndex, maxIndex, userIndex);
                // vertical down
                stepIndex = depthWidth;
                indexDiff2 = TrackSliceInDirection(indexMid, stepIndex, minIndex, maxIndex, userIndex);
            }
            // calculate depth length
            sliceData.depthPointsLength = indexDiff1 + indexDiff2 + 1;
            // check for max length (used by upper legs)
            if (maxDepthLength > 0 && sliceData.depthPointsLength > maxDepthLength)
            {
                if (indexDiff1 > indexDiff2)
                    indexDiff1 = indexDiff2;
                else
                    indexDiff2 = indexDiff1;
                sliceData.depthPointsLength = indexDiff1 + indexDiff2 + 1;
            }
            // set start and end depth points
            if (bSliceOnX)
            {
                sliceData.startDepthPoint.x -= indexDiff1;
                sliceData.endDepthPoint.x += indexDiff2;
            }
            else if (bSliceOnY)
            {
                sliceData.startDepthPoint.y -= indexDiff1;
                sliceData.endDepthPoint.y += indexDiff2;
            }
            // start point
            int index1 = (int)sliceData.startDepthPoint.y * depthWidth + (int)sliceData.startDepthPoint.x;
            ushort depth1 = sensorData.depthImage[index1];
            sliceData.startKinectPoint = kinectManager.MapDepthPointToSpaceCoords(sensorIndex, sliceData.startDepthPoint, depth1, true);
            // end point
            int index2 = (int)sliceData.endDepthPoint.y * depthWidth + (int)sliceData.endDepthPoint.x;
            ushort depth2 = sensorData.depthImage[index2];
            sliceData.endKinectPoint = kinectManager.MapDepthPointToSpaceCoords(sensorIndex, sliceData.endDepthPoint, depth2, true);
            sliceData.startColorPoint = kinectManager.MapDepthPointToColorCoords(sensorIndex, sliceData.startDepthPoint, depth1);
            sliceData.endColorPoint = kinectManager.MapDepthPointToColorCoords(sensorIndex, sliceData.endDepthPoint, depth2);
            if (sliceData.startColorPoint.x < 0)
                sliceData.startColorPoint.x = 0;
            if (sliceData.endColorPoint.x >= sensorData.colorImageWidth)
                sliceData.endColorPoint.x = sensorData.colorImageWidth - 1;
            sliceData.colorPointsLength = (int)sliceData.endColorPoint.x - (int)sliceData.startColorPoint.x + 1;
            // diameter
            sliceData.diameter = (sliceData.endKinectPoint - sliceData.startKinectPoint).magnitude;
            sliceData.isSliceValid = true;
            return sliceData;
        }
        // determines the number of points in the given direction
        private int TrackSliceInDirection(int index, int stepIndex, int minIndex, int maxIndex, byte userIndex)
        {
            int indexDiff = 0;
            int errCount = 0;
            index += stepIndex;
            while (index >= minIndex && index < maxIndex)
            {
                if (sensorData.bodyIndexImage[index] != userIndex)
                {
                    errCount++;
                    if (errCount > 0) // allow 0 error(s)
                        break;
                }
                else
                {
                    errCount = 0;
                }
                index += stepIndex;
                indexDiff++;
            }
            return indexDiff;
        }
    }
}