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

public class GrassLoader : MonoBehaviour {

    public static GrassLoader instance;
    void Awake () {
        instance = this;
        }
    
    [Header("Requiered variables")]
    public Mesh grassMesh;
    public Mesh boxMesh;
    public Material instancedGrassMaterial;
    public Material instancedConsumeGrassMaterial;
    public GrassPositionScriptableObject grassStorage;
    public ComputeShader cullShader;
    public ComputeShader convertArrayShader;

    [Header("Tilemap (Occlusion culling)")]
    public Vector3 bottomLeftCorner;
    public float cellSideLengh = 1f;
    public float totalLength = 100f;

    [Header("3D culling")]
    public Vector2 distanceBounds;
    public Texture2D ditherTexture;
    public float ditherSize;

    [Header("Manual controls")]
    public bool update;
    public bool updateData;
    public bool bakeCells;
    public bool bakeOcclusion;

    [Header("Testing")]
    public bool debugTilemap1;
    public bool debugTilemap2;
    public bool debugTilemap3;
    public bool debugTilemap4;
    public bool showTilemap;

    [Space]
    public bool doCells;
    public bool doCulling;
    public bool doOcclusionCulling;

    //[Header("Testing")]
    //public bool update = false;

    //data types
    struct grassElement {
        public Vector3 position;
        public float rotation;
        public float size;
        public float colorBlend;//0-1
        public grassElement (Vector3 position, float rotation, float size, float colorBlend) {
            this.position = position;
            this.rotation = rotation;
            this.size = size;
            this.colorBlend = colorBlend;
            }
        //Total size of is 4*3 + 3*4 = 24
        }

    struct grassCell {
        public uint[] grassElementIndexes;//map to grass elements
        public grassCell (uint[] grassElementIndexes) {
            this.grassElementIndexes = grassElementIndexes;
            }
        public grassCell(grassCell x) {
            grassElementIndexes = new uint[x.grassElementIndexes.Length];
            for(int i = 0;i < x.grassElementIndexes.Length;i++){
                grassElementIndexes[i] = x.grassElementIndexes[i] * 1;
                }
            }
        //Total size = 4*length = must be calculated
        }

    struct grassOcclusionCell {
        public uint grassCellIndex;
        public uint[] visibleCellIndexes;//Map to occlusion cells
        public grassOcclusionCell (uint grassCellIndex, uint[] visibleCellIndexes) {
            this.grassCellIndex = grassCellIndex;
            this.visibleCellIndexes = visibleCellIndexes;
            }
        //Total size = 4 + 4*length = must be calculated
        }

    //buffers
    uint[] arguments = new uint[5] {0,0,0,0,0};
        //main
    ComputeBuffer grassBuffer;
    ComputeBuffer argumentsBuffer;
    ComputeBuffer allArgumentsBuffer;
        //culling
    ComputeBuffer workBuffer;//Append/Consume buffer for grass elements
    ComputeBuffer grassCellBuffer;
    ComputeBuffer cellWorkBuffer;
    ComputeBuffer convertSizeBuffer;


    public bool isOn = true;
    

    int elementCount = 0;
    int cellCount = 0;

    bool startupDone;

    grassElement[] grassElements;
    Camera cam;

    public void BakeCells () {
        if(grassElements == null) UpdateMainBuffers();
        bakeCells = false;
        //Maps points into cells

        // 0. Create tilemap
        var tilemap = new Dictionary<int,List<int>>();
        int cellsCount = Mathf.CeilToInt(totalLength/cellSideLengh);
        for(int x = 0;x < cellsCount;x++) for(int y = 0;y < cellsCount;y++) tilemap.Add(x + y*cellsCount,new List<int>());

        // 1. Set grassObjects into tiles
        for(int i = 0;i < elementCount;i++) tilemap[GetTileIndex(grassElements[i].position)].Add(i);

        //Debug
        if(debugTilemap1) for(int x = 0;x < cellsCount;x++){
            for(int y = 0;y < cellsCount;y++){
                var pos = bottomLeftCorner + (x * Vector3.forward + y * Vector3.right)*cellSideLengh + Vector3.one * cellSideLengh/2f;
                int i = GetTileIndex(pos);
                if(tilemap.ContainsKey(i)) foreach(var k in tilemap[i]) Debug.DrawLine(grassElements[k].position + Vector3.right* 0.2f, pos,Color.green,100f);
                }
            }

        
        // 2. Cull empty tiles
        var removables = new Queue<int>();
        foreach(var x in tilemap) if(x.Value.Count == 0) removables.Enqueue(x.Key);
        foreach(var x in removables) tilemap.Remove(x);

        //Debug
        if(debugTilemap2) for(int x = 0;x < cellsCount;x++){
            for(int y = 0;y < cellsCount;y++){
                var pos = bottomLeftCorner + (x * Vector3.forward + y * Vector3.right)*cellSideLengh + Vector3.one * cellSideLengh/2f + Vector3.up;
                int i = GetTileIndex(pos);
                if(tilemap.ContainsKey(i)) foreach(var k in tilemap[i]) {Debug.DrawLine(grassElements[k].position + Vector3.right* 0.2f, pos,Color.magenta,100f); Debug.DrawRay(pos, Vector3.up,Color.magenta,100f);}
                }
            }

        // 3. Get cell average grass count - not needed anymore
        //float total = 0; float count = 0;
        //foreach(var x in tilemap) { total += x.Value.Count; count++; }
        //int mean = (int)(total/count);

        //TODO 4. Convert tiles into cells of 512 elements (512 bc byte limit is 2048 and 1 index = 4 bytes so 512*4 = 2048)

        var cells = new List<grassCell>();
        var cellIdTilemap = new Dictionary<int,List<int>>();
        foreach(var x in tilemap) {
            //foreach entry convert indexes into how many cells are needed
            var tileCellIDs = new List<int>();

            var cellGrassIndexes = x.Value;
            int leftToProcess = cellGrassIndexes.Count;

            while(leftToProcess > 0) {
                //Create new cell
                var grassIndexArray = new uint[512];
                for(int i = 0;i < grassIndexArray.Length;i++) {
                    if(cellGrassIndexes.Count > 0) { 
                        grassIndexArray[i] = (uint)(cellGrassIndexes[0]); 
                        cellGrassIndexes.RemoveAt(0); 
                        leftToProcess--;
                        } 
                    else grassIndexArray[i] = 0;
                    }
                cells.Add( new grassCell(grassIndexArray) );
                //Remember id
                tileCellIDs.Add(cells.Count-1);
                }
            //Save cell ids so they can be used in occlusion culling
            cellIdTilemap.Add(x.Key,tileCellIDs);
            }


        //Debug, foreach cell show which cells they map to by each cells connections
        if(debugTilemap3) for(int x = 0;x < cellsCount;x++){
            for(int y = 0;y < cellsCount;y++){
                var pos = bottomLeftCorner + (x * Vector3.forward + y * Vector3.right)*cellSideLengh + Vector3.one * cellSideLengh/2f + Vector3.up * 2f;
                int i = GetTileIndex(pos);
                if(cellIdTilemap.ContainsKey(i)) {
                    //foreach cell get center
                    foreach(var cellIndex in cellIdTilemap[i]) {
                        var cell = cells[cellIndex];
                        Vector3 center = Vector3.zero;
                        float count = 0;
                        foreach(var grassIndex in cell.grassElementIndexes) {
                            if(grassIndex > 0) {center += grassElements[grassIndex].position; count++; Debug.DrawLine(grassElements[grassIndex].position, pos,Color.white,100f);}
                            }
                        Debug.DrawLine(center / count, pos,Color.blue,100f);
                        }
                    }
                }
            }

        // 5. Convert cells into array (bc array in a struct is not bittable...)
        cellCount = cells.Count;

        var convertedArray = new uint[cellCount * 512];
        for(int i = 0;i < cellCount;i++){
            var c = cells[i];
            for(int j = 0;j < 512;j++) { convertedArray[j + i * 512] = c.grassElementIndexes[j]; if(debugTilemap4) Debug.DrawRay(grassElements[c.grassElementIndexes[j]].position, Vector3.up,Color.magenta,100f); }
            }

        //Wipe old buffers
        if(grassCellBuffer != null) grassCellBuffer.Release();
        if(cellWorkBuffer != null) cellWorkBuffer.Release();
        if(convertSizeBuffer != null) convertSizeBuffer.Release();

        //Create new buffers
        grassCellBuffer = new ComputeBuffer(cellCount, 2048);
        cellWorkBuffer = new ComputeBuffer(cellCount, 4, ComputeBufferType.Append);
        convertSizeBuffer = new ComputeBuffer(1, 12, ComputeBufferType.IndirectArguments);

        //Upload data
        grassCellBuffer.SetData(convertedArray);
        cellWorkBuffer.SetCounterValue(0);
        convertSizeBuffer.SetData(new uint[]{1,1,1});

        //Send buffers to shaders
        cullShader.SetBuffer(1, "grassBuffer", grassBuffer);
        cullShader.SetBuffer(1, "grassCellBuffer", grassCellBuffer);
        cullShader.SetBuffer(1, "outCellIndexBuffer", cellWorkBuffer);
        //cullShader.SetBuffer(1, "outGrassBuffer", workBuffer);

        convertArrayShader.SetBuffer(0, "grassCellBuffer", grassCellBuffer);
        convertArrayShader.SetBuffer(0, "outGrassBuffer", workBuffer);
        convertArrayShader.SetBuffer(0, "grassCellIndexBuffer", cellWorkBuffer);

        //Set buffer values
        cullShader.SetInt("grassCellCount", cellCount);
        cullShader.SetInt("grassCellSize", Mathf.CeilToInt(cellSideLengh));
        cullShader.SetVector("distanceBounts", distanceBounds);
        cullShader.SetTexture(0, "ditherTex", ditherTexture);
        cullShader.SetTexture(1, "ditherTex", ditherTexture);
        cullShader.SetFloat("ditherTexSizeX", ditherTexture.width);
        cullShader.SetFloat("ditherSize", ditherSize);
        
        Debug.Log("GRASS: Created " + cellCount.ToString() + " cells");
        }

    public void BakeOcclusionCells () {
        /*
        bakeOcclusion = false;
        //Creates occlusion cells

        //OCCLUSION GENERATION (from created cell structs)

        // 1. Create target low and full objects
        var grassCollider = new GameObject();
        grassCollider.transform.name = "TEMP_GRASS_COLLIDER";
        grassCollider.AddComponent(typeof(BoxCollider));
        var gMeshFilter = grassCollider.AddComponent(typeof(MeshFilter)) as MeshFilter;
        var gMeshRenderer = grassCollider.AddComponent(typeof(MeshRenderer)) as MeshRenderer;
        gMeshFilter.mesh = boxMesh;
        grassCollider.transform.localScale = new Vector3(cellSideLengh,0.5f,cellSideLengh);

        var fallableCollider = new GameObject();
        fallableCollider.transform.name = "TEMP_FALLABLE_COLLIDER";
        fallableCollider.AddComponent(typeof(BoxCollider));
        var fMeshFilter = fallableCollider.AddComponent(typeof(MeshFilter)) as MeshFilter;
        var fMeshRenderer = fallableCollider.AddComponent(typeof(MeshRenderer)) as MeshRenderer;
        fMeshFilter.mesh = boxMesh;
        fallableCollider.transform.localScale = new Vector3(cellSideLengh,20f,cellSideLengh);

        // 2. Foreach cell try to see each other cell, if not already marked visible

        //Create full tilemap of all possible places where camera might be
        var fullTilemap = new HashSet<int>();
        for(int x = 0;x < cellCount;x++){
            for(int y = 0;y < cellCount;y++){
                var pos = bottomLeftCorner + (x * Vector3.forward + y * Vector3.right)*cellSideLengh;
                if(Physics.Raycast(pos + Vector3.up*100f, -Vector3.up, Mathf.Infinity)) fullTilemap.Add(GetTileIndex(pos));
                }
            }

        var occlusionCells = new Dictionary<int,List<int>>();
        foreach(var x in tilemap) occlusionCells.Add(x.Key,new List<int>());
        foreach(var x in fullTilemap) if(!occlusionCells.ContainsKey(x)) occlusionCells.Add(x,new List<int>());

        void PositionColliderAtPostion (Vector3 pos, Transform collider) {
            RaycastHit hit;
            if(Physics.Raycast(pos + Vector3.up*100f, -Vector3.up, out hit, Mathf.Infinity)) {
                collider.position = new Vector3(pos.x,hit.point.y + collider.localScale.y/2f,pos.z);
                }
            else collider.position = new Vector3(pos.x,0,pos.z);
            }
        bool DoRaycastTest (Vector3 from, Vector3 to, string targetName) {
            //Will shoot rays 1. from the bottom of from, then from middle and then from top, if hit then returns true, else false
            bool ShootRays (Vector3 pos) {
                RaycastHit hit;
                for(int i = 0;i < 5;i++){
                    var start  = pos + new Vector3(Random.Range(0,1f),Random.Range(0,.5f),Random.Range(0,1f));
                    var target = to + new Vector3(Random.Range(0,1f),Random.Range(0,.5f),Random.Range(0,1f));
                    if( Physics.Raycast(start, target-start, out hit, Mathf.Infinity) ) if(hit.transform.name == targetName) return true;
                    }
                return false;
                }
            if(ShootRays(from)) return true;
            if(ShootRays(from + Vector3.up * 2f)) return true;
            if(ShootRays(from + Vector3.up * 4f)) return true;
            return false;
            }
        void TestVisibilityBetweenCells (int i, int j) {
            //0. see if already done
            var oI = occlusionCells[i];
            var oJ = occlusionCells[j];
            if(!oI.Contains(j)) {
                //1. Get both positions
                var posI = bottomLeftCorner + (i % cellCount) * Vector3.forward * cellSideLengh + Mathf.FloorToInt(i/cellCount) * Vector3.right * cellSideLengh;
                var posJ = bottomLeftCorner + (j % cellCount) * Vector3.forward * cellSideLengh + Mathf.FloorToInt(j/cellCount) * Vector3.right * cellSideLengh;
                //2. Try visibility form i -> j
                PositionColliderAtPostion(posJ, grassCollider.transform);
                if(DoRaycastTest(posI, posJ, "TEMP_GRASS_COLLIDER")) {
                    //3. If possible then register both as possible
                    oI.Add(j);
                    oJ.Add(i);
                    }
                }
            }

        int k = 0;
        float t = fullTilemap.Count * tilemap.Count;
        foreach(var x in fullTilemap) foreach(var y in tilemap) if(x != y.Key) {TestVisibilityBetweenCells(x,y.Key); k++; if(k%30000==0) {yield return 0;Debug.Log((k/t).ToString() + "% done");} } //Do all possible tests
        Destroy(grassCollider);
        Destroy(fallableCollider);

        //Debug show connections


        //TODO 3. Get cell average seeable length.

        //TODO 4. Convert all cells into structs, if more seeables then create more structs per cell

        //TODO Give cell and occulsion data to cullShader
        

        //1. Create walkable area

        //2. Foreach cell in walkable area find all grass cells that it sees

        //3. Save data to storage
        */
        }

    public void Setup () {
        cam = Camera.main;
        //arguments buffer
        argumentsBuffer    = new ComputeBuffer(1, arguments.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
        allArgumentsBuffer = new ComputeBuffer(1, arguments.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
        //Main buffer
        UpdateMainBuffers();
        //Cells
        BakeCells();
        //Occlusion
        //BakeOcclusionCells();
        }

    void DebugWorkBufferCount () {
        var tempBuffer = new ComputeBuffer(1, sizeof(uint), ComputeBufferType.IndirectArguments);
        uint[] x = new uint[1];
        ComputeBuffer.CopyCount(workBuffer,tempBuffer,0);
        tempBuffer.GetData(x);
        var s = "";
        foreach(var k in x) s += " " + k.ToString();
        Debug.Log(s);
        tempBuffer.Release();
        }

    void DebugWorkCellBufferCount () {
        var tempBuffer = new ComputeBuffer(1, sizeof(uint), ComputeBufferType.IndirectArguments);
        uint[] x = new uint[1];
        ComputeBuffer.CopyCount(cellWorkBuffer,tempBuffer,0);
        tempBuffer.GetData(x);
        var s = "";
        foreach(var k in x) s += " " + k.ToString();
        Debug.Log(s);
        tempBuffer.Release();
        }

    public void Run () {
        if(!startupDone) return;//Wait for startup do complete
        //0. manual update
        if(update) UpdateMainBuffers();
        if(bakeCells) BakeCells();
        
        //1. UpdateCullBuffer
        if(doCulling) UpdateCullBuffer();

        //Draw
        if(isOn) Graphics.DrawMeshInstancedIndirect(grassMesh, 0, doCulling?instancedConsumeGrassMaterial:instancedGrassMaterial, new Bounds(Vector3.zero, new Vector3(1000.0f, 1000.0f, 1000.0f)), doCulling?argumentsBuffer:allArgumentsBuffer);

        //Testing
        if(showTilemap) {
            int cellsCount = Mathf.CeilToInt(totalLength/cellSideLengh);
            for(int x = 0;x < cellsCount;x++){
                for(int y = 0;y < cellsCount;y++){
                    var pos = bottomLeftCorner + (x * Vector3.forward + y * Vector3.right)*cellSideLengh;
                    Debug.DrawRay(pos, Vector3.up * 5f,Color.blue);
                    }
                }
            }
        }

    void UpdateCullBuffer () {
        //1. Update frustrum culling information
        var cam = Camera.main;
        var mat = cam.projectionMatrix * cam.worldToCameraMatrix;
        var normals = new Vector3[4];
        var normalsMatrix = new float[12];

        Vector3 temp;
        //left
        temp.x = mat.m30 + mat.m00;
        temp.y = mat.m31 + mat.m01;
        temp.z = mat.m32 + mat.m02;
        normals[0] = temp;
        //right
        temp.x = mat.m30 - mat.m00;
        temp.y = mat.m31 - mat.m01;
        temp.z = mat.m32 - mat.m02;
        normals[1] = temp;
        //bottom
        temp.x = mat.m30 + mat.m10;
        temp.y = mat.m31 + mat.m11;
        temp.z = mat.m32 + mat.m12;
        normals[2] = temp;
        //top
        temp.x = mat.m30 - mat.m10;
        temp.y = mat.m31 - mat.m11;
        temp.z = mat.m32 - mat.m12;
        normals[3] = temp;
        
        for (int i = 0; i < 4; i++){
            //Debug.DrawRay(camera.transform.position, _planes[i].normal * 10f, Color.yellow);
            normalsMatrix[i + 0] = normals[i].x;
            normalsMatrix[i + 4] = normals[i].y;
            normalsMatrix[i + 8] = normals[i].z;
            }

        cullShader.SetFloats("cameraPos", cam.transform.position.x, cam.transform.position.y, cam.transform.position.z);
        cullShader.SetInt("doOcclusion",doOcclusionCulling?1:0);
        cullShader.SetFloats("cameraFrustumNormals", normalsMatrix);

        //Reset
        if(cellWorkBuffer != null) cellWorkBuffer.SetCounterValue(0);
        workBuffer.SetCounterValue(0);

        //2. Dispatch culler
        if(doCells && cellCount > 0 && cellWorkBuffer != null) {
            
            //1. Get cell indexes
            int batchCount = Mathf.CeilToInt(cellCount/64f);
            cullShader.Dispatch(1,batchCount,1,1);
            //2. Get object indexes
            //convertSizeBuffer.SetData(new uint[] {1,1,1});
            ComputeBuffer.CopyCount(cellWorkBuffer,convertSizeBuffer,0);
            
            convertArrayShader.DispatchIndirect(0,convertSizeBuffer);
            }
        else {
            int batchCount = Mathf.CeilToInt(elementCount/1024f);
            cullShader.Dispatch(0,batchCount,1,1);
            }
        
        //3. Copy work count -> arguments
        ComputeBuffer.CopyCount(workBuffer,argumentsBuffer,4);
        }

    int GetTileIndex (Vector3 pos) {
        int cellsCount = Mathf.CeilToInt(totalLength/cellSideLengh);
        //1. relative pos
        var p = pos - bottomLeftCorner;
        //2. safety
        if(p.x < 0 || p.x > totalLength || p.z < 0 || p.z > totalLength) return 0;
        //3. get index
        return Mathf.CeilToInt(p.x/cellSideLengh) + cellsCount * Mathf.CeilToInt(p.z/cellSideLengh);
        }

    void UpdateMainBuffers () {
        startupDone = false;
        update = false;

        //Debug.Log("Updating main");
        elementCount = grassStorage.points.Count;

        //Reset buffers
        if(workBuffer != null) workBuffer.Release();
        if(grassBuffer != null) grassBuffer.Release();
        grassBuffer = new ComputeBuffer(elementCount, 24);
        workBuffer = new ComputeBuffer(elementCount, 4, ComputeBufferType.Append);
        workBuffer.SetCounterValue(0);//effectivly this clears the buffer

        //Create grass elements from data
        grassElements = new grassElement[elementCount];

        for(int i = 0;i < elementCount;i++){
            grassElements[i] = new grassElement(
                grassStorage.points[i],
                Random.Range(0f,360f),
                Random.Range(0.9f,1f),
                Random.Range(0,1f)
                );
            //Debug.DrawRay(grassStorage.points[i] + Vector3.right* 0.2f, Vector3.up * 10f,Color.yellow,100f);
            }

        Debug.Log("GRASS: Working with a total of " + grassElements.Length.ToString() + " grass objects");

        //Save data to buffer
        grassBuffer.SetData(grassElements);

        distanceBounds = new Vector2(Mathf.Min(distanceBounds.y, distanceBounds.x),Mathf.Max(distanceBounds.y, distanceBounds.x)) ;

        //Set buffers
        cullShader.SetInt("grassCount", elementCount);
        cullShader.SetFloat("maxDistance", distanceBounds.y);
        cullShader.SetBuffer(0, "grassBuffer", grassBuffer);
        cullShader.SetBuffer(0, "outGrassBuffer", workBuffer);

        instancedConsumeGrassMaterial.SetBuffer("grassBuffer", grassBuffer);
        instancedConsumeGrassMaterial.SetBuffer("inGrassBuffer", workBuffer);

        instancedGrassMaterial.SetBuffer("grassBuffer", grassBuffer);
        instancedGrassMaterial.SetFloat("fadeStartDist", distanceBounds.x - cellSideLengh);
        instancedGrassMaterial.SetFloat("fadeEndDist", distanceBounds.y - cellSideLengh);

        //Set arguments
        arguments[0] = (grassMesh != null) ? (uint)grassMesh.GetIndexCount(0) : 0;
        arguments[1] = (uint)elementCount;
        argumentsBuffer.SetData(arguments);
        allArgumentsBuffer.SetData(arguments);

        if(doCulling) UpdateCullBuffer();
        startupDone = true;
        }

    void OnDisable() {
        if(grassBuffer != null) grassBuffer.Release(); grassBuffer = null;
        if(argumentsBuffer != null) argumentsBuffer.Release(); argumentsBuffer = null;
        if(allArgumentsBuffer != null) allArgumentsBuffer.Release(); allArgumentsBuffer = null;
        if(workBuffer != null) workBuffer.Release(); workBuffer = null;
        if(grassCellBuffer != null) grassCellBuffer.Release(); grassCellBuffer = null;
        if(cellWorkBuffer != null) cellWorkBuffer.Release(); cellWorkBuffer = null;
        if(convertSizeBuffer != null) convertSizeBuffer.Release(); convertSizeBuffer = null;
        }

    void Start () {
        Setup();
        }

    void Update () {
        Run();
        }
}


/*
TODOS

1. Occlusion
- Switch to quads instead of traingles for grass
- Try shape that is more similar to cutout
- Add Wind
- Second billboard shader
 - Billboard switch range
 - Standard cull two outputs
 - Cell cull two outputs
 - Cell cull second converter
- Free camera
- Cell optimizations
- Distance dither cull? (optional)

2. Fallable particles (Rain, snow)
- Plan

2.1 Rain drop splashes

3. Volumetric fog with moving 3D noise and godrays?

4. Rain puddles, shininess

*/