using UnityEngine;

/// <summary>
/// Controls _GradientIndex property per material slot independently.
/// Essential for Timeline animation when multiple materials share the same gradient system.
/// Detects materials with _GradientIndex and exposes individual sliders for each.
/// </summary>
/// <remarks>
/// USAGE:
/// - REQUIRED for 2+ materials: Allows independent gradient animation per material slot
/// - OPTIONAL for 1 material: Provides a convenient slider (can also animate directly via Renderer > Material)
/// 
/// Before using this component, ensure you've called "Prepare for Timeline" on PotionTextureSetup.
/// </remarks>
[ExecuteInEditMode]
[RequireComponent(typeof(Renderer))]
public class MaterialIndexController : MonoBehaviour
{
    [Header("Material Indices (Animate These in Timeline)")]
    [Tooltip("Gradient index for material slot 0. Animatable in Timeline.")]
    [Range(0, 109)] public float indexMaterial0 = 0f;

    [Tooltip("Gradient index for material slot 1. Animatable in Timeline.")]
    [Range(0, 109)] public float indexMaterial1 = 0f;

    [Tooltip("Gradient index for material slot 2. Animatable in Timeline.")]
    [Range(0, 109)] public float indexMaterial2 = 0f;

    [Tooltip("Gradient index for material slot 3. Animatable in Timeline.")]
    [Range(0, 109)] public float indexMaterial3 = 0f;

    [Header("Info")]
    [SerializeField] private int detectedMaterialsWithIndex = 0;

    private static readonly int GradientIndexID = Shader.PropertyToID("_GradientIndex");
    private Renderer rend;
    private MaterialPropertyBlock mpb;
    private float[] lastIndices = new float[4];
    private bool usesMaterialInstances = false;
    private bool hasReadInitialValues = false;

    void Awake()
    {
        Initialize();
    }

    void OnEnable()
    {
        Initialize();

        // Validate before proceeding
        if (!ValidateComponents())
        {
            enabled = false;
            return;
        }

        DetectMaterialsWithIndex();
        CheckIfUsingInstances();
        ReadCurrentIndicesFromMaterials();

        // Info message when component is first added
        if (!hasReadInitialValues && Application.isPlaying)
        {
            if (detectedMaterialsWithIndex > 0)
            {
                Debug.Log($"[MaterialIndexController] Detected {detectedMaterialsWithIndex} material(s) with _GradientIndex on '{gameObject.name}'. Current values loaded.", this);
            }
            else
            {
                Debug.LogWarning($"[MaterialIndexController] No materials with _GradientIndex found on '{gameObject.name}'. " +
                                "This component will have no effect. Use a compatible shader (e.g., Liquid_Effect, Stylized_Tint).", this);
            }
        }

        ApplyIndices();
    }

    void Start()
    {
        DetectMaterialsWithIndex();
        CheckIfUsingInstances();
        ReadCurrentIndicesFromMaterials();
        ApplyIndices();
    }

    void OnValidate()
    {
        if (rend == null) rend = GetComponent<Renderer>();
        DetectMaterialsWithIndex();
        CheckIfUsingInstances();
        // Don't read values in OnValidate - user might be editing sliders
    }

    void Update()
    {
        // Only update if values changed (performance optimization)
        if (HasChanged())
        {
            ApplyIndices();
        }
    }

    /// <summary>
    /// Validates that all required components are present and properly configured.
    /// </summary>
    /// <returns>True if valid, false otherwise.</returns>
    bool ValidateComponents()
    {
        if (rend == null)
        {
            Debug.LogError($"[MaterialIndexController] Renderer component missing on '{gameObject.name}'. " +
                          "Cannot control material indices without a Renderer.", this);
            return false;
        }

        if (rend.sharedMaterials == null || rend.sharedMaterials.Length == 0)
        {
            Debug.LogWarning($"[MaterialIndexController] Renderer on '{gameObject.name}' has no materials assigned. " +
                            "Assign materials to use gradient index control.", this);
            return false;
        }

        return true;
    }

    /// <summary>
    /// Initializes component references and MaterialPropertyBlock with GPU instancing enabled.
    /// </summary>
    void Initialize()
    {
        if (rend == null)
            rend = GetComponent<Renderer>();

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

    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 DetectMaterialsWithIndex()
    {
        if (rend == null) return;

        detectedMaterialsWithIndex = 0;
        Material[] materials = rend.sharedMaterials;

        for (int i = 0; i < materials.Length && i < 4; i++)
        {
            if (materials[i] != null && materials[i].HasProperty(GradientIndexID))
            {
                detectedMaterialsWithIndex++;
            }
        }
    }

    /// <summary>
    /// Reads current gradient index values from materials and syncs controller values.
    /// Only reads once during initialization to avoid overwriting user changes.
    /// </summary>
    void ReadCurrentIndicesFromMaterials()
    {
        if (rend == null) return;
        if (hasReadInitialValues) return; // Only read once on first initialization

        Material[] materials = rend.sharedMaterials;

        // Read current index values from materials
        for (int i = 0; i < materials.Length && i < 4; i++)
        {
            if (materials[i] == null) continue;
            if (!materials[i].HasProperty(GradientIndexID)) continue;

            float currentValue = materials[i].GetFloat(GradientIndexID);

            // Set the controller values to match current material values
            switch (i)
            {
                case 0: indexMaterial0 = currentValue; break;
                case 1: indexMaterial1 = currentValue; break;
                case 2: indexMaterial2 = currentValue; break;
                case 3: indexMaterial3 = currentValue; break;
            }
        }

        hasReadInitialValues = true;
    }

    /// <summary>
    /// Checks if any index value has changed since last update.
    /// </summary>
    /// <returns>True if any value changed, false otherwise.</returns>
    bool HasChanged()
    {
        return !Mathf.Approximately(indexMaterial0, lastIndices[0]) ||
               !Mathf.Approximately(indexMaterial1, lastIndices[1]) ||
               !Mathf.Approximately(indexMaterial2, lastIndices[2]) ||
               !Mathf.Approximately(indexMaterial3, lastIndices[3]);
    }

    /// <summary>
    /// Applies gradient index values to materials.
    /// Uses Material Instances (direct write) or MaterialPropertyBlock depending on current mode.
    /// </summary>
    void ApplyIndices()
    {
        if (rend == null) return;

        Material[] materials = rend.sharedMaterials;
        float[] currentIndices = { indexMaterial0, indexMaterial1, indexMaterial2, indexMaterial3 };

        if (usesMaterialInstances)
        {
            // Write directly to material instances
            for (int i = 0; i < materials.Length && i < 4; i++)
            {
                if (materials[i] == null) continue;
                if (!materials[i].HasProperty(GradientIndexID)) continue;

                materials[i].SetFloat(GradientIndexID, currentIndices[i]);
                lastIndices[i] = currentIndices[i];
            }
        }
        else
        {
            // ⚡ GPU INSTANCING: Use MPB with instancing enabled
            if (mpb == null) return;

            for (int i = 0; i < materials.Length && i < 4; i++)
            {
                if (materials[i] == null) continue;
                if (!materials[i].HasProperty(GradientIndexID)) continue;

                rend.GetPropertyBlock(mpb, i);
                mpb.SetFloat(GradientIndexID, currentIndices[i]);
                rend.SetPropertyBlock(mpb, i);

                lastIndices[i] = currentIndices[i];
            }
        }
    }

    /// <summary>
    /// Sets the gradient index for a specific material slot.
    /// </summary>
    /// <param name="materialSlot">The material slot index (0-3).</param>
    /// <param name="value">The gradient index value (0-109).</param>
    /// <remarks>
    /// Use this method to programmatically change gradient indices at runtime.
    /// The value is automatically clamped to the valid range (0-109).
    /// </remarks>
    /// <example>
    /// <code>
    /// // Set gradient index 45 for material slot 0
    /// materialIndexController.SetIndex(0, 45f);
    /// </code>
    /// </example>
    public void SetIndex(int materialSlot, float value)
    {
        value = Mathf.Clamp(value, 0, 109);

        switch (materialSlot)
        {
            case 0: indexMaterial0 = value; break;
            case 1: indexMaterial1 = value; break;
            case 2: indexMaterial2 = value; break;
            case 3: indexMaterial3 = value; break;
        }

        ApplyIndices();
    }

    /// <summary>
    /// Gets the current gradient index for a specific material slot.
    /// </summary>
    /// <param name="materialSlot">The material slot index (0-3).</param>
    /// <returns>The gradient index value for the specified slot.</returns>
    /// <example>
    /// <code>
    /// float currentIndex = materialIndexController.GetIndex(0);
    /// </code>
    /// </example>
    public float GetIndex(int materialSlot)
    {
        switch (materialSlot)
        {
            case 0: return indexMaterial0;
            case 1: return indexMaterial1;
            case 2: return indexMaterial2;
            case 3: return indexMaterial3;
            default: return 0f;
        }
    }

    /// <summary>
    /// Refreshes 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);
            }
        }

        // Read values again after switching to instances
        hasReadInitialValues = false;
        ReadCurrentIndicesFromMaterials();

        ApplyIndices();
    }
}