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

/// <summary>
/// Simulates liquid physics with wobble effects based on object movement and rotation.
/// Supports both Material Property Blocks (for shared materials) and Material Instances (for Timeline animation).
/// </summary>
[ExecuteInEditMode]
[RequireComponent(typeof(MeshFilter), typeof(Renderer))]
public class Liquid : MonoBehaviour
{
    /// <summary>
    /// Determines which time source to use for wobble calculations.
    /// </summary>
    public enum UpdateMode
    {
        /// <summary>Normal game time (affected by Time.timeScale)</summary>
        Normal,
        /// <summary>Unscaled time (ignores Time.timeScale, useful for pause menus)</summary>
        UnscaledTime
    }

    public UpdateMode updateMode;

    [Header("Wobble Settings")]
    [Tooltip("Maximum wobble intensity. Higher values = more aggressive liquid movement.")]
    [Range(0f, 0.4f)] [SerializeField] float MaxWobble = 0.03f;

    [Tooltip("Speed of wobble oscillation. Higher values = faster wobbling.")]
    [Range(0f, 1.5f)] [SerializeField] float WobbleSpeedMove = 1f;

    [Tooltip("How quickly the wobble returns to rest. Higher values = faster damping.")]
    [Range(0f, 2f)] [SerializeField] float Recovery = 1f;

    [Tooltip("Minimum velocity magnitude required to trigger wobble. Acts as a threshold.")]
    [Range(0f, 5f)] [SerializeField] float Thickness = 1f;

    [Header("Fill Settings")]
    [Tooltip("Current fill level (0 = empty, 1 = full). Can be animated.")]
    [Range(0f, 1f)] public float fillAmount = 0.5f;

    [Tooltip("Minimum fill value when fillAmount is 0.")]
    [Range(0f, 1f)] public float minFill = 0f;

    [Tooltip("Maximum fill value when fillAmount is 1.")]
    [Range(0f, 1f)] public float maxFill = 1f;

    [Tooltip("Compensates for mesh shape. 0 = uses pivot, 1 = uses lowest vertex point.")]
    [Range(0, 1)] public float CompensateShapeAmount;

    [Header("References")]
    [SerializeField] Mesh mesh;
    [SerializeField] Renderer rend;

    Vector3 pos;
    Vector3 lastPos;
    Vector3 velocity;
    Quaternion lastRot;
    Vector3 angularVelocity;
    float wobbleAmountX;
    float wobbleAmountZ;
    float wobbleAmountToAddX;
    float wobbleAmountToAddZ;
    float pulse;
    float sinewave;
    double timeOffset; // Double for better precision across platforms
    Vector3 comp;

    MaterialPropertyBlock mpb;

    private static readonly int GrayscaleTexID = Shader.PropertyToID("_GrayscaleTex");
    private static readonly int WobbleXID = Shader.PropertyToID("_WobbleX");
    private static readonly int WobbleZID = Shader.PropertyToID("_WobbleZ");
    private static readonly int FillAmountID = Shader.PropertyToID("_FillAmount");
    private static readonly int GradientIndexID = Shader.PropertyToID("_GradientIndex");

    private Texture2D cachedGrayscaleTexture;
    private bool grayscaleTextureNeedsUpdate = false;
    private bool usesMaterialInstances = false;

    // ⚡ OPTIMIZATION: Cache the lowest point of the mesh (constant value)
    private float cachedLowestPoint;
    private bool lowestPointCached = false;

    void Start()
    {
        // Validate components before initialization
        if (!ValidateComponents())
        {
            enabled = false;
            return;
        }

        GetMeshAndRend();

        // ⚡ GPU INSTANCING: Works automatically with MPB if shader has instancing enabled
        mpb = new MaterialPropertyBlock();

        CheckIfUsingInstances();
        CacheLowestPoint();

        // Initialize time offset for consistent behavior across platforms
        timeOffset = (updateMode == UpdateMode.UnscaledTime) ? Time.unscaledTime : Time.time;

        // ⚡ FIX: Limit framerate for consistent physics across platforms
        Application.targetFrameRate = 60; // Force 60 FPS everywhere
        QualitySettings.vSyncCount = 0;   // Disable VSync to use targetFrameRate
    }

    private void OnValidate()
    {
        GetMeshAndRend();
        if (mpb == null)
        {
            mpb = new MaterialPropertyBlock();
        }
        CheckIfUsingInstances();

        // Recalculate if mesh changes in editor
        lowestPointCached = false;
    }

    /// <summary>
    /// Validates that all required components are present.
    /// Logs clear errors if components are missing.
    /// </summary>
    /// <returns>True if all required components exist, false otherwise.</returns>
    bool ValidateComponents()
    {
        bool isValid = true;

        MeshFilter meshFilter = GetComponent<MeshFilter>();
        if (meshFilter == null)
        {
            Debug.LogError($"[Liquid] MeshFilter component missing on '{gameObject.name}'. " +
                          "Add a MeshFilter component to use Liquid script.", this);
            isValid = false;
        }
        else if (meshFilter.sharedMesh == null)
        {
            Debug.LogError($"[Liquid] MeshFilter on '{gameObject.name}' has no mesh assigned. " +
                          "Assign a mesh to the MeshFilter component.", this);
            isValid = false;
        }

        Renderer renderer = GetComponent<Renderer>();
        if (renderer == null)
        {
            Debug.LogError($"[Liquid] Renderer component missing on '{gameObject.name}'. " +
                          "Add a Renderer component (MeshRenderer, SkinnedMeshRenderer, etc.).", this);
            isValid = false;
        }
        else if (renderer.sharedMaterials == null || renderer.sharedMaterials.Length == 0)
        {
            Debug.LogWarning($"[Liquid] Renderer on '{gameObject.name}' has no materials assigned. " +
                            "Assign at least one material for the liquid effect to work.", this);
        }

        return isValid;
    }

    void CheckIfUsingInstances()
    {
        if (rend == null) return;

        Material[] materials = rend.sharedMaterials;

        // Check if ANY material is an instance
        usesMaterialInstances = false;
        foreach (var mat in materials)
        {
            if (mat != null && mat.name.Contains("(Instance)"))
            {
                usesMaterialInstances = true;
                break;
            }
        }
    }

    void GetMeshAndRend()
    {
        if (mesh == null)
        {
            MeshFilter mf = GetComponent<MeshFilter>();
            if (mf != null) mesh = mf.sharedMesh;
        }
        if (rend == null)
            rend = GetComponent<Renderer>();
    }

    /// <summary>
    /// ⚡ OPTIMIZATION: Calculates the lowest point of the mesh ONCE and caches it.
    /// This value is constant for a given mesh and doesn't need recalculation every frame.
    /// For a mesh with 500 vertices at 60 FPS, this saves 1.8 million iterations per minute.
    /// </summary>
    void CacheLowestPoint()
    {
        if (mesh == null || lowestPointCached) return;

        float lowestY = float.MaxValue;
        Vector3 lowestVert = Vector3.zero;
        Vector3[] vertices = mesh.vertices;

        for (int i = 0; i < vertices.Length; i++)
        {
            Vector3 position = transform.TransformPoint(vertices[i]);
            if (position.y < lowestY)
            {
                lowestY = position.y;
                lowestVert = position;
            }
        }

        cachedLowestPoint = lowestVert.y;
        lowestPointCached = true;
    }

    /// <summary>
    /// Returns the cached lowest point of the mesh in world space.
    /// If not cached, calculates and caches it first.
    /// </summary>
    /// <returns>The Y position of the lowest vertex in world space.</returns>
    float GetLowestPoint()
    {
        if (!lowestPointCached)
        {
            CacheLowestPoint();
        }
        return cachedLowestPoint;
    }

    void Update()
    {
        float deltaTime = 0;
        float currentTime = 0;

        switch (updateMode)
        {
            case UpdateMode.Normal:
                deltaTime = Time.deltaTime;
                currentTime = (float)(Time.time - timeOffset);
                break;
            case UpdateMode.UnscaledTime:
                deltaTime = Time.unscaledDeltaTime;
                currentTime = (float)(Time.unscaledTime - timeOffset);
                break;
        }

        if (deltaTime != 0)
        {
            wobbleAmountToAddX = Mathf.Lerp(wobbleAmountToAddX, 0, (deltaTime * Recovery));
            wobbleAmountToAddZ = Mathf.Lerp(wobbleAmountToAddZ, 0, (deltaTime * Recovery));

            pulse = 2 * Mathf.PI * WobbleSpeedMove;
            // ⚡ FIX: Use Time.time instead of manual accumulation for platform consistency
            sinewave = Mathf.Lerp(sinewave, Mathf.Sin(pulse * currentTime),
                deltaTime * Mathf.Clamp(velocity.magnitude + angularVelocity.magnitude, Thickness, 10));

            wobbleAmountX = wobbleAmountToAddX * sinewave;
            wobbleAmountZ = wobbleAmountToAddZ * sinewave;

            velocity = (lastPos - transform.position) / deltaTime;
            angularVelocity = GetAngularVelocity(lastRot, transform.rotation);

            wobbleAmountToAddX += Mathf.Clamp((velocity.x + (velocity.y * 0.2f) + angularVelocity.z + angularVelocity.y) * MaxWobble, -MaxWobble, MaxWobble);
            wobbleAmountToAddZ += Mathf.Clamp((velocity.z + (velocity.y * 0.2f) + angularVelocity.x + angularVelocity.y) * MaxWobble, -MaxWobble, MaxWobble);
        }

        UpdatePos(deltaTime);

        if (usesMaterialInstances)
            ApplyToMaterialInstances();
        else
            ApplyToMaterialPropertyBlock();

        lastPos = transform.position;
        lastRot = transform.rotation;
    }

    void ApplyToMaterialInstances()
    {
        if (rend == null) return;

        Material[] materials = rend.sharedMaterials;

        // Write directly to material instances (not MPB)
        // This allows Timeline to animate other properties freely
        for (int i = 0; i < materials.Length; i++)
        {
            if (materials[i] == null) continue;

            if (materials[i].HasProperty(WobbleXID))
                materials[i].SetFloat(WobbleXID, wobbleAmountX);

            if (materials[i].HasProperty(WobbleZID))
                materials[i].SetFloat(WobbleZID, wobbleAmountZ);

            if (materials[i].HasProperty(FillAmountID))
                materials[i].SetVector(FillAmountID, pos);

            if (cachedGrayscaleTexture != null && materials[i].HasProperty(GrayscaleTexID))
            {
                materials[i].SetTexture(GrayscaleTexID, cachedGrayscaleTexture);
            }
        }

        grayscaleTextureNeedsUpdate = false;
    }

    void ApplyToMaterialPropertyBlock()
    {
        if (rend == null || mpb == null) return;

        Material[] materials = rend.sharedMaterials;

        // ⚡ GPU INSTANCING: Use MPB with instancing enabled for batching
        for (int i = 0; i < materials.Length; i++)
        {
            if (materials[i] == null) continue;

            rend.GetPropertyBlock(mpb, i);

            if (materials[i].HasProperty(WobbleXID))
                mpb.SetFloat(WobbleXID, wobbleAmountX);

            if (materials[i].HasProperty(WobbleZID))
                mpb.SetFloat(WobbleZID, wobbleAmountZ);

            if (materials[i].HasProperty(FillAmountID))
                mpb.SetVector(FillAmountID, pos);

            if (!Application.isPlaying || grayscaleTextureNeedsUpdate)
            {
                if (cachedGrayscaleTexture != null && materials[i].HasProperty(GrayscaleTexID))
                {
                    mpb.SetTexture(GrayscaleTexID, cachedGrayscaleTexture);
                }
            }

            rend.SetPropertyBlock(mpb, i);
        }

        grayscaleTextureNeedsUpdate = false;
    }

    void UpdatePos(float deltaTime)
    {
        Vector3 worldPos = transform.TransformPoint(mesh.bounds.center);

        float normalizedFill = Mathf.Lerp(minFill, maxFill, fillAmount);
        float invertedFill = 1f - normalizedFill;

        if (CompensateShapeAmount > 0)
        {
            // ⚡ Now uses cached value - NO recalculation every frame
            if (deltaTime != 0)
                comp = Vector3.Lerp(comp, (worldPos - new Vector3(0, GetLowestPoint(), 0)), deltaTime * 10);
            else
                comp = (worldPos - new Vector3(0, GetLowestPoint(), 0));

            pos = worldPos - transform.position - new Vector3(0, invertedFill - (comp.y * CompensateShapeAmount), 0);
        }
        else
        {
            pos = worldPos - transform.position - new Vector3(0, invertedFill, 0);
        }
    }

    Vector3 GetAngularVelocity(Quaternion foreLastFrameRotation, Quaternion lastFrameRotation)
    {
        var q = lastFrameRotation * Quaternion.Inverse(foreLastFrameRotation);
        if (Mathf.Abs(q.w) > 1023.5f / 1024.0f) return Vector3.zero;

        // Use the deltaTime that respects UpdateMode
        float dt = (updateMode == UpdateMode.UnscaledTime) ? Time.unscaledDeltaTime : Time.deltaTime;
        if (dt == 0) return Vector3.zero; // Avoid division by zero

        float gain;
        if (q.w < 0.0f)
        {
            var angle = Mathf.Acos(-q.w);
            gain = -2.0f * angle / (Mathf.Sin(angle) * dt);
        }
        else
        {
            var angle = Mathf.Acos(q.w);
            gain = 2.0f * angle / (Mathf.Sin(angle) * dt);
        }
        Vector3 angularVelocity = new Vector3(q.x * gain, q.y * gain, q.z * gain);

        if (float.IsNaN(angularVelocity.z)) angularVelocity = Vector3.zero;
        return angularVelocity;
    }

    /// <summary>
    /// Sets the grayscale texture for LUT-based gradient coloring.
    /// The texture is used by the shader to map grayscale values to gradient colors.
    /// </summary>
    /// <param name="tex">The grayscale texture to apply. Should be a linear gradient from black to white.</param>
    /// <remarks>
    /// This method marks the texture as needing update and applies it in the next render cycle.
    /// The texture will be applied via MaterialPropertyBlock or Material Instance depending on the current mode.
    /// </remarks>
    public void SetGrayscale(Texture2D tex)
    {
        if (rend == null)
        {
            rend = GetComponent<Renderer>();
            if (rend == null)
            {
                Debug.LogWarning($"[Liquid] Cannot set grayscale texture on '{gameObject.name}': Renderer not found.", this);
                return;
            }
        }

        if (mpb == null)
        {
            mpb = new MaterialPropertyBlock();
        }

        cachedGrayscaleTexture = tex;
        grayscaleTextureNeedsUpdate = true;

        CheckIfUsingInstances();
    }

    /// <summary>
    /// Refreshes the instance detection and clears property blocks.
    /// Call this after using "Prepare for Timeline" to switch between MPB and Material Instance modes.
    /// </summary>
    /// <remarks>
    /// This method is called automatically by PotionTextureSetup when creating or removing material instances.
    /// You typically don't need to call this manually.
    /// </remarks>
    public void RefreshInstanceDetection()
    {
        CheckIfUsingInstances();

        // Clear any existing property blocks when switching modes
        if (rend != null)
        {
            Material[] materials = rend.sharedMaterials;
            for (int i = 0; i < materials.Length; i++)
            {
                rend.SetPropertyBlock(null, i);
            }
        }

        // Reapply grayscale if needed
        if (cachedGrayscaleTexture != null)
        {
            grayscaleTextureNeedsUpdate = true;
        }
    }
}