// Stylized Water 3 by Staggart Creations (http://staggart.xyz)
// COPYRIGHT PROTECTED UNDER THE UNITY ASSET STORE EULA (https://unity.com/legal/as-terms)
//    • Copying or referencing source code for the production of new asset store, or public, content is strictly prohibited!
//    • Uploading this file to a public repository will subject it to an automated DMCA takedown request.

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

namespace StylizedWater3
{
    [ExecuteInEditMode]
    [AddComponentMenu("Stylized Water 3/Water Grid")]
    public class WaterGrid : MonoBehaviour
    {
        [Tooltip("Material used on the tile meshes")]
        public Material material;
        
        [Tooltip("When not in play-mode, the water will follow the scene-view camera position.")]
        public bool followSceneCamera = false;
        [Tooltip("If enabled, the object with the \"MainCamera\" tag will be assigned as the follow target when entering play mode")]
        public bool autoAssignCamera;
        [Tooltip("The grid will follow this Transform's position on the XZ axis. Ideally set to the camera's transform.")]
        public Transform followTarget;
        
        [Tooltip("Scale of the entire grid in the length and width")]
        public float scale = 500f;
        [Range(0.15f, 10f)] 
        [Tooltip("Distance between vertices, rather higher than lower")]
        public float vertexDistance = 2f;
        [Min(0)]
        public int rowsColumns = 4;

        [HideInInspector]
        public int m_rowsColumns = 4;
        [SerializeField]
        [HideInInspector]
        private Mesh mesh;
        [SerializeField]
        [HideInInspector]
        private List<WaterObject> objects = new List<WaterObject>();
        
        [NonSerialized]
        private float tileSize;
        [NonSerialized]
        private WaterObject m_waterObject = null;
        [NonSerialized]
        private Transform actualFollowTarget;
        [NonSerialized]
        private Vector3 targetPosition;

        #if UNITY_EDITOR
        public static bool DisplayGrid = true;
        public static bool DisplayWireframe;
        #endif

        private void Reset()
        {
            Recreate();
        }
        
        private void Start()
        {
            if (autoAssignCamera) followTarget = Camera.main ? Camera.main.transform : followTarget;
        }
        
        private void OnEnable()
        {
#if UNITY_EDITOR
            UnityEditor.SceneView.duringSceneGui += OnSceneGUI;
#endif
            m_rowsColumns = rowsColumns;

            //Mesh is serialized with the scene, if component is used as a prefab, regenerate it
            if (mesh == null)
            {
                RecreateMesh();
                ReassignMesh();
            }
        }

#if UNITY_EDITOR
        private void OnDisable()
        {
            UnityEditor.SceneView.duringSceneGui -= OnSceneGUI;
        }
#endif

        void Update()
        {
            if (Application.isPlaying) actualFollowTarget = followTarget;

            if (actualFollowTarget)
            {
                targetPosition = actualFollowTarget.transform.position;

                targetPosition = SnapToGrid(targetPosition, vertexDistance);
                targetPosition.y = this.transform.position.y;
                this.transform.position = targetPosition;
            }
        }

        public void Recreate()
        {
            RecreateMesh();

            bool requireRecreate = (m_rowsColumns != rowsColumns) || objects.Count < (rowsColumns * rowsColumns);
            if (requireRecreate) m_rowsColumns = rowsColumns;

            //Only destroy/recreate objects if grid subdivision has changed
            if (requireRecreate && objects.Count > 0)
            {
                foreach (WaterObject obj in objects)
                {
                    if (obj) DestroyImmediate(obj.gameObject);
                }
                objects.Clear();
            }

            int index = 0;
            for (int x = 0; x < rowsColumns; x++)
            {
                for (int z = 0; z < rowsColumns; z++)
                {
                    if (requireRecreate)
                    {
                        m_waterObject = WaterObject.New(material, mesh);
                        objects.Add(m_waterObject);
                        
                        m_waterObject.transform.parent = this.transform;
                        m_waterObject.name = "WaterTile_x" + x + "z" + z;
                    }
                    else
                    {
                        m_waterObject = objects[index];
                        m_waterObject.AssignMesh(mesh);
                        m_waterObject.AssignMaterial(material);
                    }

                    m_waterObject.transform.localPosition = GridLocalCenterPosition(x, z);
                    m_waterObject.transform.localScale = Vector3.one;

                    index++;
                }
            }
        }

        private void CalculateTileSize()
        {
            rowsColumns = Mathf.Max(rowsColumns, 0);
            float m_scale = scale * this.transform.lossyScale.x;
            tileSize = Mathf.Max(1f, m_scale / rowsColumns);
        }
        
        private void RecreateMesh()
        {
            CalculateTileSize();
            
            float m_vertexDistance = vertexDistance * this.transform.lossyScale.x;
            
            //Value should never be larger than an individual tile
            m_vertexDistance = Mathf.Min(m_vertexDistance, tileSize);

            mesh = WaterMesh.Create(WaterMesh.Shape.Rectangle, tileSize, m_vertexDistance, tileSize);
        }

        private void ReassignMesh()
        {
            foreach (WaterObject obj in objects)
            {
                obj.AssignMesh(mesh);
            }
        }

        private Vector3 GridLocalCenterPosition(int x, int z)
        {
            return new Vector3(x * tileSize - ((tileSize * (rowsColumns)) * 0.5f) + (tileSize * 0.5f), 0f,
                z * tileSize - ((tileSize * (rowsColumns)) * 0.5f) + (tileSize * 0.5f));
        }

        public static Vector3 SnapToGrid(Vector3 position, float cellSize)
        {
            return new Vector3(SnapToGrid(position.x, cellSize), SnapToGrid(position.y, cellSize), SnapToGrid(position.z, cellSize));
        }

        private static float SnapToGrid(float position, float cellSize)
        {
            return Mathf.FloorToInt(position / cellSize) * (cellSize) + (cellSize * 0.5f);
        }

#if UNITY_EDITOR
        private void OnDrawGizmosSelected()
        {
            if (DisplayWireframe)
            {
                Gizmos.matrix = this.transform.localToWorldMatrix;
                Gizmos.color = new Color(0, 0, 0, 0.5f);

                foreach (WaterObject waterObject in objects)
                {
                    if(waterObject.meshFilter.sharedMesh) Gizmos.DrawWireMesh(waterObject.meshFilter.sharedMesh, waterObject.transform.localPosition);
                }
            }

            if (DisplayGrid)
            {
                if (tileSize <= 0) CalculateTileSize();
                
                Gizmos.color = new Color(1f, 0.25f, 0.25f, 0.5f);
                Gizmos.matrix = this.transform.localToWorldMatrix;
                
                for (int x = 0; x < rowsColumns; x++)
                {
                    for (int z = 0; z < rowsColumns; z++)
                    {
                        Vector3 pos = GridLocalCenterPosition(x, z);

                        Gizmos.DrawWireCube(pos, new Vector3(tileSize, 0f, tileSize));
                    }
                }
            }
        }

        private void OnSceneGUI(UnityEditor.SceneView sceneView)
        {
            if (followSceneCamera)
            {
                actualFollowTarget = sceneView.camera.transform;
                Update();
            }
            else
            {
                actualFollowTarget = null;
            }
        }
#endif
    }
}