﻿using System;
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;

namespace VRCExamples.MiniMapSample.Runtime
{
    [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
    public class MiniMap : UdonSharpBehaviour
    {
        [Tooltip("Only meshes on these layers will be captured to the Map texture")]
        public LayerMask captureLayers = int.MaxValue;
        [Tooltip("This is the output of the blit, you can then assign this texture to anything")]
        public RenderTexture miniMapTex;
        
        [Range(1, 82)]
        public int maxPlayers = 82;
        [Tooltip("Allows players to enable visibility of other players on the map")]
        public bool allowToShowOthers;
        [Tooltip("Shows other players on the map by default")]
        public bool showOthersByDefault;
        
        [Header("Visuals")]
        [ColorUsage(true, true)]
        public Color playerDotColor = Color.blue;
        public float playerDotSize = 1f;

        [ColorUsage(true, true)]
        public Color otherPlayersDotColor = Color.green;
        public float otherPlayersDotSize = 0.8f;

        [Tooltip("Centers the Map on the local player")]
        public bool followPlayer;
        [Range(0.5f, 30)]
        public float zoom = 1f;

        [Header("Performance")]
        [Tooltip("The amount of player positions to update per frame")]
        [Range(1, 16)]
        public int playerUpdateCountPerFrame = 4;
        [Tooltip("The amount of frames to wait before updating the player positions again")]
        [Range(1, 120)]
        public int playerUpdateRate = 1;
        [Tooltip("The amount of frames to wait before updating the map")]
        [Range(1, 120)]
        public int mapUpdateRate = 1;
        
        [Header("Map Internals")]
        public Camera captureCam;
        public RenderTexture captureTex;
        public Material miniMapBlitter;

        private VRCPlayerApi _lPlayer;
        private Matrix4x4 _transformMatrix;
        private float _camSize = 1f;
        private bool _showOthers;
        private VRCPlayerApi[] _players;
        private Vector4[] _playerPositions;

        private void Start()
        {
            // Setup the default values
            _lPlayer = Networking.LocalPlayer;
            _players = new VRCPlayerApi[maxPlayers];
            _playerPositions = new Vector4[maxPlayers];
            
            // Render the map in the initial position
            SetupCamera();
            captureCam.Render();

            // Set all the values on the Blit material
            _UpdateSettings();

            // Enable other players to be shown by default
            if (showOthersByDefault && allowToShowOthers)
            {
                _ShowOthersOn();
            }
            
            // Start main update loop
            _MapUpdateLoop();
        }

        private void SetupCamera()
        {
            captureCam.enabled = false;
            captureCam.targetTexture = captureTex;
            
            // We can utilize camera's transform matrix to convert from World Space player position
            // To camera-relative position.
            _transformMatrix = captureCam.worldToCameraMatrix;
            
            // Orthographic size returns the half-size (the value you see in inspector)
            // So to get full values we need to double the size.
            _camSize = captureCam.orthographicSize * 2;
            captureCam.cullingMask = captureLayers;
        }

        /// <summary>
        /// Captures the scene again, e.g. if you moved the camera to a new spot.
        /// Will automatically update all the properties on the Blit material.
        /// </summary>
        public void _RecaptureScene()
        {
            SetupCamera();
            captureCam.Render();
            _UpdateSettings();
        }

        /// <summary>
        /// Updates the values of the Blit material, call this after updating public variables.
        /// This function is also called after _SceneRecapture and on Start.
        /// </summary>
        public void _UpdateSettings()
        {
            miniMapBlitter.SetMatrix("_CameraMatrix", _transformMatrix);
            miniMapBlitter.SetFloat("_CameraSize", _camSize);
            miniMapBlitter.SetColor("_PlayerDotColor", playerDotColor);
            miniMapBlitter.SetFloat("_PlayerDotSize", playerDotSize);
            miniMapBlitter.SetColor("_OtherPlayersDotColor", otherPlayersDotColor);
            miniMapBlitter.SetFloat("_OtherPlayersDotSize", otherPlayersDotSize);
            miniMapBlitter.SetInt("_ShowOthers", _showOthers ? 1 : 0);
            miniMapBlitter.SetInt("_MaxPlayers", maxPlayers);
            miniMapBlitter.SetInt("_FollowPlayer", followPlayer ? 1 : 0);
            miniMapBlitter.SetFloat("_ZoomLevel", zoom);
            
            if (_showOthers)
            {
                miniMapBlitter.EnableKeyword("SHOW_OTHERS");
            }
            else
            {
                miniMapBlitter.DisableKeyword("SHOW_OTHERS");
            }
        }
        
        /// <summary>
        /// Toggles the visibility of other players on the map.
        /// Respects the value of `allowToShowOthers`.
        /// </summary>
        public void _ShowOthersToggle()
        {
            if (!allowToShowOthers && !_showOthers) return;
            _showOthers = !_showOthers;
            _UpdateSettings();
            if (!_showOthers) return;
            _OtherPlayersLoop();
        }

        /// <summary>
        /// Shows other players on the map.
        /// Respects the value of `allowToShowOthers`.
        /// </summary>
        public void _ShowOthersOn()
        {
            if (!allowToShowOthers) return;
            _showOthers = true;
            _UpdateSettings();
            _OtherPlayersLoop();
        }
        
        /// <summary>
        /// Hides other players on the map.
        /// </summary>
        public void _ShowOthersOff()
        {
            _showOthers = false;
            _UpdateSettings();
        }
        
        private int _playerUpdateIndex;
        
        /// <summary>
        /// Iterates over all the players at configurable rate of players per frame to save on performance.
        /// Runs in its own loop to allow for controlled update rate.
        /// </summary>
        public void _OtherPlayersLoop()
        {
            if (!_showOthers) return;
            // Determine how many players to process in this batch
            var maxLeft = Mathf.Clamp(maxPlayers - _playerUpdateIndex, 0, playerUpdateCountPerFrame);
            for (int i = 0; i < maxLeft; i++)
            {
                if (_players[_playerUpdateIndex + i] == null) continue;
                if (_players[_playerUpdateIndex + i].isLocal) continue;
                Vector4 playerPos = _players[_playerUpdateIndex + i].GetPosition();
                playerPos.w = 1f;
                _playerPositions[_playerUpdateIndex + i] = playerPos;
            }
            // Prepare for the next batch and call the iteration with the defined delay
            _playerUpdateIndex = (_playerUpdateIndex + playerUpdateCountPerFrame) % maxPlayers;
            SendCustomEventDelayedFrames(nameof(_OtherPlayersLoop), playerUpdateRate);
        }

        /// <summary>
        /// Passes the player positions to the Blit material every frame.
        /// All of position transformations are then performed on the gpu.
        /// </summary>
        public void _MapUpdateLoop()
        {
            if (!gameObject.activeSelf)
            {
                SendCustomEventDelayedFrames(nameof(_MapUpdateLoop), mapUpdateRate);
                return;
            }
            var pos = _lPlayer.GetPosition();
            miniMapBlitter.SetVector("_PlayerPos", pos);
            
            if (_showOthers)
            {
                miniMapBlitter.SetVectorArray("_PlayerPositions", _playerPositions);
            }
            
            // Passing the camera texture (our map background) to the `Blit` call - assigns it to `_MainTex`.
            VRCGraphics.Blit(captureTex, miniMapTex, miniMapBlitter);
            
            SendCustomEventDelayedFrames(nameof(_MapUpdateLoop), mapUpdateRate);
        }

        #region PlayerCouting
        // Save and remove players from our tracking array
        public override void OnPlayerJoined(VRCPlayerApi player)
        {
            for (int i = 0; i < _players.Length; i++)
            {
                if (_players[i] != null) continue;
                _players[i] = player;
                break;
            }
        }
        
        public override void OnPlayerLeft(VRCPlayerApi player)
        {
            for (int i = 0; i < _players.Length; i++)
            {
                if (_players[i] != player) continue;
                _players[i] = null;
                _playerPositions[i].w = 0f;
                break;
            }
        }
        #endregion
    }
}