/*
<copyright file="BGDatabaseMTManager.cs" company="BansheeGz">
    Copyright (c) 2018-2022 All Rights Reserved
</copyright>
*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using UnityEngine;
using UnityEngine.Events;
using Debug = UnityEngine.Debug;

namespace BansheeGz.BGDatabase
{
    /** <summary>
    1) Initialize  Db generated classes static fields and internal data structures, which are normally initialized on-demand
    2) Enable assets L2 cache. Preload assets and put them into L2 cache. If validateLoadedAsset=true, also validate that all set assets is loaded properly 
        (i.e. if some not null path is stored inside database- the asset should be loaded properly)
    3) Notify BGCodeGenUtils class that multi-threaded approach should be taken
     </summary>*/
    [BGPlugin(Version = "1.2")]
    public class BGDatabaseMTManager : MonoBehaviour
    {
        //this parameter ensure, that the asset was indeed loaded if some path is stored inside database
        public bool validateLoadedAsset = true;
        public bool showBenchMark = true;
        public bool showLogGUI;

        [SerializeField] private List<string> partialKeysIds = new List<string>();
        [SerializeField] private List<string> nameIndexMetaIds = new List<string>();
        public InitializationModeEnum initializationMode;
        public UnityEvent onFinished;
        public bool asyncAssetsLoading;
        public bool testAccess;
        public bool disableVSync;
        
        // not serializable
        private int fps;
        private int vSync;
        private Stopwatch stopwatch;
        private BGDatabaseMTAssetsLoaderI assetsLoader;
        private string log;
        
        public int PendingRequests => assetsLoader.PendingRequests;
        public Stopwatch Stopwatch => stopwatch;

        public List<string> PartialKeysIds => partialKeysIds;

        public List<string> NameIndexMetaIds => nameIndexMetaIds;

        public HashSet<BGMetaEntity> MetasWithNameIndex => Ids2ObjectList(nameIndexMetaIds, id => BGId.TryParse(id, out var idCasted) ? BGRepo.I.GetMeta(idCasted) : null);

        public HashSet<BGKey> PartialAccessKeys => Ids2ObjectList(partialKeysIds, id =>
        {
            if (string.IsNullOrEmpty(id)) return null;
            var parts = id.Split('.');
            if (parts.Length != 2) return null;
            if (!BGId.TryParse(parts[0], out var metaId)) return null;
            var meta = BGRepo.I.GetMeta(metaId);
            if (meta == null) return null;
            if (!BGId.TryParse(parts[1], out var keyId)) return null;
            return meta.GetKey(keyId, false);
        });


        void Awake()
        {
            if (initializationMode != InitializationModeEnum.Awake) return;
            Initialize();
        }

        void Start()
        {
            if (initializationMode != InitializationModeEnum.Start) return;
            Initialize();
        }

        private void OnGUI()
        {
            if (!showLogGUI) return;
            GUILayout.TextArea(log, GUILayout.Width(400), GUILayout.Height(150));
        }

        public void Initialize()
        {
            //prevent gameobject destroying - for what?
            //DontDestroyOnLoad(gameObject);

            //=================================================================================
            //              CodeGen utils
            //=================================================================================
            //notify CodeGenUtils class that multi-threaded approach should be taken
            BGCodeGenUtils.MultiThreadedEnvironment = true;
            
            //=================================================================================
            //              Internal structures
            //=================================================================================
            //get code gen addon (it's used to find generated properties names)
            var addon = BGRepo.I.Addons.Get<BGAddonCodeGen>();

            stopwatch = new Stopwatch();
            stopwatch.Start();

            var metasWithNameIndex = MetasWithNameIndex;
            var partialAccessKeys = PartialAccessKeys;
            //iterate over all tables
            BGRepo.I.ForEachMeta(meta =>
            {
                //get generated class 
                var metaTypeName = addon.GetMetaType(meta.Name);
                var metaType = BGUtil.GetType(metaTypeName);
                if (metaType == null) throw new Exception($"Can not find generated class {metaTypeName}");

                //initialize "Id" key
                meta.GetEntity(BGId.Empty);

                //initialize "name" key
                if (metasWithNameIndex.Count == 0 || metasWithNameIndex.Contains(meta)) meta.GetEntity("");

                //indexes
                meta.ForEachIndex(index =>
                {
                    index.Build();

                    //initialize static generated index field
                    InitProperty(metaType, "_" + index.Name,
                        propertyName => $"Can not find [{index.Name}] index property with name {propertyName}, generated class [{metaType.Name}]");
                });

                //keys
                meta.ForEachKey(key =>
                {
                    //make sure internal storage is created and values are indexed
                    if (partialAccessKeys.Count == 0 || partialAccessKeys.Contains(key)) key.BuildAll();
                    else key.Build();

                    //initialize static generated key field
                    InitProperty(metaType, "_" + key.Name, propertyName => $"Can not find [{key.Name}] key property with name {propertyName}, generated class [{metaType.Name}]");
                });

                //iterate fields
                meta.ForEachField(field =>
                {
                    //initialize static generated field field
                    InitProperty(metaType, "_" + addon.GetFieldName(field.Name),
                        propertyName => $"Can not find [{field.Name}] field property with name {propertyName}, generated class [{metaType.FullName}]");

                    //initialize reverse relation cache
                    if (field is BGRelationI relation) relation.GetRelatedIn(BGId.Empty);

                    //initialize all graphs for calculated fields
                    if (field is BGFieldCalcI calcField && field is BGStorable<BGFieldCalcValue> calcValueField)
                    {
                        calcField.Graph?.EnsureGraphIsLoaded();
                        field.ForEachValue(index =>
                        {
                            var value = calcValueField.GetStoredValue(index);
                            value?.Graph?.EnsureGraphIsLoaded();
                        });
                    }

                    //initialize programmable fields
                    if (field is BGFieldCodedI programmableField && field is BGStorable<BGFieldCodedValue> programmableValueField)
                    {
                        var @delegate = programmableField.DelegateInstance;
                        field.ForEachValue(index =>
                        {
                            var value = programmableValueField.GetStoredValue(index);
                            var delegateInstance = value?.DelegateInstance;
                        });
                    }
                });
            });
            stopwatch.Stop();
            if (showBenchMark) Log($"BGDatabaseMTManager benchmark: structures are initialized in {stopwatch.ElapsedMilliseconds} mls.");

            //=================================================================================
            //              VSync
            //=================================================================================
            //turn off vSync cause it seems to affect Addressables performance badly
            if (disableVSync)
            {
                fps = Application.targetFrameRate;
                vSync = QualitySettings.vSyncCount;
                Application.targetFrameRate = -1;
                QualitySettings.vSyncCount = 0;
            }

            //=================================================================================
            //              Unity assets
            //=================================================================================
            stopwatch.Reset();
            stopwatch.Start();
            
            //enable assets cache
            BGAssetsCache.Enabled = true;

            //preload and cache assets
            assetsLoader = asyncAssetsLoading
                ? new BGDatabaseMTAssetsLoaderAsync(validateLoadedAsset, FireOnFinished, AssetCacheListener)
                : new BGDatabaseMTAssetsLoaderSync(validateLoadedAsset, FireOnFinished);
            assetsLoader.Load();

            if (asyncAssetsLoading)
            {
                stopwatch.Stop();
                if (showBenchMark) Log($"BGDatabaseMTManager benchmark: assets loading requests are sent in {stopwatch.ElapsedMilliseconds} mls.");
                stopwatch.Reset();
                stopwatch.Start();
            }
            
            // print("DB initialized");
        }

        private void Log(string message)
        {
            log += message + Environment.NewLine;
            Debug.Log(message);
        }

        private BGDatabaseMTAssetsLoaderAsync.AssetCacheListener AssetCacheListener
        {
            get
            {
                var components = GetComponents<BGDatabaseMTAssetsLoaderAsync.AssetCacheListener>();
                if (components == null || components.Length == 0) return null;
                foreach (var component in components)
                {
                    if(!(component is MonoBehaviour mb)) continue;
                    if(mb.enabled) return component;
                }

                return null;
            }
        }
        
        

        private void FireOnFinished()
        {
            //restore vSync
            if (disableVSync)
            {
                Application.targetFrameRate = fps;
                QualitySettings.vSyncCount = vSync;
            }
            
            stopwatch.Stop();
            if (showBenchMark) Log($"BGDatabaseMTManager benchmark: assets are loaded in {stopwatch.ElapsedMilliseconds} mls.");
            stopwatch = null;
            onFinished?.Invoke();
            if (testAccess) new Thread(TestAccess){IsBackground = true}.Start();
        }

        private void TestAccess()
        {
            var count = 0;
            var errors = 0;
            var noValue = 0;
            BGRepo.I.ForEachMeta(meta =>
            {
                meta.ForEachField(field =>
                {
                    meta.ForEachEntity(entity =>
                    {
                        //check for no value
                        if (string.IsNullOrWhiteSpace(((BGStorable<string>)field).GetStoredValue(entity.Index)))
                        {
                            noValue++;
                            return;
                        }

                        count++;
                        object asset = null;
                        try
                        {
                            asset = field.GetValue(entity.Index);
                        }
                        catch (Exception e)
                        {
                            Debug.LogException(e);
                        }

                        if (asset == null)
                        {
                            errors++;
                            Debug.Log($"AssetsAccessTest : ERROR: asset can not be loaded! field={field.FullName}, entity#={entity.Index}");
                        }
                    });
                }, field => field is BGAssetLoaderA.WithLoaderI);
            });
            Log($"BGDatabaseMTManager : assets read test is finished. {count} assets were tested, {errors} errors found, {noValue} cells without value");
        }

        private static void InitProperty(Type type, string propertyName, Func<string, string> errorMessageProvider)
        {
            var property = BGPrivate.GetProperty(type, propertyName);
            if (property == null) throw new Exception(errorMessageProvider(propertyName));
            property.GetValue(null);
        }

        private static HashSet<T> Ids2ObjectList<T>(List<string> ids, Func<string, T> getValue) where T : BGMetaObject
        {
            var objects = new HashSet<T>();
            if (!BGRepo.Ok) return objects;
            foreach (var id in ids)
            {
                var obj = getValue(id);
                if (obj == null) continue;
                objects.Add(obj);
            }

            return objects;
        }

        public enum InitializationModeEnum
        {
            Awake,
            Start,
            Manual
        }
    }
}