﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using Unity.Profiling;
using UnityEngine;
using UnityEngine.Profiling;
using Debug = UnityEngine.Debug;
using VRC.Dynamics;

// ReSharper disable RedundantNameQualifier

namespace VRC.SDKBase.Validation
{
    public static class AvatarValidation
    {
        private const int MAX_STATIONS_PER_AVATAR = 6;
        private const float MAX_STATION_ACTIVATE_DISTANCE = 0f;
        private const float MAX_STATION_LOCATION_DISTANCE = 2f;
        private const float MAX_STATION_COLLIDER_DIMENSION = 2f;

        public static readonly string[] ComponentTypeWhiteListCommon = new string[]
        {
            #if UNITY_STANDALONE
            "DynamicBone",
            "DynamicBoneCollider",
            "RootMotion.FinalIK.IKExecutionOrder",
            "RootMotion.FinalIK.VRIK",
            "RootMotion.FinalIK.FullBodyBipedIK",
            "RootMotion.FinalIK.LimbIK",
            "RootMotion.FinalIK.AimIK",
            "RootMotion.FinalIK.BipedIK",
            "RootMotion.FinalIK.GrounderIK",
            "RootMotion.FinalIK.GrounderFBBIK",
            "RootMotion.FinalIK.GrounderVRIK",
            "RootMotion.FinalIK.GrounderQuadruped",
            "RootMotion.FinalIK.TwistRelaxer",
            "RootMotion.FinalIK.ShoulderRotator",
            "RootMotion.FinalIK.FBBIKArmBending",
            "RootMotion.FinalIK.FBBIKHeadEffector",
            "RootMotion.FinalIK.FABRIK",
            "RootMotion.FinalIK.FABRIKChain",
            "RootMotion.FinalIK.FABRIKRoot",
            "RootMotion.FinalIK.CCDIK",
            "RootMotion.FinalIK.RotationLimit",
            "RootMotion.FinalIK.RotationLimitHinge",
            "RootMotion.FinalIK.RotationLimitPolygonal",
            "RootMotion.FinalIK.RotationLimitSpline",
            "UnityEngine.Cloth",
            "UnityEngine.Light",
            "UnityEngine.BoxCollider",
            "UnityEngine.SphereCollider",
            "UnityEngine.CapsuleCollider",
            "UnityEngine.Rigidbody",
            "UnityEngine.Joint",
            "UnityEngine.Animations.AimConstraint",
            "UnityEngine.Animations.LookAtConstraint",
            "UnityEngine.Animations.ParentConstraint",
            "UnityEngine.Animations.PositionConstraint",
            "UnityEngine.Animations.RotationConstraint",
            "UnityEngine.Animations.ScaleConstraint",
            "UnityEngine.Camera",
            "UnityEngine.AudioSource",
            "ONSPAudioSource",
            #endif
            #if !VRC_CLIENT
            "VRC.Core.PipelineSaver",
            #endif
            "VRC.Core.PipelineManager",
            "UnityEngine.Transform",
            "UnityEngine.Animator",
            "UnityEngine.SkinnedMeshRenderer",
            "LimbIK", // our limbik based on Unity ik
            "LoadingAvatarTextureAnimation",
            "UnityEngine.MeshFilter",
            "UnityEngine.MeshRenderer",
            "UnityEngine.Animation",
            "UnityEngine.ParticleSystem",
            "UnityEngine.ParticleSystemRenderer",
            "UnityEngine.TrailRenderer",
            "UnityEngine.FlareLayer",
            "UnityEngine.GUILayer",
            "UnityEngine.LineRenderer",
            "RealisticEyeMovements.EyeAndHeadAnimator",
            "RealisticEyeMovements.LookTargetController",
        };

        public static readonly string[] ComponentTypeWhiteListSdk2 = new string[]
        {
            #if UNITY_STANDALONE
            "VRCSDK2.VRC_SpatialAudioSource",
            #endif
            "VRCSDK2.VRC_AvatarDescriptor",
            "VRCSDK2.VRC_AvatarVariations",
            "VRCSDK2.VRC_IKFollower",
            "VRCSDK2.VRC_Station",
        };

        public static readonly string[] ComponentTypeWhiteListSdk3 = new string[]
        {
            #if UNITY_STANDALONE
            "VRC.SDK3.Avatars.Components.VRCSpatialAudioSource",
            #endif
            "VRC.SDK3.VRCTestMarker",
            "VRC.SDK3.Avatars.Components.VRCAvatarDescriptor",
            "VRC.SDK3.Avatars.Components.VRCStation",
            "VRC.SDK3.Dynamics.PhysBone.Components.VRCPhysBone",
            "VRC.SDK3.Dynamics.PhysBone.Components.VRCPhysBoneCollider",
            "VRC.SDK3.Dynamics.Contact.Components.VRCContactSender",
            "VRC.SDK3.Dynamics.Contact.Components.VRCContactReceiver",
            "VRC.SDKBase.VRC_Slider",
        };

#pragma warning disable 0414

        private static string[] CombinedComponentTypeWhiteListSdk2 = null;
        private static string[] CombinedComponentTypeWhiteListSdk3 = null;

#pragma warning restore 0414

        public static readonly string[] ShaderWhiteList = new string[]
        {
            "VRChat/Mobile/Standard Lite",
            "VRChat/Mobile/Diffuse",
            "VRChat/Mobile/Bumped Diffuse",
            "VRChat/Mobile/Bumped Mapped Specular",
            "VRChat/Mobile/Toon Lit",
            "VRChat/Mobile/MatCap Lit",

            "VRChat/Mobile/Particles/Additive",
            "VRChat/Mobile/Particles/Multiply",
        };

        public static bool ps_limiter_enabled = false;
        public static int ps_max_particles = 50000;
        public static int ps_max_systems = 200;
        public static int ps_max_emission = 5000;
        public static int ps_max_total_emission = 40000;
        public static int ps_mesh_particle_divider = 50;
        public static int ps_mesh_particle_poly_limit = 50000;
        public static int ps_collision_penalty_high = 120;
        public static int ps_collision_penalty_med = 60;
        public static int ps_collision_penalty_low = 10;
        public static int ps_trails_penalty = 10;
        public static int ps_max_particle_force = 0; // can not be disabled

        private static HashSet<System.Type> GetWhitelistForSDK(GameObject avatar)
        {
            VRC.SDKBase.VRC_AvatarDescriptor descriptor = avatar.GetComponent<VRC.SDKBase.VRC_AvatarDescriptor>();

            #if VRC_SDK_VRCSDK2
            if(descriptor is VRCSDK2.VRC_AvatarDescriptor)
            {
                if(CombinedComponentTypeWhiteListSdk2 == null)
                {
                    List<string> concatenation = new List<string>(ComponentTypeWhiteListCommon);
                    concatenation.AddRange(ComponentTypeWhiteListSdk2);
                    CombinedComponentTypeWhiteListSdk2 = concatenation.ToArray();
                }

                return ValidationUtils.WhitelistedTypes("avatar-sdk2", CombinedComponentTypeWhiteListSdk2);
            }
            #endif
            #if VRC_SDK_VRCSDK3
            if(descriptor is VRC.SDK3.Avatars.Components.VRCAvatarDescriptor)
            {
                if(CombinedComponentTypeWhiteListSdk3 == null)
                {
                    List<string> concatenation = new List<string>(ComponentTypeWhiteListCommon);
                    concatenation.AddRange(ComponentTypeWhiteListSdk3);
                    CombinedComponentTypeWhiteListSdk3 = concatenation.ToArray();
                }

                return ValidationUtils.WhitelistedTypes("avatar-sdk3", CombinedComponentTypeWhiteListSdk3);
            }
            #endif
            //throw new System.Exception("Malformed avatar");
            // instead of exception, log error, and return empty whitelist
            Debug.LogError("Malformed avatar");
            return new HashSet<System.Type>();
        }

        public static void RemoveIllegalComponents(GameObject target, bool retry = true)
        {
            ValidationUtils.RemoveIllegalComponents(target, GetWhitelistForSDK(target), retry);
        }

        public static IEnumerable<Component> FindIllegalComponents(GameObject target)
        {
            return ValidationUtils.FindIllegalComponents(target, GetWhitelistForSDK(target));
        }

        private static ProfilerMarker _enforceAudioSourceLimitsProfilerMarker = new ProfilerMarker("AvatarValidation.EnforceAudioSourceLimits");

        public static void EnforceAudioSourceLimits(GameObject currentAvatar)
        {
            using(_enforceAudioSourceLimitsProfilerMarker.Auto())
            {
                if(currentAvatar == null)
                {
                    return;
                }

                Queue<GameObject> children = new Queue<GameObject>();
                if(currentAvatar != null)
                {
                    children.Enqueue(currentAvatar.gameObject);
                }

                while(children.Count > 0)
                {
                    GameObject child = children.Dequeue();
                    if(child == null)
                    {
                        continue;
                    }

                    int childCount = child.transform.childCount;
                    for(int idx = 0; idx < childCount; ++idx)
                    {
                        children.Enqueue(child.transform.GetChild(idx).gameObject);
                    }

                    #if VRC_CLIENT
                    if(child.GetComponent<USpeaker>() != null)
                    {
                        continue;
                    }
                    #endif

                    AudioSource[] sources = child.transform.GetComponents<AudioSource>();
                    if(sources == null || sources.Length <= 0)
                    {
                        continue;
                    }

                    AudioSource audioSource = sources[0];
                    if(audioSource == null)
                    {
                        continue;
                    }


                    #if VRC_CLIENT
                    audioSource.outputAudioMixerGroup = VRCAudioManager.GetAvatarGroup();
                    audioSource.priority = Mathf.Clamp(audioSource.priority, 200, 255);
                    #else
                    ProcessSpatialAudioSources(audioSource);
                    #endif //!VRC_CLIENT

                    if(sources.Length <= 1)
                    {
                        continue;
                    }

                    Debug.LogError("Disabling extra AudioSources on GameObject(" + child.name + "). Only one is allowed per GameObject.");
                    for(int i = 1; i < sources.Length; i++)
                    {
                        if(sources[i] == null)
                        {
                            Profiler.EndSample();
                            continue;
                        }

                        #if VRC_CLIENT
                        sources[i].enabled = false;
                        sources[i].clip = null;
                        #else
                        ValidationUtils.RemoveComponent(sources[i]);
                        #endif //!VRC_CLIENT
                    }
                }
            }
        }

        public static void EnforceClothLimits(GameObject avatarGameObject)
        {
            const int clothMaxSolverFrequency = 240;
            foreach(Cloth cloth in avatarGameObject.GetComponentsInChildren<Cloth>(true))
            {
                if(cloth.clothSolverFrequency > clothMaxSolverFrequency)
                {
                    cloth.clothSolverFrequency = clothMaxSolverFrequency;
                }
            }
        }

        #if VRC_CLIENT
        public static void EnforceAimIKLimits(GameObject avatarGameObject)
        {
            const int aimIKMaxSolverFrequency = 64;
            foreach(RootMotion.FinalIK.AimIK aimIK in avatarGameObject.GetComponentsInChildren<RootMotion.FinalIK.AimIK>(true))
            {
                if(aimIK.solver.maxIterations > aimIKMaxSolverFrequency)
                {
                    aimIK.solver.maxIterations = aimIKMaxSolverFrequency;
                }
            }
        }
        #endif

        #if !VRC_CLIENT
        private static void ProcessSpatialAudioSources(AudioSource audioSource)
        {
            #if VRC_SDK_VRCSDK2
            VRC_SpatialAudioSource vrcSpatialAudioSource2 = audioSource.gameObject.GetComponent<VRC_SpatialAudioSource>();
            if (vrcSpatialAudioSource2 == null)
            {
                // user has not yet added VRC_SpatialAudioSource (or ONSP)
                // so set up some defaults
                vrcSpatialAudioSource2 = audioSource.gameObject.AddComponent<VRC_SpatialAudioSource>();
                vrcSpatialAudioSource2.Gain = AudioManagerSettings.AvatarAudioMaxGain;
                vrcSpatialAudioSource2.Far = AudioManagerSettings.AvatarAudioMaxRange;
                vrcSpatialAudioSource2.Near = 0f;
                vrcSpatialAudioSource2.VolumetricRadius = 0f;
                vrcSpatialAudioSource2.EnableSpatialization = true;
                vrcSpatialAudioSource2.enabled = true;
                audioSource.spatialize = true;
                audioSource.priority = Mathf.Clamp(audioSource.priority, 200, 255);
                audioSource.bypassEffects = false;
                audioSource.bypassListenerEffects = false;
                audioSource.spatialBlend = 1f;
                audioSource.spread = 0;

                // user is allowed to change, but for now put a safe default
                audioSource.maxDistance = AudioManagerSettings.AvatarAudioMaxRange;
                audioSource.minDistance = audioSource.maxDistance / 500f;
                audioSource.rolloffMode = AudioRolloffMode.Logarithmic;
            }
            #elif VRC_SDK_VRCSDK3
            VRC.SDK3.Avatars.Components.VRCSpatialAudioSource vrcSpatialAudioSource2 = audioSource.gameObject.GetComponent<VRC.SDK3.Avatars.Components.VRCSpatialAudioSource>();
            if (vrcSpatialAudioSource2 == null)
            {
                // user has not yet added VRC_SpatialAudioSource (or ONSP)
                // so set up some defaults
                vrcSpatialAudioSource2 = audioSource.gameObject.AddComponent<VRC.SDK3.Avatars.Components.VRCSpatialAudioSource>();
                vrcSpatialAudioSource2.Gain = AudioManagerSettings.AvatarAudioMaxGain;
                vrcSpatialAudioSource2.Far = AudioManagerSettings.AvatarAudioMaxRange;
                vrcSpatialAudioSource2.Near = 0f;
                vrcSpatialAudioSource2.VolumetricRadius = 0f;
                vrcSpatialAudioSource2.EnableSpatialization = true;
                vrcSpatialAudioSource2.enabled = true;
                audioSource.spatialize = true;
                audioSource.priority = Mathf.Clamp(audioSource.priority, 200, 255);
                audioSource.bypassEffects = false;
                audioSource.bypassListenerEffects = false;
                audioSource.spatialBlend = 1f;
                audioSource.spread = 0;

                // user is allowed to change, but for now put a safe default
                audioSource.maxDistance = AudioManagerSettings.AvatarAudioMaxRange;
                audioSource.minDistance = audioSource.maxDistance / 500f;
                audioSource.rolloffMode = AudioRolloffMode.Logarithmic;
            }
            #endif
        }
        #endif

        public static void EnforceRealtimeParticleSystemLimits(Dictionary<ParticleSystem, int> particleSystems, bool includeDisabled = false, bool stopSystems = true)
        {
            float totalEmission = 0;
            ParticleSystem ps = null;
            int max = 0;
            int em_penalty = 1;
            ParticleSystem.EmissionModule em;
            float emission = 0;
            ParticleSystem.Burst[] bursts;

            foreach(KeyValuePair<ParticleSystem, int> kp in particleSystems)
            {
                if(kp.Key == null)
                    continue;

                if(!kp.Key.isPlaying && !includeDisabled)
                    continue;

                ps = kp.Key;
                max = kp.Value;
                em_penalty = 1;
                if(ps.collision.enabled)
                {
                    // particle force is always restricted (not dependent on ps_limiter_enabled)
                    var restrictedCollision = ps.collision;
                    restrictedCollision.colliderForce = ps_max_particle_force;

                    if(ps_limiter_enabled)
                    {
                        switch(ps.collision.quality)
                        {
                            case ParticleSystemCollisionQuality.High:
                                max = max / ps_collision_penalty_high;
                                em_penalty += 3;
                                break;
                            case ParticleSystemCollisionQuality.Medium:
                                max = max / ps_collision_penalty_med;
                                em_penalty += 2;
                                break;
                            case ParticleSystemCollisionQuality.Low:
                                max = max / ps_collision_penalty_low;
                                em_penalty += 2;
                                break;
                        }
                    }
                }

                if(ps_limiter_enabled && ps.trails.enabled)
                {
                    max = max / ps_trails_penalty;
                    em_penalty += 3;
                }

                if(ps_limiter_enabled && ps.emission.enabled)
                {
                    em = ps.emission;
                    emission = 0;
                    emission += GetCurveMax(em.rateOverTime);
                    emission += GetCurveMax(em.rateOverDistance);

                    bursts = new ParticleSystem.Burst[em.burstCount];
                    em.GetBursts(bursts);
                    for(int i = 0; i < bursts.Length; i++)
                    {
                        float adjMax = bursts[i].repeatInterval > 1 ? bursts[i].maxCount : bursts[i].maxCount * bursts[i].repeatInterval;
                        if(adjMax > ps_max_emission)
                            bursts[i].maxCount = (short)Mathf.Clamp(adjMax, 0, ps_max_emission);
                    }

                    em.SetBursts(bursts);

                    emission *= em_penalty;
                    totalEmission += emission;
                    if((emission > ps_max_emission || totalEmission > ps_max_total_emission) && stopSystems)
                    {
                        kp.Key.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
                        // Debug.LogWarning("Particle system named " + kp.Key.gameObject.name + " breached particle emission limits, it has been stopped");
                    }
                }

                if(ps_limiter_enabled && ps.main.maxParticles > Mathf.Clamp(max, 1, kp.Value))
                {
                    ParticleSystem.MainModule psm = ps.main;
                    psm.maxParticles = Mathf.Clamp(psm.maxParticles, 1, max);
                    if(stopSystems)
                        kp.Key.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);

                    Debug.LogWarning("Particle system named " + kp.Key.gameObject.name + " breached particle limits, it has been limited");
                }
            }
        }

        private static ProfilerMarker _enforceAvatarStationLimitsProfilerMarker = new ProfilerMarker("AvatarValidation.EnforceAudioSourceLimits");

        public static void EnforceAvatarStationLimits(GameObject currentAvatar)
        {
            using(_enforceAvatarStationLimitsProfilerMarker.Auto())
            {
                int stationCount = 0;
                foreach(VRC.SDKBase.VRCStation station in currentAvatar.gameObject.GetComponentsInChildren<VRC.SDKBase.VRCStation>(true))
                {
                    if(station == null)
                    {
                        continue;
                    }

                    #if VRC_CLIENT
                    VRC_StationInternal stationInternal = station.transform.GetComponent<VRC_StationInternal>();
                    #endif
                    if(stationCount < MAX_STATIONS_PER_AVATAR)
                    {
                        #if VRC_CLIENT
                        bool markedForDestruction = false;
                        #endif
                        // keep this station, but limit it
                        if(station.disableStationExit)
                        {
                            Debug.LogError("[" + currentAvatar.name + "]==> Stations on avatars cannot disable station exit. Re-enabled.");
                            station.disableStationExit = false;
                        }

                        if(station.stationEnterPlayerLocation != null)
                        {
                            if(Vector3.Distance(station.stationEnterPlayerLocation.position, station.transform.position) > MAX_STATION_LOCATION_DISTANCE)
                            {
                                #if VRC_CLIENT
                                markedForDestruction = true;
                                Debug.LogError(
                                    "[" + currentAvatar.name + "]==> Station enter location is too far from station (max dist=" + MAX_STATION_LOCATION_DISTANCE +
                                    "). Station disabled.");
                                #else
                                Debug.LogError("Station enter location is too far from station (max dist="+MAX_STATION_LOCATION_DISTANCE+"). Station will be disabled at runtime.");
                                #endif
                            }

                            if(Vector3.Distance(station.stationExitPlayerLocation.position, station.transform.position) > MAX_STATION_LOCATION_DISTANCE)
                            {
                                #if VRC_CLIENT
                                markedForDestruction = true;
                                Debug.LogError(
                                    "[" + currentAvatar.name + "]==> Station exit location is too far from station (max dist=" + MAX_STATION_LOCATION_DISTANCE +
                                    "). Station disabled.");
                                #else
                                Debug.LogError("Station exit location is too far from station (max dist="+MAX_STATION_LOCATION_DISTANCE+"). Station will be disabled at runtime.");
                                #endif
                            }

                            #if VRC_CLIENT
                            if(markedForDestruction)
                            {
                                ValidationUtils.RemoveComponent(station);
                                if(stationInternal != null)
                                {
                                    ValidationUtils.RemoveComponent(stationInternal);
                                }
                            }
                            #endif
                        }
                    }
                    else
                    {
                        #if VRC_CLIENT
                        Debug.LogError("[" + currentAvatar.name + "]==> Removing station over limit of " + MAX_STATIONS_PER_AVATAR);
                        ValidationUtils.RemoveComponent(station);
                        if(stationInternal != null)
                        {
                            ValidationUtils.RemoveComponent(stationInternal);
                        }
                        #else
                        Debug.LogError("Too many stations on avatar("+ currentAvatar.name +"). Maximum allowed="+MAX_STATIONS_PER_AVATAR+". Extra stations will be removed at runtime.");
                        #endif
                    }

                    stationCount++;
                }
            }
        }

        public static void RemoveCameras(GameObject currentAvatar, bool localPlayer, bool friend)
        {
            if(!localPlayer && currentAvatar != null)
            {
                foreach(Camera camera in currentAvatar.GetComponentsInChildren<Camera>(true))
                {
                    if(camera == null || camera.gameObject == null)
                        continue;

                    Debug.LogWarning("Removing camera from " + camera.gameObject.name);

                    if(friend && camera.targetTexture != null)
                    {
                        camera.enabled = false;
                    }
                    else
                    {
                        camera.enabled = false;
                        if(camera.targetTexture != null)
                            camera.targetTexture = new RenderTexture(16, 16, 24);

                        ValidationUtils.RemoveComponent(camera);
                    }
                }
            }
        }

        public static void StripAnimations(GameObject currentAvatar)
        {
            foreach(Animator anim in currentAvatar.GetComponentsInChildren<Animator>(true))
            {
                if(anim == null)
                    continue;

                StripRuntimeAnimatorController(anim.runtimeAnimatorController);
            }

            foreach(VRC.SDKBase.VRCStation station in currentAvatar.GetComponentsInChildren<VRC.SDKBase.VRCStation>(true))
            {
                if(station == null)
                    continue;

                StripRuntimeAnimatorController(station.animatorController);
            }
            #if VRC_SDK_VRCSDK3
            // also strip any controllers inside the av3 descriptor
            var desc3 = currentAvatar.GetComponent<VRC.SDK3.Avatars.Components.VRCAvatarDescriptor>();
            if(desc3 != null)
            {
                foreach(var layer in desc3.baseAnimationLayers)
                    StripRuntimeAnimatorController(layer.animatorController);

                foreach(var layer in desc3.specialAnimationLayers)
                    StripRuntimeAnimatorController(layer.animatorController);
            }
            #endif
        }

        private static void StripRuntimeAnimatorController(RuntimeAnimatorController rc)
        {
            if(rc == null || rc.animationClips == null)
                return;

            foreach(AnimationClip clip in rc.animationClips)
            {
                if(clip == null)
                    continue;

                if(clip.events != null && clip.events.Length > 0)
                    Debug.LogWarning("Removing animation events found on " + clip.name + " on animcontroller " + rc.name);

                clip.events = null;
            }
        }

        public static void RemoveExtraAnimationComponents(GameObject currentAvatar)
        {
            if(currentAvatar == null)
                return;

            // remove Animator comps
            {
                Animator mainAnimator = currentAvatar.GetComponent<Animator>();
                bool removeMainAnimator = false;
                if(mainAnimator != null)
                {
                    if(!mainAnimator.isHuman || mainAnimator.avatar == null || !mainAnimator.avatar.isValid)
                    {
                        removeMainAnimator = true;
                    }
                }

                foreach(Animator anim in currentAvatar.GetComponentsInChildren<Animator>(true))
                {
                    if(anim == null || anim.gameObject == null)
                        continue;

                    // exclude the main avatar animator
                    if(anim == mainAnimator)
                    {
                        if(!removeMainAnimator)
                        {
                            continue;
                        }
                    }

                    Debug.LogWarning("Removing Animator comp from " + anim.gameObject.name);

                    anim.enabled = false;
                    ValidationUtils.RemoveComponent(anim);
                }
            }

            ValidationUtils.RemoveComponentsOfType<UnityEngine.Animation>(currentAvatar);
        }

        private static Color32 GetTrustLevelColor(VRC.Core.APIUser user)
        {
            #if VRC_CLIENT
            Color32 color = new Color32(255, 255, 255, 255);
            if(user == null)
            {
                return color;
            }

            color = VRCPlayer.GetDisplayColorForSocialRank(user);
            return color;
            #else
            // we are in sdk, this is not meaningful anyway
            return (Color32)Color.grey;
            #endif
        }

        private static Material CreateFallbackMaterial(Material originalMaterial, VRC.Core.APIUser user)
        {
            #if VRC_CLIENT
            Material fallbackMaterial;
            Color trustCol = user != null ? (Color)GetTrustLevelColor(user) : Color.white;
            string displayName = user != null ? user.displayName : "localUser";

            if(originalMaterial == null || originalMaterial.shader == null)
            {
                fallbackMaterial = VRC.Core.AssetManagement.CreateMatCap(trustCol * 0.8f + new Color(0.2f, 0.2f, 0.2f));
                fallbackMaterial.name = string.Format("MC_{0}_{1}", fallbackMaterial.shader.name, displayName);
            }
            else
            {
                var safeShader = VRC.Core.AssetManagement.GetSafeShader(originalMaterial.shader.name);
                if(safeShader == null)
                {
                    fallbackMaterial = VRC.Core.AssetManagement.CreateSafeFallbackMaterial(originalMaterial, trustCol * 0.8f + new Color(0.2f, 0.2f, 0.2f));
                    fallbackMaterial.name = string.Format("FB_{0}_{1}_{2}", fallbackMaterial.shader.name, displayName, originalMaterial.name);
                }
                else
                {
                    //Debug.Log("<color=cyan>*** using safe internal fallback for shader:"+ safeShader.name + "</color>");
                    fallbackMaterial = new Material(safeShader);
                    if(safeShader.name == "Standard" || safeShader.name == "Standard (Specular setup)")
                    {
                        VRC.Core.AssetManagement.SetupBlendMode(fallbackMaterial);
                    }

                    fallbackMaterial.CopyPropertiesFromMaterial(originalMaterial);
                    fallbackMaterial.name = string.Format("INT_{0}_{1}_{2}", fallbackMaterial.shader.name, displayName, originalMaterial.name);
                }
            }

            return fallbackMaterial;
            #else
            // we are in sdk, this is not meaningful anyway
            return new Material(Shader.Find("Standard"));
            #endif
        }

        public static void BuildAvatarRenderersList(GameObject currentAvatar, List<Renderer> avatarRenderers)
        {
            currentAvatar.GetComponentsInChildren(true, avatarRenderers);
        }

        // TCL's method of allocation avoidance
        private static readonly List<Material> _replaceShadersWorkingList = new List<Material>();

        public static void ReplaceShaders(VRC.Core.APIUser user, List<Renderer> avatarRenderers, FallbackMaterialCache fallbackMaterialCache, bool debug = false)
        {
            foreach(Renderer avatarRenderer in avatarRenderers)
            {
                if(avatarRenderer == null)
                {
                    continue;
                }

                avatarRenderer.GetSharedMaterials(_replaceShadersWorkingList);
                bool anyReplaced = false;
                for(int i = 0; i < _replaceShadersWorkingList.Count; ++i)
                {
                    Material currentMaterial = _replaceShadersWorkingList[i];
                    if(currentMaterial == null)
                    {
                        continue;
                    }

                    // Check if the material has a cached fallback material if not then create a new one.
                    if(!fallbackMaterialCache.TryGetFallbackMaterial(currentMaterial, out Material fallbackMaterial))
                    {
                        fallbackMaterial = CreateFallbackMaterial(currentMaterial, user);

                        // Map the current material to the fallback and the fallback to itself.
                        fallbackMaterialCache.AddFallbackMaterial(currentMaterial, fallbackMaterial);
                        fallbackMaterialCache.AddFallbackMaterial(fallbackMaterial, fallbackMaterial);

                        if(debug)
                        {
                            Debug.Log($"<color=cyan>*** Creating new fallback: '{fallbackMaterial.shader.name}' </color>");
                        }

                        if(fallbackMaterial == currentMaterial)
                        {
                            continue;
                        }

                        _replaceShadersWorkingList[i] = fallbackMaterial;
                        anyReplaced = true;
                        continue;
                    }

                    // If the material is the fallback then we don't need to change it.
                    if(currentMaterial == fallbackMaterial)
                    {
                        continue;
                    }

                    if(debug)
                    {
                        Debug.Log($"<color=cyan>*** Using existing fallback: '{fallbackMaterial.shader.name}' </color>");
                    }

                    _replaceShadersWorkingList[i] = fallbackMaterial;
                    anyReplaced = true;
                }

                if(anyReplaced)
                {
                    avatarRenderer.sharedMaterials = _replaceShadersWorkingList.ToArray();
                }
            }
        }

        public static void ReplaceShadersRealtime(VRC.Core.APIUser user, List<Renderer> avatarRenderers, FallbackMaterialCache fallbackMaterialCache, bool debug = false)
        {
            ReplaceShaders(user, avatarRenderers, fallbackMaterialCache, debug);
        }

        public static void SetupParticleLimits()
        {
            ps_limiter_enabled = VRC.Core.ConfigManager.RemoteConfig.GetBool("ps_limiter_enabled", ps_limiter_enabled);
            ps_max_particles = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_max_particles", ps_max_particles);
            ps_max_systems = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_max_systems", ps_max_systems);
            ps_max_emission = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_max_emission", ps_max_emission);
            ps_max_total_emission = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_max_total_emission", ps_max_total_emission);
            ps_mesh_particle_divider = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_mesh_particle_divider", ps_mesh_particle_divider);
            ps_mesh_particle_poly_limit = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_mesh_particle_poly_limit", ps_mesh_particle_poly_limit);
            ps_collision_penalty_high = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_collision_penalty_high", ps_collision_penalty_high);
            ps_collision_penalty_med = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_collision_penalty_med", ps_collision_penalty_med);
            ps_collision_penalty_low = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_collision_penalty_low", ps_collision_penalty_low);
            ps_trails_penalty = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_trails_penalty", ps_trails_penalty);

            if(Application.isMobilePlatform)
            {
                ps_limiter_enabled = true;
            }
            else
            {
                ps_limiter_enabled = VRC.Core.ConfigManager.LocalConfig.GetList("betas").Contains("particle_system_limiter") || ps_limiter_enabled;
                ps_max_particles = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_max_particles", ps_max_particles);
                ps_max_systems = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_max_systems", ps_max_systems);
                ps_max_emission = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_max_emission", ps_max_emission);
                ps_max_total_emission = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_max_total_emission", ps_max_total_emission);
                ps_mesh_particle_divider = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_mesh_particle_divider", ps_mesh_particle_divider);
                ps_mesh_particle_poly_limit = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_mesh_particle_poly_limit", ps_mesh_particle_poly_limit);
                ps_collision_penalty_high = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_collision_penalty_high", ps_collision_penalty_high);
                ps_collision_penalty_med = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_collision_penalty_med", ps_collision_penalty_med);
                ps_collision_penalty_low = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_collision_penalty_low", ps_collision_penalty_low);
                ps_trails_penalty = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_trails_penalty", ps_trails_penalty);
            }
        }

        public static Dictionary<ParticleSystem, int> EnforceParticleSystemLimits(GameObject currentAvatar)
        {
            Dictionary<ParticleSystem, int> particleSystems = new Dictionary<ParticleSystem, int>();

            foreach(ParticleSystem ps in currentAvatar.transform.GetComponentsInChildren<ParticleSystem>(true))
            {
                int realtime_max = ps_max_particles;

                // always limit collision force
                var collision = ps.collision;
                if(collision.colliderForce > ps_max_particle_force)
                {
                    collision.colliderForce = ps_max_particle_force;
                    Debug.LogError("Collision force is restricted on avatars, particle system named " + ps.gameObject.name + " collision force restricted to " + ps_max_particle_force);
                }

                if(ps_limiter_enabled)
                {
                    if(particleSystems.Count > ps_max_systems)
                    {
                        Debug.LogError("Too many particle systems, #" + particleSystems.Count + " named " + ps.gameObject.name + " deleted");
                        ValidationUtils.RemoveComponent(ps);
                        continue;
                    }
                    else
                    {
                        var main = ps.main;
                        var emission = ps.emission;

                        ParticleSystemRenderer renderer = ps.GetComponent<ParticleSystemRenderer>();
                        if(renderer != null)
                        {
                            if(renderer.renderMode == ParticleSystemRenderMode.Mesh)
                            {
                                Mesh[] meshes = new Mesh[0];
                                int highestPoly = 0;
                                renderer.GetMeshes(meshes);
                                if(meshes.Length == 0 && renderer.mesh != null)
                                {
                                    meshes = new Mesh[] {renderer.mesh};
                                }

                                // Debug.Log(meshes.Length + " meshes possible emmited meshes from " + ps.gameObject.name);
                                foreach(Mesh m in meshes)
                                {
                                    if(m.isReadable)
                                    {
                                        if(m.triangles.Length / 3 > highestPoly)
                                        {
                                            highestPoly = m.triangles.Length / 3;
                                        }
                                    }
                                    else
                                    {
                                        if(1000 > highestPoly)
                                        {
                                            highestPoly = int.MaxValue;
                                        }
                                    }
                                }

                                if(highestPoly > 0)
                                {
                                    highestPoly = Mathf.Clamp(highestPoly / ps_mesh_particle_divider, 1, highestPoly);
                                    realtime_max = Mathf.FloorToInt((float)realtime_max / highestPoly);

                                    if(highestPoly > ps_mesh_particle_poly_limit)
                                    {
                                        Debug.LogError("Particle system named " + ps.gameObject.name + " breached polygon limits, it has been deleted");
                                        ValidationUtils.RemoveComponent(ps);
                                        continue;
                                    }
                                }
                            }
                        }


                        ParticleSystem.MinMaxCurve rate = emission.rateOverTime;

                        if(rate.mode == ParticleSystemCurveMode.Constant)
                        {
                            rate.constant = Mathf.Clamp(rate.constant, 0, ps_max_emission);
                        }
                        else if(rate.mode == ParticleSystemCurveMode.TwoConstants)
                        {
                            rate.constantMax = Mathf.Clamp(rate.constantMax, 0, ps_max_emission);
                        }
                        else
                        {
                            rate.curveMultiplier = Mathf.Clamp(rate.curveMultiplier, 0, ps_max_emission);
                        }

                        emission.rateOverTime = rate;
                        rate = emission.rateOverDistance;

                        if(rate.mode == ParticleSystemCurveMode.Constant)
                        {
                            rate.constant = Mathf.Clamp(rate.constant, 0, ps_max_emission);
                        }
                        else if(rate.mode == ParticleSystemCurveMode.TwoConstants)
                        {
                            rate.constantMax = Mathf.Clamp(rate.constantMax, 0, ps_max_emission);
                        }
                        else
                        {
                            rate.curveMultiplier = Mathf.Clamp(rate.curveMultiplier, 0, ps_max_emission);
                        }

                        emission.rateOverDistance = rate;

                        //Disable collision with PlayerLocal layer
                        collision.collidesWith &= ~(1 << 10);
                    }
                }

                particleSystems.Add(ps, realtime_max);
            }

            EnforceRealtimeParticleSystemLimits(particleSystems, true, false);

            return particleSystems;
        }

        public static bool ClearLegacyAnimations(GameObject currentAvatar)
        {
            bool hasLegacyAnims = false;
            foreach(var ani in currentAvatar.GetComponentsInChildren<UnityEngine.Animation>(true))
            {
                if(ani.clip != null)
                    if(ani.clip.legacy)
                    {
                        Debug.LogWarningFormat("Legacy animation found named '{0}' on '{1}', removing", ani.clip.name, ani.gameObject.name);
                        ani.clip = null;
                        hasLegacyAnims = true;
                    }

                foreach(AnimationState anistate in ani)
                    if(anistate.clip.legacy)
                    {
                        Debug.LogWarningFormat("Legacy animation found named '{0}' on '{1}', removing", anistate.clip.name, ani.gameObject.name);
                        ani.RemoveClip(anistate.clip);
                        hasLegacyAnims = true;
                    }
            }

            return hasLegacyAnims;
        }

        private static float GetCurveMax(ParticleSystem.MinMaxCurve minMaxCurve)
        {
            switch(minMaxCurve.mode)
            {
                case ParticleSystemCurveMode.Constant:
                    return minMaxCurve.constant;
                case ParticleSystemCurveMode.TwoConstants:
                    return minMaxCurve.constantMax;
                default:
                    return minMaxCurve.curveMultiplier;
            }
        }

        public static bool AreAnyParticleSystemsPlaying(Dictionary<ParticleSystem, int> particleSystems)
        {
            foreach(KeyValuePair<ParticleSystem, int> kp in particleSystems)
            {
                if(kp.Key != null && kp.Key.isPlaying)
                    return true;
            }

            return false;
        }

        public static void StopAllParticleSystems(Dictionary<ParticleSystem, int> particleSystems)
        {
            foreach(KeyValuePair<ParticleSystem, int> kp in particleSystems)
            {
                if(kp.Key != null && kp.Key.isPlaying)
                {
                    kp.Key.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
                }
            }
        }

        public static IEnumerable<Shader> FindIllegalShaders(GameObject target)
        {
            return ShaderValidation.FindIllegalShaders(target, ShaderWhiteList);
        }

        /// <summary>
        /// NOTE: intended to be called from 'VRCAvatarManager.SafetyCheckAndComponentScan'
        /// but temporarily disabled (until we enable texture streaming)
        /// </summary>
        public static void ReportTexturesWithoutMipMapStreaming(VRC.Core.ApiAvatar avatar, GameObject target)
        {
            var badTextures = new List<Texture2D>();
            foreach(Renderer r in target.GetComponentsInChildren<Renderer>())
            {
                foreach(Material m in r.sharedMaterials)
                {
                    foreach(int i in m.GetTexturePropertyNameIDs())
                    {
                        Texture2D t = m.GetTexture(i) as Texture2D;
                        if(!t)
                            continue;

                        if((t.mipmapCount > 0) && !t.streamingMipmaps)
                            badTextures.Add(t);
                    }
                }
            }

            if(badTextures.Count > 0)
            {
                string warning = "[" + avatar.name + "]==> One or more avatar textures have non-streaming mipmaps: ";
                foreach(Texture2D t in badTextures)
                {
                    warning += "'" + t.name + "', ";
                }

                warning = warning.Remove(warning.LastIndexOf(",", StringComparison.Ordinal));
                Debug.LogWarning(warning + ".");
            }
        }

        public static void ConvertDynamicBoneToAvatarDynamics(GameObject avatarGameObject)
        {
#if VRC_CLIENT
            AvatarDynamicsPlayerSetup.ConvertDynamicBonesToPhysBones(avatarGameObject);
#endif
        }
    }
}
