using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

[AddComponentMenu("Camera/Camera Follow")]
public class CameraFollow : MonoBehaviour
{
    public Transform target;
    public Vector3 offset = new Vector3(0f, 8f, -8f);
    public float smoothTime = 0.12f;
    public bool keepFixedRotation = true;
    public Vector3 fixedEulerRotation = new Vector3(30f, 0f, 0f);
    Vector3 velocity;

    [Header("Occluder Settings")]
    public LayerMask occluderLayers = ~0;
    [Range(0f, 1f)]
    public float occluderAlpha = 0.25f;
    public bool debugOccluders = false;
    public Material fallbackTransparentMaterial;
    public bool forceFallback = false;
    private Dictionary<Renderer, Material[]> originalMaterials = new Dictionary<Renderer, Material[]>();
    private Dictionary<Renderer, ShadowCastingMode> originalShadowCasting = new Dictionary<Renderer, ShadowCastingMode>();
    private Dictionary<Renderer, bool> originalReceiveShadows = new Dictionary<Renderer, bool>();
    private List<Renderer> currentlyOccluding = new List<Renderer>();
    private Material runtimeFallback;

    void LateUpdate()
    {
        if (target == null) return;
        Vector3 desiredPos = target.position + offset;
        transform.position = Vector3.SmoothDamp(transform.position, desiredPos, ref velocity, smoothTime);

        if (keepFixedRotation)
        {
            transform.rotation = Quaternion.Euler(fixedEulerRotation);
        }

        UpdateOccluders();
    }

    private void UpdateOccluders()
    {
        currentlyOccluding.Clear();

        Vector3 camPos = transform.position;
        Vector3 targetPos = target.position;
        Vector3 dir = (targetPos - camPos);
        float dist = dir.magnitude;
        if (dist <= 0.001f) return;
        dir /= dist;

        RaycastHit[] hits = Physics.RaycastAll(camPos, dir, dist, occluderLayers);
        foreach (var hit in hits)
        {
            if (hit.collider == null) continue;
            if (hit.collider.transform.IsChildOf(target)) continue;
            Renderer r = hit.collider.GetComponent<Renderer>();
            if (r == null) r = hit.collider.GetComponentInParent<Renderer>();
            if (r == null) r = hit.collider.GetComponentInChildren<Renderer>();
            if (r == null) continue;
            if (debugOccluders) Debug.Log($"Occluder hit: {r.gameObject.name} at distance {hit.distance}");
            if (r.transform.IsChildOf(transform)) continue;
            if (!currentlyOccluding.Contains(r)) currentlyOccluding.Add(r);
        }

        var keys = new List<Renderer>(originalMaterials.Keys);
        foreach (var r in keys)
        {
            if (!currentlyOccluding.Contains(r))
            {
                RestoreRendererMaterials(r);
            }
        }

        foreach (var r in currentlyOccluding)
        {
            if (!originalMaterials.ContainsKey(r)) MakeRendererTransparent(r, occluderAlpha);
        }
    }

    private void MakeRendererTransparent(Renderer r, float alpha)
    {
        if (r == null) return;
        try
        {
            Material[] shared = r.sharedMaterials;
            if (shared == null || shared.Length == 0) return;

            originalMaterials[r] = shared;
            originalShadowCasting[r] = r.shadowCastingMode;
            originalReceiveShadows[r] = r.receiveShadows;

            Material[] mats = r.materials;
            for (int i = 0; i < mats.Length; i++)
            {
                var m = mats[i];
                if (m == null)
                {
                    mats[i] = null;
                    continue;
                }
                var src = shared.Length > i ? shared[i] : null;

                bool setAlpha = false;
                string[] colorProps = new string[] { "_BaseColor", "_Color", "_TintColor", "_MainColor" };
                foreach (var prop in colorProps)
                {
                    if (m.HasProperty(prop))
                    {
                        try
                        {
                            Color c = m.GetColor(prop);
                            c.a = alpha;
                            m.SetColor(prop, c);
                            setAlpha = true;
                            break;
                        }
                        catch { }
                    }
                }

                if (m.HasProperty("_Surface"))
                {
                    try
                    {
                        m.SetFloat("_Surface", 1f);
                        if (m.HasProperty("_ZWrite")) m.SetFloat("_ZWrite", 0f);
                        m.renderQueue = 3000;
                        setAlpha = true;
                    }
                    catch { }
                }

                if (!setAlpha && m.HasProperty("_Mode"))
                {
                    m.SetFloat("_Mode", 3);
                    m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
                    m.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
                    m.SetInt("_ZWrite", 0);
                    m.DisableKeyword("_ALPHATEST_ON");
                    m.EnableKeyword("_ALPHABLEND_ON");
                    m.DisableKeyword("_ALPHAPREMULTIPLY_ON");
                    m.renderQueue = 3000;
                    setAlpha = true;
                }

                if (!setAlpha && m.HasProperty("_Color"))
                {
                    try
                    {
                        Color c = m.GetColor("_Color");
                        c.a = alpha;
                        m.SetColor("_Color", c);
                    }
                    catch { }
                }

                bool alphaApplied = false;
                string[] checkProps = new string[] { "_BaseColor", "_Color", "_TintColor", "_MainColor", "_Surface", "_Mode" };
                foreach (var p in checkProps)
                {
                    if (m.HasProperty(p))
                    {
                        alphaApplied = true;
                        break;
                    }
                }

                if (!alphaApplied || forceFallback)
                {
                    Material fallback = GetOrCreateFallbackTransparent(src != null ? src : m, alpha);
                    mats[i] = fallback;
                }
                else
                {
                    ApplyTransparentSettings(m, alpha);
                    mats[i] = m;
                }
            }

            r.materials = mats;
            try
            {
                var mpb = new MaterialPropertyBlock();
                Material mat0 = mats.Length > 0 ? mats[0] : null;
                if (mat0 != null)
                {
                    if (mat0.HasProperty("_BaseColor"))
                    {
                        Color c = mat0.GetColor("_BaseColor"); c.a = alpha; mpb.SetColor("_BaseColor", c);
                    }
                    else if (mat0.HasProperty("_Color"))
                    {
                        Color c = mat0.GetColor("_Color"); c.a = alpha; mpb.SetColor("_Color", c);
                    }
                    if (mat0.HasProperty("_Surface")) mpb.SetFloat("_Surface", 1f);
                    if (mat0.HasProperty("_Mode")) mpb.SetFloat("_Mode", 3f);
                }
                r.SetPropertyBlock(mpb);
            }
            catch { }
            try
            {
                r.shadowCastingMode = ShadowCastingMode.Off;
                r.receiveShadows = false;
            }
            catch { }
        }
        catch (System.Exception)
        {
            
        }
    }

    private void RestoreRendererMaterials(Renderer r)
    {
        if (r == null) return;
        if (!originalMaterials.ContainsKey(r)) return;

        try
        {
            r.sharedMaterials = originalMaterials[r];
        }
        catch (System.Exception)
        {
        
        }
        try
        {
            if (originalShadowCasting.ContainsKey(r)) r.shadowCastingMode = originalShadowCasting[r];
            if (originalReceiveShadows.ContainsKey(r)) r.receiveShadows = originalReceiveShadows[r];
        }
        catch { }
        try
        {
            r.SetPropertyBlock(null);
        }
        catch { }
        originalMaterials.Remove(r);
        if (originalShadowCasting.ContainsKey(r)) originalShadowCasting.Remove(r);
        if (originalReceiveShadows.ContainsKey(r)) originalReceiveShadows.Remove(r);
    }

    private void OnDisable()
    {
        var keys = new List<Renderer>(originalMaterials.Keys);
        foreach (var r in keys)
            RestoreRendererMaterials(r);
        if (runtimeFallback != null)
        {
            DestroyImmediate(runtimeFallback);
            runtimeFallback = null;
        }
    }

    private Material GetOrCreateFallbackTransparent(Material src, float alpha)
    {
        if (fallbackTransparentMaterial != null)
        {
            Material m = new Material(fallbackTransparentMaterial);
            try
            {
                if (src.HasProperty("_MainTex") && m.HasProperty("_MainTex"))
                {
                    Texture t = src.GetTexture("_MainTex");
                    Texture mod = TryCreateAlphaModifiedTexture(t, alpha);
                    m.SetTexture("_MainTex", mod != null ? mod : t);
                }
                if (src.HasProperty("_Color") && m.HasProperty("_Color"))
                {
                    Color c = src.GetColor("_Color");
                    c.a = alpha;
                    m.SetColor("_Color", c);
                }
                if (src.HasProperty("_BaseMap") && m.HasProperty("_BaseMap"))
                {
                    Texture t = src.GetTexture("_BaseMap");
                    Texture mod = TryCreateAlphaModifiedTexture(t, alpha);
                    m.SetTexture("_BaseMap", mod != null ? mod : t);
                }
                if (src.HasProperty("_BaseColor") && m.HasProperty("_BaseColor"))
                {
                    Color bc = src.GetColor("_BaseColor");
                    bc.a = alpha;
                    m.SetColor("_BaseColor", bc);
                }
            }
            catch { }
            return m;
        }

        if (runtimeFallback == null)
        {
            Shader shader = Shader.Find("Universal Render Pipeline/Unlit");
            if (shader == null) shader = Shader.Find("Universal Render Pipeline/Lit");
            if (shader == null) shader = Shader.Find("Standard");
            runtimeFallback = new Material(shader != null ? shader : Shader.Find("Standard"));
            if (runtimeFallback.HasProperty("_Surface")) runtimeFallback.SetFloat("_Surface", 1f);
            if (runtimeFallback.HasProperty("_Mode")) runtimeFallback.SetFloat("_Mode", 3f);
            runtimeFallback.renderQueue = 3000;
        }
        Material inst = new Material(runtimeFallback);
        try
        {
            if (src.HasProperty("_MainTex") && inst.HasProperty("_MainTex"))
            {
                Texture t = src.GetTexture("_MainTex");
                Texture mod = TryCreateAlphaModifiedTexture(t, alpha);
                inst.SetTexture("_MainTex", mod != null ? mod : t);
            }
            if (src.HasProperty("_Color") && inst.HasProperty("_Color"))
            {
                Color c = src.GetColor("_Color");
                c.a = alpha;
                inst.SetColor("_Color", c);
            }
            if (src.HasProperty("_BaseMap") && inst.HasProperty("_BaseMap"))
            {
                Texture t = src.GetTexture("_BaseMap");
                Texture mod = TryCreateAlphaModifiedTexture(t, alpha);
                inst.SetTexture("_BaseMap", mod != null ? mod : t);
            }
            if (src.HasProperty("_BaseColor") && inst.HasProperty("_BaseColor"))
            {
                Color bc = src.GetColor("_BaseColor");
                bc.a = alpha;
                inst.SetColor("_BaseColor", bc);
            }
        }
        catch { }

        return inst;
    }

    private void ApplyTransparentSettings(Material m, float alpha)
    {
        if (m == null) return;
        try
        {
            m.SetOverrideTag("RenderType", "Transparent");
            if (m.HasProperty("_Mode"))
            {
                m.SetFloat("_Mode", 3f);
                m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
                m.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
                if (m.HasProperty("_ZWrite")) m.SetFloat("_ZWrite", 0f);
                m.DisableKeyword("_ALPHATEST_ON");
                m.EnableKeyword("_ALPHABLEND_ON");
                m.DisableKeyword("_ALPHAPREMULTIPLY_ON");
                m.renderQueue = 3000;
            }

            if (m.HasProperty("_Surface"))
            {
                m.SetFloat("_Surface", 1f);
                if (m.HasProperty("_ZWrite")) m.SetFloat("_ZWrite", 0f);
                m.renderQueue = 3000;
            }

            string[] colorProps = new string[] { "_BaseColor", "_Color", "_TintColor", "_MainColor" };
            foreach (var prop in colorProps)
            {
                if (m.HasProperty(prop))
                {
                    try
                    {
                        Color c = m.GetColor(prop);
                        c.a = alpha;
                        m.SetColor(prop, c);
                    }
                    catch { }
                }
            }
        }
        catch { }
    }

    private Texture TryCreateAlphaModifiedTexture(Texture tex, float alpha)
    {
        if (tex == null) return null;
        Texture2D src = tex as Texture2D;
        if (src == null) return null;
        try
        {
            if (!src.isReadable) return null;
            Color32[] px = src.GetPixels32();
            for (int i = 0; i < px.Length; i++)
            {
                px[i].a = (byte)(px[i].a * Mathf.Clamp01(alpha));
            }
            Texture2D t = new Texture2D(src.width, src.height, src.format, src.mipmapCount > 1);
            t.SetPixels32(px);
            t.Apply();
            t.wrapMode = src.wrapMode;
            t.filterMode = src.filterMode;
            t.name = src.name + "_alpha";
            return t;
        }
        catch { return null; }
    }
}