#nullable enable

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using UVC.Log;

namespace UVC.Data
{
    /// <summary>
    /// 키-값 쌍의 데이터를 관리하고 변경 사항을 추적하는 동적 데이터 객체입니다.
    /// SortedDictionary를 상속하여 키를 기준으로 정렬된 데이터를 제공합니다.
    /// </summary>
    /// <remarks>
    /// 이 클래스는 JSON과 호환되는 데이터 구조를 표현하며, 데이터 변경 추적 기능을 통해
    /// 효율적인 데이터 동기화를 지원합니다.
    /// </remarks>
    /// <example>
    /// <code>
    /// // DataMask 생성 및 설정
    /// var mask = new DataMask();
    /// mask.ObjectIdKey = "id";
    /// mask.ObjectName = "users";
    /// 
    /// // 필드 이름 변환 설정
    /// mask.NamesForReplace = new Dictionary<string, string>
    /// {
    ///     { "userName", "name" },
    ///     { "userEmail", "email" }
    /// };
    /// 
    /// // JObject 속성으로 마스킹 규칙 추가
    /// mask["include"] = new JArray("name", "email", "age");
    /// mask["exclude"] = new JArray("password", "token");
    /// 
    /// // DataObject에 마스크 적용하는 예시
    /// var dataObject = new DataObject();
    /// dataObject["userName"] = "홍길동";
    /// dataObject["userEmail"] = "hong@example.com";
    /// dataObject["password"] = "secret123";
    /// 
    /// // 마스크 적용 결과 (개념적 예시):
    /// // - userName은 name으로 변환됨
    /// // - userEmail은 email로 변환됨
    /// // - password는 제외됨
    /// </code>
    /// </example>
    public class DataObject : SortedDictionary<string, object>, IDataObject
    {

        /// <summary>
        /// 객체의 고유 식별자를 나타내는 속성입니다. DataArray에서 데이터를 식별하는 데 사용됩니다.
        /// </summary>
        public string Id { get => (IdKey != null && ContainsKey(IdKey)) ? this[IdKey].ToString() : this.First().Value.ToString(); }

        /// <summary>
        /// Id에 해당하는 key  문자열
        /// </summary>
        internal string? IdKey { get; set; } = null;

        /// <summary>
        /// DataObject의 이름을 나타내는 속성입니다.
        /// </summary>
        public string Name { get; internal set; } = string.Empty;


        /// <summary>
        /// 직접적인 변경이 있었던 키를 저장하는 리스트입니다.
        /// </summary>
        protected List<string> changedProperies = new List<string>();

        /// <summary>
        /// 변경된 속성의 키 목록을 읽기 전용으로 반환합니다.
        /// </summary>
        public ReadOnlyCollection<string> ChangedProperies => changedProperies.AsReadOnly();

        // 기존에 있던 속성 키를 추적하기 위한 집합
        private HashSet<string> existingProperties = new HashSet<string>();

        /// <summary>
        /// 기본 생성자입니다. 빈 데이터 객체를 생성합니다.
        /// </summary>
        public DataObject() { }

        /// <summary>
        /// JObject로부터 데이터 객체를 생성합니다.
        /// </summary>
        /// <param name="other">복사할 JObject 객체</param>
        public DataObject(JObject other)
        {
            // JObject로부터 속성을 복사
            foreach (var prop in other.Properties())
            {
                // JToken을 object로 변환
                this[prop.Name] = ConvertJTokenToObject(prop.Value);
                existingProperties.Add(prop.Name);
            }
        }

        /// <summary>
        /// Dictionary로 데이터 객체를 초기화합니다.
        /// </summary>
        /// <param name="dictionary">초기화에 사용할 Dictionary 객체</param>
        public DataObject(Dictionary<string, object> dictionary) : base(dictionary)
        {
            // 생성자에서 초기 속성들을 기존 속성으로 등록
            foreach (var key in dictionary.Keys)
            {
                existingProperties.Add(key);
            }
        }

        /// <summary>
        /// JToken을 적절한 C# 객체 타입으로 변환하는 헬퍼 메서드입니다.
        /// </summary>
        /// <param name="token">변환할 JToken 객체</param>
        /// <returns>변환된 C# 객체</returns>
        private object ConvertJTokenToObject(JToken token)
        {
            switch (token.Type)
            {
                case JTokenType.String:
                    return token.ToString();
                case JTokenType.Integer:
                    return token.ToObject<int>();
                case JTokenType.Float:
                    return token.ToObject<float>();
                case JTokenType.Boolean:
                    return token.ToObject<bool>();
                case JTokenType.Object:
                    return new DataObject((JObject)token);
                case JTokenType.Array:
                    JArray array = (JArray)token;
                    return new DataArray(array);
                default:
                    return token.ToString();
            }
        }

        /// <summary>
        /// 모든 프로퍼티를 변경된 것으로 표시합니다.
        /// 전체 데이터가 갱신되었을 때 사용합니다.
        /// </summary>
        public void InitData()
        {
            changedProperies.Clear();
            changedProperies.AddRange(this.Keys);
        }


        /// <summary>
        /// 속성이 변경될 때 호출되는 메서드입니다.
        /// 파생 클래스에서 오버라이드하여 추가 동작을 정의할 수 있습니다.
        /// </summary>
        /// <param name="propertyName">변경된 속성의 이름</param>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            // 기존에 존재하던 속성인 경우에만 변경된 것으로 추적
            if (existingProperties.Contains(propertyName))
            {
                if (!changedProperies.Contains(propertyName))
                {
                    changedProperies.Add(propertyName);
                }
            }
            else
            {
                // 새로 추가된 속성은 기존 속성 목록에 추가
                existingProperties.Add(propertyName);
            }
        }

        /// <summary>
        /// 새 속성을 추가하고 변경 사항을 추적합니다.
        /// </summary>
        /// <param name="propertyName">추가할 속성의 이름</param>
        /// <param name="value">속성의 값</param>
        public new void Add(string propertyName, object value)
        {
            // 추가하기 전에 확인 - 속성이 이미 있는지 확인
            bool isExisting = ContainsKey(propertyName);

            // 기본 구현 호출
            base.Add(propertyName, value);

            // 새로 추가된 속성이면 기존 속성 목록에 추가
            if (!isExisting)
            {
                existingProperties.Add(propertyName);
            }
        }

        /// <summary>
        /// 인덱서를 통해 속성값에 접근하고 설정합니다.
        /// 속성값이 변경될 때마다 변경 사항을 자동으로 추적합니다.
        /// </summary>
        /// <param name="key">접근할 속성의 키</param>
        /// <returns>속성값</returns>
        public new object this[string key]
        {
            get => base[key];
            set
            {
                // 속성 설정 전에 기존에 있는 속성인지 확인
                bool isExisting = ContainsKey(key) || existingProperties.Contains(key);

                // 기본 구현 호출
                base[key] = value;

                // 기존 속성이었다면 변경된 것으로 처리
                if (isExisting)
                {
                    if (!changedProperies.Contains(key))
                    {
                        changedProperies.Add(key);
                    }
                }
                else
                {
                    // 새로 추가된 속성은 기존 속성으로 등록
                    existingProperties.Add(key);
                }

                OnPropertyChanged(key);
            }
        }

        /// <summary>
        /// 지정된 속성의 값을 정수(int)로 변환하여 반환합니다.
        /// </summary>
        /// <param name="propertyName">속성 이름</param>
        /// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
        /// <returns>변환된 정수 값 또는 기본값</returns>
        public int GetInt(string propertyName, int defaultValue = 0)
        {
            if (TryGetValue(propertyName, out object value) && value != null)
            {
                if (value is int intValue)
                    return intValue;
                return Convert.ToInt32(value);
            }
            return defaultValue;
        }

        /// <summary>
        /// 지정된 속성의 값을 문자열(string)로 변환하여 반환합니다.
        /// </summary>
        /// <param name="propertyName">속성 이름</param>
        /// <param name="defaultValue">속성이 없거나 null인 경우 반환할 기본값</param>
        /// <returns>변환된 문자열 값 또는 기본값</returns>
        public string? GetString(string propertyName, string? defaultValue = null)
        {
            if (TryGetValue(propertyName, out object value) && value != null)
            {
                return value.ToString();
            }
            return defaultValue;
        }

        /// <summary>
        /// 지정된 속성의 값을 불리언(bool)으로 변환하여 반환합니다.
        /// </summary>
        /// <param name="propertyName">속성 이름</param>
        /// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
        /// <returns>변환된 불리언 값 또는 기본값</returns>
        public bool GetBool(string propertyName, bool defaultValue = false)
        {
            if (TryGetValue(propertyName, out object value) && value != null)
            {
                if (value is bool boolValue)
                    return boolValue;
                return Convert.ToBoolean(value);
            }
            return defaultValue;
        }

        /// <summary>
        /// 지정된 속성의 값을 부동 소수점(float)으로 변환하여 반환합니다.
        /// </summary>
        /// <param name="propertyName">속성 이름</param>
        /// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
        /// <returns>변환된 부동 소수점 값 또는 기본값</returns>
        public float GetFloat(string propertyName, float defaultValue = 0f)
        {
            if (TryGetValue(propertyName, out object value) && value != null)
            {
                if (value is float floatValue)
                    return floatValue;
                return Convert.ToSingle(value);
            }
            return defaultValue;
        }

        /// <summary>
        /// 지정된 속성의 값을 더블 정밀도 부동 소수점(double)으로 변환하여 반환합니다.
        /// </summary>
        /// <param name="propertyName">속성 이름</param>
        /// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
        /// <returns>변환된 더블 값 또는 기본값</returns>
        public double GetDouble(string propertyName, double defaultValue = 0.0)
        {
            if (TryGetValue(propertyName, out object value) && value != null)
            {
                if (value is double doubleValue)
                    return doubleValue;
                return Convert.ToDouble(value);
            }
            return defaultValue;
        }

        /// <summary>
        /// 지정된 속성의 값을 DateTime으로 변환하여 반환합니다.
        /// </summary>
        /// <param name="propertyName">속성 이름</param>
        /// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
        /// <returns>변환된 DateTime 값 또는 기본값</returns>
        public DateTime? GetDateTime(string propertyName, DateTime? defaultValue = null)
        {
            if (TryGetValue(propertyName, out object value) && value != null)
            {
                if (value is DateTime dateTime)
                    return dateTime;
                return Convert.ToDateTime(value);
            }
            return defaultValue;
        }

        /// <summary>
        /// 지정된 속성의 값을 열거형(Enum)으로 변환하여 반환합니다.
        /// </summary>
        /// <typeparam name="T">변환할 열거형 타입</typeparam>
        /// <param name="propertyName">속성 이름</param>
        /// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
        /// <returns>변환된 열거형 값 또는 기본값</returns>
        public T GetEnum<T>(string propertyName, T defaultValue = default) where T : Enum
        {
            if (TryGetValue(propertyName, out object value) && value != null)
            {
                if (value is T enumValue)
                    return enumValue;
                return (T)Enum.Parse(typeof(T), value.ToString());
            }
            return defaultValue;
        }

        /// <summary>
        /// 지정된 속성의 값을 IDataObject로 반환합니다.
        /// </summary>
        /// <param name="propertyName">속성 이름</param>
        /// <param name="defaultValue">속성이 없거나 IDataObject가 아닌 경우 반환할 기본값</param>
        /// <returns>IDataObject 인터페이스를 구현한 객체 또는 기본값</returns>
        public IDataObject? Get(string propertyName, IDataObject? defaultValue = null)
        {
            if (TryGetValue(propertyName, out object value) && value != null)
            {
                if (value is IDataObject dataObject) return dataObject;
            }
            return defaultValue;
        }

        /// <summary>
        /// 지정된 속성의 값을 DataArray로 변환하여 반환합니다.
        /// </summary>
        /// <param name="propertyName">속성 이름</param>
        /// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
        /// <returns>DataArray 객체 또는 기본값</returns>
        public DataArray? GetDataArray(string propertyName, DataArray? defaultValue = null)
        {
            if (TryGetValue(propertyName, out object value) && value != null)
            {
                if (value is DataArray dataArray)
                    return dataArray;
                if (value is JArray jArray)
                    return new DataArray(jArray);
            }
            return defaultValue;
        }

        /// <summary>
        /// 지정된 속성의 값을 DataObject로 변환하여 반환합니다.
        /// </summary>
        /// <param name="propertyName">속성 이름</param>
        /// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
        /// <returns>DataObject 객체 또는 기본값</returns>
        public DataObject? GetDataObject(string propertyName, DataObject? defaultValue = null)
        {
            if (TryGetValue(propertyName, out object value) && value != null)
            {
                if (value is DataObject dataObject)
                    return dataObject;
                if (value is JObject jObject)
                    return new DataObject(jObject);
                if (value is Dictionary<string, object> dict)
                    return new DataObject(dict);
            }       
            
            // propertyName으로 직접 객체를 찾지 못했거나, 찾았지만 적절한 타입이 아닌 경우,
            // 현재 DataObject의 값들 중 DataObject 타입인 것들을 순회하며 내부에서 propertyName을 다시 검색합니다.
            foreach (KeyValuePair<string, object> keyValue in this)
            {
                if (keyValue.Value is DataObject nestedDataObject)
                {
                    // 중첩된 DataObject에서 propertyName을 검색합니다.
                    // 여기서 defaultValue로 null을 전달하여, 이 탐색 단계에서 찾지 못하면 
                    // 외부 호출의 defaultValue가 사용되도록 합니다.
                    DataObject? foundInNested = nestedDataObject.GetDataObject(propertyName, null);
                    if (foundInNested != null)
                    {
                        return foundInNested; // 중첩된 객체에서 찾았으면 반환
                    }
                }
            }
            
            return defaultValue;
        }

        /// <summary>
        /// 속성이 제거될 때 기존 속성 목록에서도 제거합니다.
        /// </summary>
        /// <param name="key">제거할 속성의 키</param>
        /// <returns>제거 성공 여부</returns>
        public new bool Remove(string key)
        {
            bool result = base.Remove(key);
            if (result)
            {
                existingProperties.Remove(key);
                changedProperies.Remove(key);
            }
            return result;
        }

        /// <summary>
        /// 모든 속성을 제거하고 추적 리스트를 초기화합니다.
        /// </summary>
        public void RemoveAll()
        {
            base.Clear();
            existingProperties.Clear();
            changedProperies.Clear();
        }

        /// <summary>
        /// 현재 DataObject를 JObject로 변환합니다.
        /// </summary>
        /// <returns>현재 객체의 데이터를 담은 JObject</returns>
        public JObject ToJObject()
        {
            JObject result = new JObject();
            foreach (var kvp in this)
            {
                result[kvp.Key] = JToken.FromObject(kvp.Value);
            }
            return result;
        }

        /// <summary>
        /// 다른 DataObject와 현재 객체를 비교하여 다른 부분만 설정합니다.
        /// 변경된 키는 자동으로 추적됩니다.
        /// </summary>
        /// <param name="other">비교할 DataObject</param>
        public void UpdateDifferent(IDataObject other)
        {
            if (other == null || other is not DataObject) return;
            changedProperies.Clear();
            foreach (var keyValue in (DataObject)other)
            {
                if (!this.ContainsKey(keyValue.Key) || !this[keyValue.Key].Equals(keyValue.Value))
                {
                    this[keyValue.Key] = keyValue.Value;
                    changedProperies.Add(keyValue.Key);
                }
            }
        }

        /// <summary>
        /// 업데이트된 속성만 포함하는 새로운 DataObject를 반환합니다.
        /// </summary>
        /// <returns>업데이트 된 항목만 가지고 있는 DataObject</returns>
        public IDataObject GetUpdatedObject()
        {
            DataObject updated = new DataObject();
            foreach (var key in changedProperies)
            {
                if (this.ContainsKey(key))
                {
                    updated[key] = this[key];
                }
            }
            return updated;
        }

        /// <summary>
        /// 업데이트 된 속성의 수.
        /// </summary>
        /// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다.</returns>
        public int UpdatedCount { get => changedProperies.Count; }

        /// <summary>
        /// DataObject의 내용을 문자열로 반환합니다.
        /// 각 키-값 쌍이 "키:값" 형식으로 쉼표로 구분되어 표시됩니다.
        /// </summary>
        /// <returns>DataObject의 내용을 나타내는 문자열</returns>
        public override string ToString()
        {
            return string.Join(", ", this.Select(kvp => $"{kvp.Key}:{kvp.Value}"));
        }

    }


    /// <summary>
    /// DataObject 인스턴스를 재사용하기 위한 객체 풀 클래스입니다.
    /// 메모리 할당과 가비지 컬렉션을 최소화하기 위해 DataObject 인스턴스를 관리합니다.
    /// </summary>
    /// <remarks>
    /// 객체 풀링은 자주 생성 및 삭제되는 객체의 성능을 향상시키는 패턴입니다.
    /// 사용이 끝난 객체를 삭제하는 대신 풀에 반환하고, 새 객체가 필요할 때 풀에서 꺼내 재사용합니다.
    /// </remarks>
    /// <example>
    /// <code>
    /// // 객체 풀에서 DataObject 가져오기
    /// DataObject obj = DataObjectPool.GetDataObjectFromPool();
    /// 
    /// // 객체 사용
    /// obj["name"] = "홍길동";
    /// obj["age"] = 30;
    /// 
    /// // 작업 완료 후 풀에 반환
    /// DataObjectPool.ReturnToPool(obj);
    /// </code>
    /// </example>
    public static class DataObjectPool
    {
        /// <summary>
        /// DataObject 인스턴스를 저장하는 큐입니다.
        /// </summary>
        private static Queue<DataObject> dataObjectPool = new Queue<DataObject>();
        /// <summary>
        /// 풀의 최대 크기입니다. 이 크기를 초과하는 객체는 풀에 저장되지 않습니다.
        /// </summary>
        private static int maxPoolSize = 1000;

        /// <summary>
        /// 풀에서 DataObject 인스턴스를 가져옵니다.
        /// 풀이 비어있으면 새 인스턴스를 생성하여 반환합니다.
        /// </summary>
        /// <returns>재사용 가능한 DataObject 인스턴스</returns>
        public static DataObject GetDataObjectFromPool()
        {
            if (dataObjectPool.Count > 0)
            {
                return dataObjectPool.Dequeue();
            }
            return new DataObject();
        }

        /// <summary>
        /// 사용이 완료된 DataObject를 풀에 반환합니다.
        /// 객체는 반환 전에 초기화되어 모든 속성이 제거됩니다.
        /// </summary>
        /// <param name="obj">풀에 반환할 DataObject 인스턴스</param>
        public static void ReturnToPool(DataObject obj)
        {
            if (obj != null && dataObjectPool.Count < maxPoolSize)
            {
                obj.RemoveAll(); // 재사용 전 정리
                dataObjectPool.Enqueue(obj);
            }
        }
    }

}