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

namespace Plugins.Animate_UI_Materials
{
  /// <summary>
  ///   Used in combination with GraphicMaterialOverride to modify and animate shader properties
  ///   The base class is used for the shared Editor script, and reacting to OnValidate events
  /// </summary>
  [ExecuteAlways]
  public abstract class GraphicPropertyOverride : MonoBehaviour, IMaterialPropertyModifier
  {
    /// <summary>
    /// The name of the shader property, serialized
    /// </summary>
    [SerializeField] protected string propertyName;

    public virtual string DisplayName => propertyName;

    /// <summary>
    /// The id of the shader property
    /// </summary>
    /// <remarks> Should not be serialized, as it can change between game runs</remarks>
    protected int PropertyId;

    // Request a material update whenever the parent changes
    void OnEnable()
    {
      SetMaterialDirty(true);
    }

    void OnDisable()
    {
      SetMaterialDirty();
    }

#if UNITY_EDITOR // if in the unity editor, include unity editor callbacks
    /// <summary>
    /// On editor change, mark as dirty
    /// </summary>
    void OnValidate()
    {
      SetMaterialDirty(true);
    }
#endif

    /// <summary>
    /// Try to retrieve and apply the default property value
    /// If the source material cannot be found, reset to sensible defaults
    /// </summary>
    public abstract void ResetPropertyToDefault();

    /// <summary>
    /// Set the material as dirty
    /// ApplyModifiedProperty will be called by the parent GraphicMaterialProperty
    /// </summary>
    /// <param name="renewId">If the GraphicPropertyOverride should try to get the shader property id, just to be safe</param>
    public void SetMaterialDirty(bool renewId = false)
    {
      if (renewId || PropertyId == 0) PropertyId = Shader.PropertyToID(propertyName);
      GraphicMaterialOverride parent = ParentOverride;
      if (parent) parent.SetMaterialDirty();
    }

    /// <summary>
    /// Try to apply the GraphicPropertyOverride property to the material
    /// Does not create a copy, only feed material instances to this
    /// </summary>
    /// <param name="material">The material to modify</param>
    public abstract void ApplyModifiedProperty(Material material);

    /// <summary>
    /// The name of the shader property to override
    /// Can be invalid if the shader has changed, or if the component was not setup
    /// </summary>
    public string PropertyName
    {
      get => propertyName;
      set
      {
        propertyName = value;
        SetMaterialDirty(true);
      }
    }

    /// <summary>
    ///   Try to get the Graphic component on the parent
    /// </summary>
    protected Graphic ParentGraphic => transform.parent ? transform.parent.GetComponent<Graphic>() : null;

    /// <summary>
    ///   Try to get the GraphicMaterialOverride component on the parent
    /// </summary>
    protected GraphicMaterialOverride ParentOverride =>
      transform.parent ? transform.parent.GetComponent<GraphicMaterialOverride>() : null;
  }

  /// <summary>
  /// Template extension of GraphicPropertyOverride
  /// Adds LateUpdate function to react to value changes
  /// Compares value changes using EqualityComparer
  /// Adds a SerializedField of type T
  /// </summary>
  /// <typeparam name="T"></typeparam>
  public abstract class GraphicPropertyOverride<T> : GraphicPropertyOverride
  {
    /// <summary>
    /// The serialized value, modified by the inspector or the animator
    /// </summary>
    [SerializeField] protected T propertyValue;

    /// <summary>
    /// The last known value, init the the type default (0, null, ...)
    /// Used to check for changes
    /// NonSerialized to prevent unity from serializing this
    /// </summary>
    [NonSerialized] T _previousValue;

    /// <summary>
    ///   If _previousValue was set since last construction
    /// </summary>
    [NonSerialized] bool _previousValueIsInit;

    /// <summary>
    /// Checks if any changes happened just before rendering
    /// Can be removed to optimize, since OnDidApplyAnimationProperties is doing the heavy lifting
    /// But OnDidApplyAnimationProperties is undocumented, and will potentially change silently in the future
    /// </summary>
    void LateUpdate()
    {
      // If a previous value was recorded
      // And it perfectly matches the current value
      // Then ignore this update
      if (_previousValueIsInit && EqualityComparer<T>.Default.Equals(propertyValue, _previousValue)) return;

      _previousValueIsInit = true;
      _previousValue = propertyValue;
      SetMaterialDirty();
    }

    /// <summary>
    /// Called by the animator system when a value is modified
    /// </summary>
    public void OnDidApplyAnimationProperties()
    {
      _previousValueIsInit = true;
      _previousValue = propertyValue;
      SetMaterialDirty();
    }

    /// <summary>
    /// The value of the overriding property
    /// Will react correctly when changed
    /// </summary>
    public T PropertyValue
    {
      get => propertyValue;
      set
      {
        _previousValueIsInit = true;
        _previousValue = propertyValue = value;
        SetMaterialDirty();
      }
    }

    /// <summary>
    /// Try to retrieve and apply the default property value
    /// If the source material cannot be found, reset to sensible defaults
    /// </summary>
    public override void ResetPropertyToDefault()
    {
      // Try to get the associated Graphic component
      Graphic graphic = ParentGraphic;
      // If successful, get the material
      Material material = graphic ? graphic.material : null;
      // init the reset value to default
      T value = default;
      bool gotDefaultValue = false;

      // If material was received, try to get the default value from the material
      if (material) gotDefaultValue = GetDefaultValue(material, out value);

      // Log a warning if we failed
      if (!gotDefaultValue) Debug.LogWarning("Could not retrieve material default value", this);

      // Set current value to what we managed to retrieve, and update
      PropertyValue = value;
    }

    /// <summary>
    /// Retrieve the default property value from the source material
    /// </summary>
    /// <param name="material">The source material</param>
    /// <param name="defaultValue">The value from the material</param>
    /// <returns>True if the value could be retrieved</returns>
    public abstract bool GetDefaultValue(Material material, out T defaultValue);
  }
}