//========= Copyright 2016-2018, HTC Corporation. All rights reserved. ===========

using HTC.UnityPlugin.Utility;
using HTC.UnityPlugin.VRModuleManagement;
using System;
using UnityEngine;

namespace HTC.UnityPlugin.Vive
{
    // ViveRoleProperty is a serializable class that preserve vive role using 2 strings.
    // There also has a property drawer so you can use it as a serialized field in your MonoBevaviour.
    // Note that when deserializing, result of type and value is based on the valid role info stored in ViveRoleEnum class
    [Serializable]
    public class ViveRoleProperty
    {
        public delegate void RoleChangedListener();
        public delegate void RoleChangedListenerEx(Type previousRoleType, int previousRoleValue);
        public delegate void DeviceIndexChangedListener(uint deviceIndex);

        public static readonly Type DefaultRoleType = typeof(HandRole);
        public static readonly int DefaultRoleValue = (int)HandRole.RightHand;

        [SerializeField]
        private string m_roleTypeFullName;
        [SerializeField]
        private string m_roleValueName;
        [SerializeField]
        private int m_roleValueInt;

        private bool m_isTypeDirty = true;
        private bool m_isValueDirty = true;
        private bool m_lockUpdate;

        private ViveRole.IMap m_roleMap = null;
        private Type m_roleType = DefaultRoleType;
        private int m_roleValue = DefaultRoleValue;
        private uint m_deviceIndex = VRModule.INVALID_DEVICE_INDEX;

        private Action m_onChanged;
        private RoleChangedListener m_onRoleChanged;
        private RoleChangedListenerEx m_onRoleChangedEx;
        private DeviceIndexChangedListener m_onDeviceIndexChanged;

        public Type roleType
        {
            get
            {
                Update();
                return m_roleType;
            }
            set
            {
                m_isTypeDirty |= ChangeProp.Set(ref m_roleTypeFullName, value.FullName);
                Update();
            }
        }

        public int roleValue
        {
            get
            {
                Update();
                return m_roleValue;
            }
            set
            {
                if (ChangeProp.Set(ref m_roleValueName, m_roleMap.RoleValueInfo.GetNameByRoleValue(value)))
                {
                    m_roleValueInt = value;
                    m_isValueDirty = true;
                }

                Update();
            }
        }

        [Obsolete("Use onRoleChanged instead")]
        public event Action Changed
        {
            add { m_onChanged += value; }
            remove { m_onChanged -= value; }
        }

        public event RoleChangedListener onRoleChanged
        {
            add { m_onRoleChanged += value; }
            remove { m_onRoleChanged -= value; }
        }

        public event RoleChangedListenerEx onRoleChangedEx
        {
            add { m_onRoleChangedEx += value; }
            remove { m_onRoleChangedEx -= value; }
        }

        public event DeviceIndexChangedListener onDeviceIndexChanged
        {
            add
            {
                var wasEmpty = m_onDeviceIndexChanged == null;

                m_onDeviceIndexChanged += value;

                if (wasEmpty && m_onDeviceIndexChanged != null && m_roleMap != null)
                {
                    m_deviceIndex = m_roleMap.GetMappedDeviceByRoleValue(m_roleValue); // update deviceIndex before first time listening to MappingChanged event
                    m_roleMap.onRoleValueMappingChanged += OnMappingChanged;
                }
            }
            remove
            {
                var wasEmpty = m_onDeviceIndexChanged == null;

                m_onDeviceIndexChanged -= value;

                if (!wasEmpty && m_onDeviceIndexChanged == null && m_roleMap != null)
                {
                    m_roleMap.onRoleValueMappingChanged -= OnMappingChanged;
                }
            }
        }

        public static ViveRoleProperty New()
        {
            return New(DefaultRoleType, DefaultRoleValue);
        }

        public static ViveRoleProperty New(Type type, int value)
        {
            return New(type.FullName, ViveRoleEnum.GetInfo(type).GetNameByRoleValue(value));
        }

        public static ViveRoleProperty New<TRole>(TRole role)
        {
            return New(typeof(TRole).FullName, role.ToString());
        }

        public static ViveRoleProperty New(string typeFullName, string valueName)
        {
            var prop = new ViveRoleProperty();
            prop.m_roleTypeFullName = typeFullName;
            prop.m_roleValueName = valueName;

            Type roleType;
            if (ViveRoleEnum.ValidViveRoleTable.TryGetValue(typeFullName, out roleType))
            {
                var roleInfo = ViveRoleEnum.GetInfo(roleType);
                var roleIndex = roleInfo.GetElementIndexByName(valueName);
                if (roleIndex >= 0)
                {
                    prop.m_roleValueInt = roleInfo.GetRoleValueByElementIndex(roleIndex);
                }
                else
                {
                    prop.m_roleValueInt = roleInfo.InvalidRoleValue;
                }
            }

            return prop;
        }

        public void SetTypeDirty() { m_isTypeDirty = true; }

        public void SetValueDirty() { m_isValueDirty = true; }

        private void OnMappingChanged(ViveRole.IMap map, ViveRole.MappingChangedEventArg args)
        {
            if (args.roleValue == m_roleValue)
            {
                m_onDeviceIndexChanged(m_deviceIndex = args.currentDeviceIndex);
            }
        }

        // update type and value changes
        public void Update()
        {
            if (m_lockUpdate && (m_isTypeDirty || m_isValueDirty)) { throw new Exception("Can't change value during onChange event callback"); }

            var oldRoleType = m_roleType;
            var oldRoleValue = m_roleValue;
            var roleTypeChanged = false;
            var roleValueChanged = false;
            var deviceIndexChanged = false;

            if (m_isTypeDirty || m_roleType == null)
            {
                m_isTypeDirty = false;
                
                if (string.IsNullOrEmpty(m_roleTypeFullName) || !ViveRoleEnum.ValidViveRoleTable.TryGetValue(m_roleTypeFullName, out m_roleType))
                {
                    m_roleType = DefaultRoleType;
                }

                roleTypeChanged = oldRoleType != m_roleType;
            }

            // maintain m_roleMap cache
            // m_roleMap could be null on first update
            if (roleTypeChanged || m_roleMap == null)
            {
                if (m_onDeviceIndexChanged != null)
                {
                    if (m_roleMap != null)
                    {
                        m_roleMap.onRoleValueMappingChanged -= OnMappingChanged;
                        m_roleMap = ViveRole.GetMap(m_roleType);
                        m_roleMap.onRoleValueMappingChanged += OnMappingChanged;
                    }
                    else
                    {
                        m_roleMap = ViveRole.GetMap(m_roleType);
                        m_deviceIndex = m_roleMap.GetMappedDeviceByRoleValue(m_roleValue); // update deviceIndex before first time listening to MappingChanged event
                        m_roleMap.onRoleValueMappingChanged += OnMappingChanged;
                    }
                }
                else
                {
                    m_roleMap = ViveRole.GetMap(m_roleType);
                }
            }

            if (m_isValueDirty || roleTypeChanged)
            {
                m_isValueDirty = false;
                
                var info = m_roleMap.RoleValueInfo;
                if (string.IsNullOrEmpty(m_roleValueName) || !info.TryGetRoleValueByName(m_roleValueName, out m_roleValue))
                {
                    m_roleValue = info.IsValidRoleValue(m_roleValueInt) ? m_roleValueInt : info.InvalidRoleValue;
                }

                roleValueChanged = oldRoleValue != m_roleValue;
            }

            if (roleTypeChanged || roleValueChanged)
            {
                if (m_onDeviceIndexChanged != null)
                {
                    var oldDeviceIndex = m_deviceIndex;
                    m_deviceIndex = m_roleMap.GetMappedDeviceByRoleValue(m_roleValue);

                    if (VRModule.IsValidDeviceIndex(oldDeviceIndex) || VRModule.IsValidDeviceIndex(m_deviceIndex))
                    {
                        deviceIndexChanged = oldDeviceIndex != m_deviceIndex;
                    }
                }

                m_lockUpdate = true;

                if (m_onChanged != null) { m_onChanged(); }

                if (m_onRoleChanged != null) { m_onRoleChanged(); }

                if (m_onRoleChangedEx != null) { m_onRoleChangedEx(oldRoleType, oldRoleValue); }

                if (deviceIndexChanged) { m_onDeviceIndexChanged(m_deviceIndex); }

                m_lockUpdate = false;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TRole">Can be DeviceRole, HandRole or TrackerRole</typeparam>
        /// <param name="role"></param>
        public void SetEx<TRole>(TRole role)
        {
            Set(typeof(TRole).FullName, role.ToString());
        }

        public void Set(Type type, int value)
        {
            Set(type.FullName, ViveRoleEnum.GetInfo(type).GetNameByRoleValue(value));
        }

        public void Set(ViveRoleProperty prop)
        {
            Set(prop.m_roleTypeFullName, prop.m_roleValueName);
        }

        // set by value name to preserve the enum element, since different enum element could have same enum value
        public void Set(string typeFullName, string valueName)
        {
            m_isTypeDirty |= ChangeProp.Set(ref m_roleTypeFullName, typeFullName);
            m_isValueDirty |= ChangeProp.Set(ref m_roleValueName, valueName);

            Update();
        }

        public uint GetDeviceIndex()
        {
            Update();

            if (m_onDeviceIndexChanged == null)
            {
                return m_roleMap.GetMappedDeviceByRoleValue(m_roleValue);
            }
            else
            {
                return m_deviceIndex;
            }
        }

        public TRole ToRole<TRole>()
        {
            Update();

            TRole role;
            var roleInfo = ViveRoleEnum.GetInfo<TRole>();
            if (m_roleType != typeof(TRole) || !roleInfo.TryGetRoleByName(m_roleValueName, out role))
            {
                // return invalid if role type not match or the value name not found in roleInfo
                return roleInfo.InvalidRole;
            }

            return role;
        }

        public bool IsRole(Type type, int value)
        {
            Update();

            return m_roleType == type && m_roleValue == value;
        }

        public bool IsRole<TRole>(TRole role)
        {
            Update();

            if (m_roleType != typeof(TRole)) { return false; }
            var roleInfo = ViveRoleEnum.GetInfo<TRole>();

            return m_roleValue == roleInfo.ToRoleValue(role);
        }

        public static bool operator ==(ViveRoleProperty p1, ViveRoleProperty p2)
        {
            if (ReferenceEquals(p1, p2)) { return true; }
            if (ReferenceEquals(p1, null)) { return false; }
            if (ReferenceEquals(p2, null)) { return false; }
            if (p1.roleType != p2.roleType) { return false; }
            if (p1.roleValue != p2.roleValue) { return false; }
            return true;
        }

        public static bool operator !=(ViveRoleProperty p1, ViveRoleProperty p2)
        {
            return !(p1 == p2);
        }

        public bool Equals(ViveRoleProperty prop)
        {
            return this == prop;
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as ViveRoleProperty);
        }

        public override int GetHashCode()
        {
            Update();

            var hash = 17;
            hash = hash * 23 + (m_roleType == null ? 0 : m_roleType.GetHashCode());
            hash = hash * 23 + m_roleValue.GetHashCode();
            return hash;
        }

        public override string ToString()
        {
            Update();

            return m_roleType.Name + "." + ViveRoleEnum.GetInfo(m_roleType).GetNameByRoleValue(m_roleValue);
        }
    }
}