// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Microsoft.MixedReality.Toolkit.UI
{
    /// <summary>
    /// Base abstract class for all Theme Engines. Extend to create custom Theme logic
    /// </summary>
    public abstract class InteractableThemeBase
    {
        /// <summary>
        /// Types of component this Theme Engine will target on the initialized GameObject or related GameObjects
        /// </summary>
        public Type[] Types { get; protected set; } = Array.Empty<Type>();

        /// <summary>
        /// Name of Theme Engine
        /// </summary>
        public string Name { get; protected set; } = "Base Theme";

        /// <summary>
        /// List of Properties with values per state
        /// </summary>
        public List<ThemeStateProperty> StateProperties { get; set; } = new List<ThemeStateProperty>();

        /// <summary>
        /// List of global Theme Engine properties
        /// </summary>
        public List<ThemeProperty> Properties { get; set; } = new List<ThemeProperty>();

        /// <summary>
        /// GameObject initialized with this ThemeEngine and being targeted based on state changes
        /// </summary>
        public GameObject Host { get; set; }

        /// <summary>
        /// Defines how to ease between values during state changes
        /// </summary>
        public Easing Ease { get; set; } = new Easing();

        /// <summary>
        /// True if Theme Engine has been initialized, false otherwise
        /// </summary>
        public bool Loaded { get; protected set; } = false;

        /// <summary>
        /// Indicates whether the current Theme engine implementation supports easing between state values
        /// </summary>
        public virtual bool IsEasingSupported => true;

        /// <summary>
        /// Indicates whether the current Theme engine implementation supports shader targeting on state properties
        /// </summary>
        public virtual bool AreShadersSupported => false;

        /// <summary>
        /// Instruct theme to set value for current property with given index state and at given lerp percentage
        /// </summary>
        /// <param name="property">property to update value</param>
        /// <param name="index">index of state to access array of values</param>
        /// <param name="percentage">percentage transition between values</param>
        public abstract void SetValue(ThemeStateProperty property, int index, float percentage);

        /// <summary>
        /// Get the current property value for the provided state property
        /// </summary>
        /// <param name="property">state property to access</param>
        /// <returns>Value currently for given state property</returns>
        public abstract ThemePropertyValue GetProperty(ThemeStateProperty property);

        /// <summary>
        /// Generates the default theme definition configuration for the current theme implementation
        /// </summary>
        /// <returns>Default ThemeDefinition to initialize with the current theme engine implementation</returns>
        public abstract ThemeDefinition GetDefaultThemeDefinition();

        /// <summary>
        /// Instruct theme to set value for current property with ThemePropertyValue value provided
        /// </summary>
        /// <param name="property">property to update value</param>
        /// <param name="value">Value for theme to set</param>
        protected abstract void SetValue(ThemeStateProperty property, ThemePropertyValue value);

        protected Dictionary<ThemeStateProperty, ThemePropertyValue> originalStateValues = new Dictionary<ThemeStateProperty, ThemePropertyValue>();

        private bool hasFirstState = false;
        private int lastState = -1;

        /// <summary>
        /// Helper method to instantiate a Theme Engine of provided type. Type must extend InteractableThemeBase
        /// </summary>
        /// <param name="themeType">Type of ThemeEngine to create</param>
        /// <returns>Instance of ThemeEngine of given type</returns>
        public static InteractableThemeBase CreateTheme(Type themeType)
        {
            if (!themeType.IsSubclassOf(typeof(InteractableThemeBase)))
            {
                Debug.LogError($"Trying to initialize theme of type {themeType} but type does not extend {typeof(InteractableThemeBase)}");
                return null;
            }

            return (InteractableThemeBase)Activator.CreateInstance(themeType);
        }

        /// <summary>
        /// Helper method to create and initialize a Theme Engine for given configuration and targeted GameObject
        /// </summary>
        /// <param name="definition">Theme configuration with type information and properties to initialize ThemeEngine with</param>
        /// <param name="host">GameObject for Theme Engine to target</param>
        /// <returns>Instance of Theme Engine initialized</returns>
        public static InteractableThemeBase CreateAndInitTheme(ThemeDefinition definition, GameObject host = null)
        {
            var theme = CreateTheme(definition.ThemeType);
            theme.Init(host, definition);
            return theme;
        }

        /// <summary>
        /// Initialize current Theme Engine with given configuration and target the provided GameObject
        /// </summary>
        /// <param name="host">GameObject to target changes against</param>
        /// <param name="definition">Configuration information to initialize Theme Engine</param>
        public virtual void Init(GameObject host, ThemeDefinition definition)
        {
            Host = host;

            StateProperties = new List<ThemeStateProperty>();
            foreach (ThemeStateProperty stateProp in definition.StateProperties)
            {
                // This is a temporary workaround to support backward compatible themes
                // If the current state properties is one we know supports shaders, try to migrate data
                // See ThemeStateProperty class for more details
                if (ThemeStateProperty.IsShaderPropertyType(stateProp.Type))
                {
                    stateProp.MigrateShaderData();
                }

                StateProperties.Add(new ThemeStateProperty()
                {
                    Name = stateProp.Name,
                    Type = stateProp.Type,
                    Values = stateProp.Values,
                    Default = stateProp.Default,
                    TargetShader = stateProp.TargetShader,
                    ShaderPropertyName = stateProp.ShaderPropertyName,
                });

                originalStateValues.Add(stateProp, GetProperty(stateProp));
            }

            Properties = new List<ThemeProperty>();
            foreach (ThemeProperty prop in definition.CustomProperties)
            {
                Properties.Add(new ThemeProperty()
                {
                    Name = prop.Name,
                    Tooltip = prop.Tooltip,
                    Type = prop.Type,
                    Value = prop.Value,
                });
            }

            Debug.Assert(GetDefaultThemeDefinition().StateProperties.Count == StateProperties.Count, $"{Name}  state properties inconsistency with default theme definition, consider reserializing.");
            Debug.Assert(GetDefaultThemeDefinition().CustomProperties.Count == Properties.Count, $"{Name}  custom properties inconsistency with default theme definition, consider reserializing.");

            if (definition.Easing != null)
            {
                Ease = definition.Easing.Copy();
                Ease.Stop();
            }

            Loaded = true;
        }

        /// <summary>
        /// Resets properties on Host GameObject to their original values when Init() was called for this theme engine.
        /// Useful for reverting changes done by this theme engine.
        /// </summary>
        public virtual void Reset()
        {
            foreach (var originalState in originalStateValues)
            {
                SetValue(originalState.Key, originalState.Value);
            }
        }

        /// <summary>
        /// Update ThemeEngine for given state based on Theme logic. Check, sets, and possibly eases values based on given state
        /// </summary>
        /// <param name="state">current state to target</param>
        /// <param name="force">force update call even if state is not new</param>
        public virtual void OnUpdate(int state, bool force = false)
        {
            if (state != lastState || force)
            {
                int themePropCount = StateProperties.Count;
                for (int i = 0; i < themePropCount; i++)
                {
                    ThemeStateProperty current = StateProperties[i];
                    current.StartValue = GetProperty(current);
                    if (hasFirstState || force)
                    {
                        Ease.Start();
                        SetValue(current, state, Ease.GetCurved());
                        hasFirstState = true;
                    }
                    else
                    {
                        SetValue(current, state, 1);
                        if (i >= themePropCount - 1)
                        {
                            hasFirstState = true;
                        }
                    }
                    StateProperties[i] = current;
                }

                lastState = state;
            }
            else if (Ease.Enabled && Ease.IsPlaying())
            {
                Ease.OnUpdate();
                int themePropCount = StateProperties.Count;
                for (int i = 0; i < themePropCount; i++)
                {
                    ThemeStateProperty current = StateProperties[i];
                    SetValue(current, state, Ease.GetCurved());
                }
            }

            lastState = state;
        }

        protected float LerpFloat(float s, float e, float t)
        {
            return (e - s) * t + s;
        }

        protected int LerpInt(int s, int e, float t)
        {
            return Mathf.RoundToInt((e - s) * t) + s;
        }

        protected ThemeProperty GetThemeProperty(int index)
        {
            if (index >= 0)
            {
                if (Properties != null && Properties.Count > index)
                {
                    return Properties[index];
                }

                // If Theme's data does not have desired property, return defaults.
                // If index is not in range, throw exception to fail fast. 
                return GetDefaultThemeDefinition().CustomProperties[index];
            }

            return null;
        }
    }
}
