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

public class UniqueShadowManager
{
    static UniqueShadowManager instance;
    const int SHADOWCASTERPASS = 1;
    public struct UniqueDrawInfo
    {
        public Renderer renderer;
        public Material mat;
        public int shaderPass;
        public bool lastState; //used to impl deadzone offset
        public UniqueDrawInfo(Renderer r, Material m, int passID)
        {
            this.renderer = r;
            this.mat = m;
            lastState = false;
            this.shaderPass = passID;
        }
    }
    Dictionary<GameObject, List<UniqueDrawInfo>> characterPartsDict = new Dictionary<GameObject, List<UniqueDrawInfo>>(4);
    List<UniqueDrawInfo> inShadowRangeRenderers = new List<UniqueDrawInfo>(4);

    GameObject mainCharacter;
    Transform lightTrans;
    GameObject decalBox;
    Vector3[] cornerIndice = { new Vector3(1.0f, 1.0f, 1.0f), new Vector3(1.0f, 1.0f, -1.0f), new Vector3(1.0f, -1.0f, -1.0f), new Vector3(1.0f, -1.0f, -1.0f),
                                   new Vector3(-1.0f, 1.0f, 1.0f), new Vector3(-1.0f, 1.0f, -1.0f), new Vector3(-1.0f, -1.0f, -1.0f), new Vector3(-1.0f, -1.0f, -1.0f)};
    //
    const float OrthogonalHalfSize = 5.0f;
    const float nearPlane = 0.3f;
    const float farPlane = 30.0f;
    const float deadZone = 1.0f;

    Matrix4x4 viewMatrix = Matrix4x4.zero;
    Matrix4x4 projMatrix = Matrix4x4.zero;
    public static UniqueShadowManager Instance
    {
        get
        {
            if (null == instance)
            {
                instance = new UniqueShadowManager();
                instance.CalculateProjMatrix();
            }
            return instance;
        }
    }

    public bool HasNoRendererJob()
    {
        if (0 == inShadowRangeRenderers.Count)
            return true;
        else
            return false;
    }

    public void PopulateCommondListForUniqueShadowmap(CommandBuffer cmd)
    {
        for (int i = 0; i < inShadowRangeRenderers.Count; ++i)
        {
            var drawInfo = inShadowRangeRenderers[i];
            cmd.DrawRenderer(drawInfo.renderer, drawInfo.mat, 0, SHADOWCASTERPASS);
        }
    }

    public void ExtractLightMatrix(Light light, out Matrix4x4 view, out Matrix4x4 proj, out Matrix4x4 shadowTransform)
    {
        CaculateShadowBoxPosture(light.transform);

        view = viewMatrix;
        proj = projMatrix;

        shadowTransform = GetShadowTransform(proj, view);
    }
    public void RegisterUniqueShadow(GameObject go)
    {
        if (null == go || characterPartsDict.ContainsKey(go))
            return;

        var parts = go.GetComponentsInChildren<Renderer>();
        float characterSize = 1.0f;

        var drawInfoList = new List<UniqueDrawInfo>();

        for (int i = 0; i < parts.Length; ++i)
        {
            //first turn off its original shadow caster
            parts[i].shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
            //add to our unique shadow draw info
            var mat = parts[i].sharedMaterial;
            var shadowPassId = mat.FindPass("ShadowCaster");
            var drawInfo = new UniqueDrawInfo(parts[i], mat, shadowPassId);
            drawInfoList.Add(drawInfo);

            var b = parts[i].bounds;
            var temp = Mathf.Max(Mathf.Max(b.size.x, b.size.y), b.size.z);
            characterSize = Mathf.Max(temp, characterSize);
        }

        characterPartsDict.Add(go, drawInfoList);

    }

    public void RegisterLight(GameObject go, bool isMain, Light light)
    {
        if (isMain)
        {
            mainCharacter = go;
            lightTrans = light.transform;
            CreateShadowDecalBox(go.transform, lightTrans);
        }
    }

    public void UnRegisterUniqueShadow(GameObject go)
    {
        if (!characterPartsDict.ContainsKey(go))
            return;
        if (mainCharacter == go)
            mainCharacter = null;

        var parts = characterPartsDict[go];
        for (int i = 0; i < parts.Count; ++i)
        {
            parts[i].renderer.shadowCastingMode = ShadowCastingMode.On;
        }
        characterPartsDict.Remove(go);
        //clear cache
        inShadowRangeRenderers.Clear();
    }

    public void OnRendererPartsChanged(GameObject go)
    {
        characterPartsDict.TryGetValue(go, out List<UniqueDrawInfo> oList);
        if (null == oList)
        {
            oList = new List<UniqueDrawInfo>();
            characterPartsDict.Add(go, oList);
        }

        var oriList = characterPartsDict[go];
        oriList.Clear();

        Transform exclude = decalBox.transform;

        float size = 1.0f;
        var parts = go.GetComponentsInChildren<Renderer>();
        for (int i = 0; i < parts.Length && parts[i].transform != exclude; ++i)
        {
            //first turn off its original shadow caster
            parts[i].shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
            //add to our unique shadow draw info
            var mat = parts[i].sharedMaterial;
            var shadowPassId = mat.FindPass("ShadowCaster");
            var drawInfo = new UniqueDrawInfo(parts[i], mat, shadowPassId);
            oriList.Add(drawInfo);

            var b = parts[i].bounds;
            var temp = Mathf.Max(Mathf.Max(b.size.x, b.size.y), b.size.z);
            size = Mathf.Max(temp, size);
        }
    }

    void CreateShadowDecalBox(Transform mainCharacterTrans, Transform lightTransform)
    {
        if (null == decalBox)
        {
            decalBox = GameObject.CreatePrimitive(PrimitiveType.Cube);
            decalBox.name = "uniqueDecal";
            decalBox.layer = LayerMask.NameToLayer("TransparentFX");
            decalBox.transform.parent = mainCharacterTrans;
            var col = decalBox.GetComponent<BoxCollider>();
            if (col)
                UnityEngine.Object.Destroy(col);
            var renderer = decalBox.GetComponent<Renderer>();

#if UNITY_EDITOR
            renderer.shadowCastingMode = ShadowCastingMode.Off;
            renderer.receiveShadows = false;
            Shader s = Shader.Find("Hidden/Universal Render Pipeline/FallbackError");
            var tempMat = new Material(s);
            tempMat.color = Color.red;
            renderer.sharedMaterial = tempMat;
            renderer.enabled = false;
#else
            if (renderer)
                UnityEngine.Object.Destroy(renderer);
#endif
        }
        SetDecalBoxSize();
        UpdateShadowDecalBoxPosture(lightTransform);
    }

    void SetDecalBoxSize()
    {
        decalBox.transform.localScale = Vector3.one;
    }

    void UpdateShadowDecalBoxPosture(Transform lightTransform)
    {
        var decalOffset = lightTransform.forward * (-decalBox.transform.localScale.x);
        decalBox.transform.position = decalOffset + mainCharacter.transform.position + new Vector3(0.0f, 0.5f * decalBox.transform.localScale.y, 0.0f);
        decalBox.transform.forward = -lightTransform.forward;
    }

    void UpdateShadowDecalBoxPosture(Transform lightTransform, Vector3 decalPos, float characterExtent)
    {
        var decalOffset = lightTransform.forward * (- 5 * characterExtent); // this is arbitrary since in orthogonal projection z has no impact with scale
        decalBox.transform.position = decalOffset + decalPos + new Vector3(0.0f, 0.5f * characterExtent, 0.0f);
        decalBox.transform.forward = -lightTransform.forward;
    }

    void CaculateShadowBoxPosture(Transform lightTransform)
    {
        if (null == mainCharacter)
            return;
        inShadowRangeRenderers.Clear();

        var mainPos = mainCharacter.transform.position;
        float characterExtent = 1.0f;

        foreach(var proxyPair in characterPartsDict)
        {
            var go = proxyPair.Key;
            //Bounds bb = ExtractBoundsFromCollider(go);
            Bounds bb = ExtractBoundsFromRenderer(proxyPair.Value, out bool lastState);

            var bbExtent = Mathf.Max(Mathf.Max(bb.size.x, bb.size.y), bb.size.z);

            var posCandi = go.transform.position;
            var candDist = (posCandi - mainPos).magnitude;
            //we only add target that is neither too far nor too big
            if(go == mainCharacter || bbExtent < OrthogonalHalfSize && candDist < 2.0f * OrthogonalHalfSize && FullyInsideShadowBox(ref bb, lastState))
            {
                SetOriginalShadowOff(proxyPair.Value);
                inShadowRangeRenderers.AddRange(proxyPair.Value);

                characterExtent = Mathf.Max(bbExtent, characterExtent);
            }
            else
            {
                SetOriginalShadowOn(proxyPair.Value);
            }
        }

        UpdateShadowDecalBoxPosture(lightTransform, mainPos, characterExtent);
        UpdateLightMatrix();
    }

    bool FullyInsideShadowBox(ref Bounds bb, bool lastState)
    {
        float deadOffset = lastState ? deadZone : -deadZone;

        for (int i = 0; i < 8; ++i)
        {
            var corner = bb.center + Vector3.Scale(cornerIndice[i], bb.extents);
            var cornerPosSS = decalBox.transform.InverseTransformPoint(corner);

            float distSqr = cornerPosSS.x * cornerPosSS.x + cornerPosSS.y * cornerPosSS.y;

            if (distSqr > OrthogonalHalfSize * OrthogonalHalfSize + deadOffset)
                return false;
        }

        return true;
    }

    Bounds ExtractBoundsFromRenderer(List<UniqueDrawInfo> parts, out bool lastState)
    {
        Bounds bb = parts[0].renderer.bounds;
        lastState = parts[0].lastState;

        for (int i = 1; i < parts.Count; ++i)
        {
            bb.Encapsulate(parts[i].renderer.bounds);
        }

        return bb;
    }

    Bounds ExtractBoundsFromCollider(GameObject go)
    {
        var collider = go.GetComponent<Collider>();
        return collider.bounds;
    }

    void SetOriginalShadowOff(List<UniqueDrawInfo> parts)
    {
        UniqueDrawInfo t = parts[0];
        t.lastState = true;
        parts[0] = t;

        for (int i = 0; i < parts.Count; ++i)
        {
            parts[i].renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
        }
    }

    void SetOriginalShadowOn(List<UniqueDrawInfo> parts)
    {
        UniqueDrawInfo t = parts[0];
        t.lastState = false;
        parts[0] = t;

        for (int i = 0; i < parts.Count; ++i)
        {
            parts[i].renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On;
        }
    }

    void UpdateLightMatrix()
    {
        viewMatrix = decalBox.transform.worldToLocalMatrix;
    }

    void CalculateProjMatrix()
    {
        projMatrix = Matrix4x4.zero;
        projMatrix.m00 = 1.0f / OrthogonalHalfSize;
        projMatrix.m11 = projMatrix.m00;
        projMatrix.m22 = -2.0f / (farPlane - nearPlane);
        projMatrix.m23 = -(nearPlane + farPlane) / (farPlane - nearPlane);
        projMatrix.m33 = 1;
    }

    static Matrix4x4 GetShadowTransform(Matrix4x4 proj, Matrix4x4 view)
    {
        // Currently CullResults ComputeDirectionalShadowMatricesAndCullingPrimitives doesn't
        // apply z reversal to projection matrix. We need to do it manually here.
        if (SystemInfo.usesReversedZBuffer)
        {
            proj.m20 = -proj.m20;
            proj.m21 = -proj.m21;
            proj.m22 = -proj.m22;
            proj.m23 = -proj.m23;
        }

        Matrix4x4 worldToShadow = proj * view;

        var textureScaleAndBias = Matrix4x4.identity;
        textureScaleAndBias.m00 = 0.5f;
        textureScaleAndBias.m11 = 0.5f;
        textureScaleAndBias.m22 = 0.5f;
        textureScaleAndBias.m03 = 0.5f;
        textureScaleAndBias.m23 = 0.5f;
        textureScaleAndBias.m13 = 0.5f;

        // Apply texture scale and offset to save a MAD in shader.
        return textureScaleAndBias * worldToShadow;
    }
}
