// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

#if UNITY_WINRT && !UNITY_EDITOR
#define USE_WINRT
#endif

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using UnityEngine;
using UnityEngine.Assertions;
using HoloToolkit.Unity;

namespace HoloToolkit.Sharing.SyncModel
{
    /// <summary>
    /// The SyncObject class is a container object that can hold multiple SyncPrimitives.
    /// </summary>
    public class SyncObject : SyncPrimitive
    {
        private ObjectElementAdapter syncListener;                 // The sync object that we use to get sync events for this element
        private Dictionary<long, SyncPrimitive> primitiveMap;      // A dictionary of all of the primitives that are children of this object.  Maps element GUID to data
        private List<SyncPrimitive> primitives;                    // Raw list of child primitives

        public event Action<SyncObject> ObjectChanged;             // Invoked when one of the child primitives has changed
        public event Action<SyncObject> InitializationComplete;    // Invoked when this object has been fully initialized

        private ObjectElement internalObjectElement;
        /// <summary>
        /// This is the sync element for this object.
        /// </summary>
        public ObjectElement Element
        {
            get { return internalObjectElement; }
            internal set
            {
                if (internalObjectElement == null)
                {
                    internalObjectElement = value;
                    NetworkElement = value;

                    if (internalObjectElement != null)
                    {
                        CreateSyncListener(internalObjectElement);
                    }
                }
            }
        }

#if UNITY_EDITOR
        /// <summary>
        /// Raw Value used when displaying sync objects in the inspector.
        /// </summary>
        public override object RawValue
        {
            get { return null; }
        }
#endif

        private User owner;
        /// <summary>
        /// Optional user that owns this object.
        /// </summary>
        public User Owner
        {
            get { return owner; }
            set
            {
                if (owner == null)
                {
                    owner = value;
                }
            }
        }

        /// <summary>
        /// The Object Type as string.
        /// </summary>
        public string ObjectType
        {
            get
            {
                return internalObjectElement != null ? internalObjectElement.GetObjectType().GetString() : null;
            }
        }

        /// <summary>
        /// Returns the object element registered owner id.
        /// </summary>
        public int OwnerId
        {
            get { return internalObjectElement != null ? internalObjectElement.GetOwnerID() : int.MaxValue; }
        }

        public SyncObject(string field)
            : base(field)
        {
            InitializePrimitives();
        }

        public SyncObject()
            : base(string.Empty)
        {
            InitializePrimitives();
        }

        /// <summary>
        /// Returns a list of all child primitives.
        /// </summary>
        /// <returns>Array of SyncPrimitives.</returns>
        public SyncPrimitive[] GetChildren()
        {
            return primitives.ToArray();
        }

        private void InitializePrimitives()
        {
            primitiveMap = new Dictionary<long, SyncPrimitive>();
            primitives = new List<SyncPrimitive>();

            // Scan the type of object this is a look for the SyncDataAttribute
            Type baseType = GetType();

#if USE_WINRT
            var typeFields = baseType.GetRuntimeFields();
#else
            var typeFields = baseType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
#endif

            foreach (FieldInfo typeField in typeFields)
            {
                SyncDataAttribute attribute = null;

#if USE_WINRT
                attribute = typeField.GetCustomAttribute<SyncDataAttribute>(true);
#else
                object[] customAttributes = typeField.GetCustomAttributes(typeof(SyncDataAttribute), true);
                if (customAttributes.Length > 0)
                {
                    attribute = customAttributes[0] as SyncDataAttribute;
                }
#endif

                if (attribute != null)
                {
                    Type fieldType = typeField.FieldType;
                    string memberName = typeField.Name;

                    // Override the member name if provided
                    if (!string.IsNullOrEmpty(attribute.CustomFieldName))
                    {
                        memberName = attribute.CustomFieldName;
                    }

                    // Auto instantiate the primitive if it doesn't already exist
                    SyncPrimitive dataPrimitive = typeField.GetValue(this) as SyncPrimitive;
                    if (dataPrimitive == null)
                    {
                        try
                        {
                            // Constructors are not inherited, as per Section 1.6.7.1 of the C# Language Specification.
                            // This means that if a class subclasses Object or Primitive, they must either declare a constructor
                            // that takes the "memberName" property or use the default (parameter less constructor).

                            // First check if there is a constructor that takes the member name and if so call it
                            bool hasConstructor = fieldType.GetConstructor(new[] { typeof(string) }) != null;
                            if (hasConstructor)
                            {
                                dataPrimitive = (SyncPrimitive)Activator.CreateInstance(fieldType, memberName);
                            }
                            else
                            {
                                // Fallback on using the default constructor and manually assign the member name
                                dataPrimitive = (SyncPrimitive)Activator.CreateInstance(fieldType, null);
                                dataPrimitive.FieldName = memberName;
                            }

                            typeField.SetValue(this, dataPrimitive);
                        }
                        catch (Exception ex)
                        {
                            Debug.LogWarningFormat("Unable to create SyncPrimitive of type {0}.  Exception: {1}", memberName, ex);
                        }
                    }

                    if (dataPrimitive != null)
                    {
                        // Register the child
                        AddChild(dataPrimitive);
                    }
                }
            }
        }

        /// <summary>
        /// Register a new data model primitive as part of this object.  Can be called multiple times on the same child.
        /// </summary>
        /// <param name="data">Sync Primitive Data</param>
        protected void AddChild(SyncPrimitive data)
        {
            if (data.HasNetworkElement)
            {
                long guid = data.Guid;
                Assert.AreNotEqual(SharingClient.kInvalidXGuid, guid, "A primitive GUID should never be invalid if it is networked.");
                primitiveMap.Add(guid, data);
            }

            if (!primitives.Contains(data))
            {
                primitives.Add(data);
            }
        }

        /// <summary>
        /// Remove a child primitive that belongs to this object.
        /// </summary>
        /// <param name="data">Sync Primitive Data</param>
        protected void RemoveChild(SyncPrimitive data)
        {
            // Manually remove from maps
            if (primitives.Remove(data) && data.HasNetworkElement)
            {
                primitiveMap.Remove(data.NetworkElement.GetGUID());

                // Object has been removed internally, notify network
                ObjectElement parentElement = ObjectElement.Cast(data.NetworkElement.GetParent());
                if (parentElement != null)
                {
                    parentElement.RemoveElement(data.NetworkElement);
                }
            }
        }

        /// <summary>
        /// Handler for if a child is added.
        /// </summary>
        /// <param name="element">Added Element</param>
        protected virtual void OnElementAdded(Element element)
        {
            // Find the existing element.
            bool primitiveDataChanged = false;
            for (int i = 0; i < primitives.Count; i++)
            {
                if (primitives[i].XStringFieldName.IsEqual(element.GetName()))
                {
                    // Found it, register the element so it knows where to pull data from
                    primitives[i].AddFromRemote(element);

                    // Update the internal map
                    long guid = element.GetGUID();
                    primitiveMap[guid] = primitives[i];
                    primitiveDataChanged = true;
                    break;
                }
            }

            if (primitiveDataChanged)
            {
                // If there are no more primitives waiting to be added, finish the initialization
                if (primitiveMap.Count == primitives.Count)
                {
                    if (InitializationComplete != null)
                    {
                        InitializationComplete(this);
                    }
                }
            }
        }

        /// <summary>
        /// Handler for if a child is deleted.
        /// </summary>
        /// <param name="element">Deleted Element</param>
        protected virtual void OnElementDeleted(Element element)
        {
            // If the child exists, then remove it.
            long guid = element.GetGUID();
            if (primitiveMap.ContainsKey(guid))
            {
                RemoveChild(primitiveMap[guid]);
            }
        }

        /// <summary>
        /// Handler for if a child bool changes.
        /// </summary>
        /// <param name="elementID">Element Id</param>
        /// <param name="newValue">New Value</param>
        protected virtual void OnBoolElementChanged(long elementID, bool newValue)
        {
            if (primitiveMap.ContainsKey(elementID))
            {
                SyncPrimitive primitive = primitiveMap[elementID];
                primitive.UpdateFromRemote(newValue);
                NotifyPrimitiveChanged(primitive);
            }
            else
            {
                LogUnknownElement(elementID.ToString(), newValue.ToString(), typeof(bool));
            }
        }

        /// <summary>
        /// Handler for if a child int changes.
        /// </summary>
        /// <param name="elementID">Element Id</param>
        /// <param name="newValue">New Value</param>
        protected virtual void OnIntElementChanged(long elementID, int newValue)
        {
            if (primitiveMap.ContainsKey(elementID))
            {
                SyncPrimitive primitive = primitiveMap[elementID];
                primitive.UpdateFromRemote(newValue);
                NotifyPrimitiveChanged(primitive);
            }
            else
            {
                LogUnknownElement(elementID.ToString(), newValue.ToString(), typeof(int));
            }
        }

        /// <summary>
        /// Handler for if a child long changes
        /// </summary>
        /// <param name="elementID">Element Id</param>
        /// <param name="newValue">New Value</param>
        protected virtual void OnLongElementChanged(long elementID, long newValue)
        {
            if (primitiveMap.ContainsKey(elementID))
            {
                SyncPrimitive primitive = primitiveMap[elementID];
                primitive.UpdateFromRemote(newValue);
                NotifyPrimitiveChanged(primitive);
            }
            else
            {
                LogUnknownElement(elementID.ToString(), newValue.ToString(), typeof(long));
            }
        }

        /// <summary>
        /// Handler for if a child float changes
        /// </summary>
        /// <param name="elementID">Element Id</param>
        /// <param name="newValue">New Value</param>
        protected virtual void OnFloatElementChanged(long elementID, float newValue)
        {
            if (primitiveMap.ContainsKey(elementID))
            {
                SyncPrimitive primitive = primitiveMap[elementID];
                primitive.UpdateFromRemote(newValue);
                NotifyPrimitiveChanged(primitive);
            }
            else
            {
                LogUnknownElement(elementID.ToString(), newValue.ToString(CultureInfo.InvariantCulture), typeof(float));
            }
        }

        /// <summary>
        /// Handler for if a child double changes
        /// </summary>
        /// <param name="elementID">Element Id</param>
        /// <param name="newValue">New Value</param>
        protected virtual void OnDoubleElementChanged(long elementID, double newValue)
        {
            if (primitiveMap.ContainsKey(elementID))
            {
                SyncPrimitive primitive = primitiveMap[elementID];
                primitive.UpdateFromRemote(newValue);
                NotifyPrimitiveChanged(primitive);
            }
            else
            {
                LogUnknownElement(elementID.ToString(), newValue.ToString(CultureInfo.InvariantCulture), typeof(double));
            }
        }

        /// <summary>
        /// Handler for if a child string changes
        /// </summary>
        /// <param name="elementID">Element Id</param>
        /// <param name="newValue">New Value</param>
        protected virtual void OnStringElementChanged(long elementID, XString newValue)
        {
            if (primitiveMap.ContainsKey(elementID))
            {
                SyncPrimitive primitive = primitiveMap[elementID];
                primitive.UpdateFromRemote(newValue);
                NotifyPrimitiveChanged(primitive);
            }
            else
            {
                LogUnknownElement(elementID.ToString(), newValue.GetString(), typeof(string));
            }
        }

        private void LogUnknownElement(string elementName, string elementValue, Type elementType)
        {
            Debug.LogWarningFormat("Error: Trying to update an unknown child element!  Discarding update.  Type: {0}, Value: {1}, Id: {2}", elementType, elementValue, elementName);
        }

        protected virtual void NotifyPrimitiveChanged(SyncPrimitive primitive)
        {
            ObjectChanged.RaiseEvent(this);
        }

        private void CreateSyncListener(ObjectElement element)
        {
            // Create a listener for this
            syncListener = new ObjectElementAdapter();
            syncListener.ElementAddedEvent += OnElementAdded;
            syncListener.ElementDeletedEvent += OnElementDeleted;
            syncListener.BoolChangedEvent += OnBoolElementChanged;
            syncListener.IntChangedEvent += OnIntElementChanged;
            syncListener.LongChangedEvent += OnLongElementChanged;
            syncListener.FloatChangedEvent += OnFloatElementChanged;
            syncListener.DoubleChangedEvent += OnDoubleElementChanged;
            syncListener.StringChangedEvent += OnStringElementChanged;
            element.AddListener(syncListener);
        }

        #region From SyncPrimitive

        /// <summary>
        /// Initializes this object for local use.  Doesn't wait for network initialization. 
        /// </summary>
        /// <param name="parentElement">Parent element of this SyncObject.</param>
        public override void InitializeLocal(ObjectElement parentElement)
        {
            // Auto create element if needed
            if (Element == null)
            {
                Element = parentElement.CreateObjectElement(XStringFieldName, GetType().FullName, Owner);
                NetworkElement = Element;
            }

            // Initialize all primitives
            for (int i = 0; i < primitives.Count; i++)
            {
                SyncPrimitive data = primitives[i];
                data.InitializeLocal(Element);
                primitiveMap[data.Guid] = data;
            }

            // Complete the initialization
            if (InitializationComplete != null)
            {
                InitializationComplete(this);
            }
        }

        public override void AddFromRemote(Element remoteElement)
        {
            Element = ObjectElement.Cast(remoteElement);
            NetworkElement = remoteElement;
        }

        #endregion //From SyncPrimitive
    }
}