using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using PrimeTween;
using UnityEditor;
using UnityEngine;
using Assert = UnityEngine.Assertions.Assert;

// todo check all TweenType are covered in TweenTypeToTweenData()
internal class CodeGenerator : ScriptableObject {
    [SerializeField] MonoScript methodsScript;
    [SerializeField] MonoScript dotweenMethodsScript;
    [SerializeField] MonoScript tweenComponentScript;
    [SerializeField] MonoScript editorUtilsScript;
    [SerializeField] MethodGenerationData[] methodsData;
    [SerializeField] AdditiveMethodsGenerator additiveMethodsGenerator;
    [SerializeField] SpeedBasedMethodsGenerator speedBasedMethodsGenerator;

    /*void OnEnable() {
        #if PRIME_TWEEN_SAFETY_CHECKS
        foreach (var data in methodsData) {
            data.description = "";
            var methodPrefix = getMethodPrefix(data.dependency);
            if (!string.IsNullOrEmpty(methodPrefix)) {
                data.description += methodPrefix + "_";
            }
            data.description = data.methodName + "_" + getTypeByName(data.targetType).Name;
        }
        #endif
    }*/

    [Serializable]
    class AdditiveMethodsGenerator {
        [SerializeField] AdditiveMethodsGeneratorData[] additiveMethods;

        [Serializable]
        class AdditiveMethodsGeneratorData {
            [SerializeField] internal string methodName;
            [SerializeField] internal PropType propertyType;
            [SerializeField] internal string setter;
        }

        [NotNull]
        internal string Generate() {
            string result = @"
#if PRIME_TWEEN_EXPERIMENTAL";
            foreach (var data in additiveMethods) {
                const string template = @"        
        public static Tween PositionAdditive([NotNull] UnityEngine.Transform target, Single deltaValue, float duration, Ease ease = Ease.Default, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => PositionAdditive(target, deltaValue, new TweenSettings(duration, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime));
        public static Tween PositionAdditive([NotNull] UnityEngine.Transform target, Single deltaValue, float duration, Easing ease, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => PositionAdditive(target, deltaValue, new TweenSettings(duration, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime));
        public static Tween PositionAdditive([NotNull] UnityEngine.Transform target, Single deltaValue, TweenSettings settings) 
            => CustomAdditive(target, deltaValue, settings, (_target, delta) => additiveTweenSetter());
";
                result += template.Replace("Single", data.propertyType.ToFullTypeName())
                    .Replace("PositionAdditive", data.methodName)
                    .Replace("additiveTweenSetter()", data.setter);
            }
            result += "#endif";
            return result;
        }
    }

    [ContextMenu(nameof(generateAllMethods))]
    internal void generateAllMethods() {
        generateMethods();
        generateDotweenMethods();
    }

    const string generatorBeginLabel = "// CODE GENERATOR BEGIN";

    [ContextMenu(nameof(generateTweenComponent))]
    void generateTweenComponent() {
        if (tweenComponentScript == null) {
            Debug.LogError("Not generating TweenComponent script because this component is only available in PrimeTween PRO.");
            return;
        }

        var str = tweenComponentScript.text;
        var searchIndex = str.IndexOf(generatorBeginLabel, StringComparison.Ordinal);
        Assert.AreNotEqual(-1, searchIndex);
        str = str.Substring(0, searchIndex + generatorBeginLabel.Length) + "\n";

        var tweenTypes = (TweenType[]) Enum.GetValues(typeof(TweenType));
        tweenTypes = tweenTypes.SkipWhile(x => x != TweenType.LightRange).ToArray();
        var generationData = methodsData
            .GroupBy(_ => _.dependency)
            .SelectMany(x => x)
            .SkipWhile(x => x.description != "Range_Light")
            .ToArray();

        Dependency dependency = Dependency.None;
        for (var i = 0; i < generationData.Length; i++) {
            var data = generationData[i];
            if (dependency != data.dependency) {
                if (shouldWrapInDefine(dependency)) {
                    str += "                    #endif\n";
                }
                dependency = data.dependency;
                if (shouldWrapInDefine(dependency)) {
                    switch (dependency) {
                        case Dependency.PRIME_TWEEN_EXPERIMENTAL:
                        case Dependency.UI_ELEMENTS_MODULE_INSTALLED:
                        case Dependency.TEXT_MESH_PRO_INSTALLED:
                            str += $"                    #if {dependency}\n";
                            break;
                        default:
                            str += $"                    #if !UNITY_2019_1_OR_NEWER || {dependency}\n";
                            break;
                    }
                }
            }
            var tweenType = tweenTypes[i];
            if (data.dependency == Dependency.UI_ELEMENTS_MODULE_INSTALLED || data.targetType.Contains("PrimeTween.")) { // skip both TweenTimeScale
                continue;
            }
            Assert.IsTrue(tweenType.ToString().Contains(data.methodName), $"{i}, {tweenType}, {data.methodName}");
            // Debug.Log(data.methodName);

            var template = @"                    case TweenType.Position:
                        return target is Transform positionTarget ? Tween.Position(positionTarget, settingsVector3) : (Tween?)null;";
            template = template.Replace("TweenType.Position", $"TweenType.{tweenType}");
            template = template.Replace("Transform", getTypeByName(data.targetType).Name);
            var varName = tweenType.ToString();
            varName = char.ToLower(varName[0]) + varName.Substring(1) + "Target";
            template = template.Replace("positionTarget", varName);
            template = template.Replace("Tween.Position", $"Tween.{getPrefix()}{data.methodName}");
            template = template.Replace("settingsVector3", $"settings{data.propertyType}");
            str += template + "\n";

            string getPrefix() => data.placeInGlobalScope ? null : getMethodPrefix(data.dependency);
        }
        if (shouldWrapInDefine(dependency)) {
            str += "                    #endif\n";
        }
        str += @"                    default:
                        throw new Exception();
                }
            }
        }
    }
}
";
        SaveScript(tweenComponentScript, str);
    }

    static void SaveScript(MonoScript script, string text) {
        var path = AssetDatabase.GetAssetPath(script);
        if (text == File.ReadAllText(path)) {
            return;
        }
        File.WriteAllText(path, text);
        EditorUtility.SetDirty(script);
        AssetDatabase.Refresh();
        AssetDatabase.SaveAssets();
    }

    void generateDotweenMethods() {
        // todo combine adapter files into one file
        var str = @"#if PRIME_TWEEN_DOTWEEN_ADAPTER
// This file is generated by CodeGenerator.cs
using JetBrains.Annotations;
using System;

namespace PrimeTween {
    [PublicAPI]
    public static partial class DOTweenAdapter {";
        const string dotweenOverload = "        public static Tween DOTWEEN_METHOD_NAME([NotNull] this UnityEngine.Camera target, Single endValue, float duration) => Tween.METHOD_NAME(target, endValue, duration);";
        str += generateWithDefines(data => {
            if (!data.dotweenMethodName.Any()) {
                return string.Empty;
            }
            Assert.IsTrue(data.dotweenMethodName.Any());
            string result = "";
            result += "\n";
            result += populateTemplate(dotweenOverload.Replace("DOTWEEN_METHOD_NAME", data.dotweenMethodName), data);
            return result;
        });
        str += @"
    }
}
#endif";
        SaveScript(dotweenMethodsScript, str);
    }

    [CanBeNull]
    static string getMethodPrefix(Dependency dep) {
        switch (dep) {
            case Dependency.UNITY_UGUI_INSTALLED:
                return "UI";
            case Dependency.AUDIO_MODULE_INSTALLED:
                return "Audio";
            case Dependency.PHYSICS_MODULE_INSTALLED:
            case Dependency.PHYSICS2D_MODULE_INSTALLED:
                return nameof(Rigidbody);
            case Dependency.None:
            case Dependency.PRIME_TWEEN_EXPERIMENTAL:
            case Dependency.UI_ELEMENTS_MODULE_INSTALLED:
            case Dependency.TEXT_MESH_PRO_INSTALLED:
                return null;
        }
        return dep.ToString();
    }

    static bool shouldWrapInDefine(Dependency d) {
        switch (d) {
            case Dependency.UNITY_UGUI_INSTALLED:
            case Dependency.AUDIO_MODULE_INSTALLED:
            case Dependency.PHYSICS_MODULE_INSTALLED:
            case Dependency.PHYSICS2D_MODULE_INSTALLED:
            case Dependency.PRIME_TWEEN_EXPERIMENTAL:
            case Dependency.UI_ELEMENTS_MODULE_INSTALLED:
            case Dependency.TEXT_MESH_PRO_INSTALLED:
                return true;
        }
        return false;
    }

    const string overloadTemplateTo = @"        public static Tween METHOD_NAME([NotNull] UnityEngine.Camera target, Single endValue, float duration, Ease ease = Ease.Default, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => METHOD_NAME(target, new TweenSettings<float>(endValue, new TweenSettings(duration, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)));
        public static Tween METHOD_NAME([NotNull] UnityEngine.Camera target, Single endValue, float duration, Easing ease, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => METHOD_NAME(target, new TweenSettings<float>(endValue, new TweenSettings(duration, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)));
        public static Tween METHOD_NAME([NotNull] UnityEngine.Camera target, Single startValue, Single endValue, float duration, Ease ease = Ease.Default, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => METHOD_NAME(target, new TweenSettings<float>(startValue, endValue, new TweenSettings(duration, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)));
        public static Tween METHOD_NAME([NotNull] UnityEngine.Camera target, Single startValue, Single endValue, float duration, Easing ease, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => METHOD_NAME(target, new TweenSettings<float>(startValue, endValue, new TweenSettings(duration, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)));
        public static Tween METHOD_NAME([NotNull] UnityEngine.Camera target, Single endValue, TweenSettings settings) => METHOD_NAME(target, new TweenSettings<float>(endValue, settings));
        public static Tween METHOD_NAME([NotNull] UnityEngine.Camera target, Single startValue, Single endValue, TweenSettings settings) => METHOD_NAME(target, new TweenSettings<float>(startValue, endValue, settings));";
    const string fullTemplate = @"        public static Tween METHOD_NAME([NotNull] UnityEngine.Camera target, TweenSettings<float> settings) {
            return animate(target, ref settings, _tween => {
                var _target = _tween.target as UnityEngine.Camera;
                var val = _tween.FloatVal;
                _target.orthographicSize = val;
            }, t => (t.target as UnityEngine.Camera).orthographicSize.ToContainer(), TweenType.CameraOrthographicSize);
        }";

    void generateMethods() {
        var text = methodsScript.text;
        var searchIndex = text.IndexOf(generatorBeginLabel, StringComparison.Ordinal);
        Assert.AreNotEqual(-1, searchIndex);
        text = text.Substring(0, searchIndex + generatorBeginLabel.Length) + "\n";

        var methodDataToEnumName = new Dictionary<MethodGenerationData, string>();
        { // generate enums
            foreach (var group in methodsData.GroupBy(_ => _.dependency)) {
                foreach (var data in group) {
                    text += "        ";
                    var enumName = GetTweenTypeEnumName(data);
                    methodDataToEnumName.Add(data, enumName);
                    text += enumName;
                    text += ",\n";
                }
            }
            text += "    }\n\n";
        }
        { // generate TweenTypeToTweenData()
            // todo combine Utils class with TweenGenerated or Extensions file
            string utilsText = @"using System;
using PrimeTween;

internal static class Utils {
    internal static (PropType, Type) TweenTypeToTweenData(TweenType tweenType) {
        switch (tweenType) {
";
            foreach (var group in methodsData.GroupBy(x => x.dependency)) {
                var dependency = group.Key;
                if (shouldWrapInDefine(dependency)) {
                    switch (dependency) {
                        case Dependency.PRIME_TWEEN_EXPERIMENTAL:
                        case Dependency.UI_ELEMENTS_MODULE_INSTALLED:
                        case Dependency.TEXT_MESH_PRO_INSTALLED:
                            utilsText += $"            #if {dependency}\n";
                            break;
                        default:
                            utilsText += $"            #if !UNITY_2019_1_OR_NEWER || {dependency}\n";
                            break;
                    }
                }
                foreach (var data in group) {
                    utilsText += $"            case TweenType.{methodDataToEnumName[data]}:\n";
                    utilsText += $"                return (PropType.{data.propertyType}, typeof({getTypeByName(data.targetType).FullName}));\n";
                }
                if (shouldWrapInDefine(dependency)) {
                    utilsText += "            #endif\n";
                }
            }
            var tweenData = new List<(TweenType, PropType, Type)> {
                (TweenType.None, PropType.None, null),
                #if PRIME_TWEEN_PRO
                (TweenType.TweenComponent, PropType.None, typeof(TweenComponent)),
                #endif
                (TweenType.Delay, PropType.Float, null),
                (TweenType.Callback, PropType.Float, null),
                (TweenType.ShakeLocalPosition, PropType.Vector3, typeof(Transform)),
                (TweenType.ShakeLocalRotation, PropType.Quaternion, typeof(Transform)),
                (TweenType.ShakeScale, PropType.Vector3, typeof(Transform)),
                (TweenType.ShakeCustom, PropType.Vector3, typeof(Transform)),
                (TweenType.ShakeCamera, PropType.Float, typeof(Camera)),
                (TweenType.CustomFloat, PropType.Float, null),
                (TweenType.CustomColor, PropType.Color, null),
                (TweenType.CustomVector2, PropType.Vector2, null),
                (TweenType.CustomVector3, PropType.Vector3, null),
                (TweenType.CustomVector4, PropType.Vector4, null),
                (TweenType.CustomQuaternion, PropType.Quaternion, null),
                (TweenType.CustomRect, PropType.Rect, null),
                (TweenType.CustomDouble, PropType.Double, null),
                (TweenType.MaterialColorProperty, PropType.Color, typeof(Material)),
                (TweenType.MaterialProperty, PropType.Float, typeof(Material)),
                (TweenType.MaterialAlphaProperty, PropType.Float, typeof(Material)),
                (TweenType.MaterialTextureOffset, PropType.Vector2, typeof(Material)),
                (TweenType.MaterialTextureScale, PropType.Vector2, typeof(Material)),
                (TweenType.MaterialPropertyVector4, PropType.Vector4, typeof(Material)),
                (TweenType.EulerAngles, PropType.Vector3, typeof(Transform)),
                (TweenType.LocalEulerAngles, PropType.Vector3, typeof(Transform)),
                (TweenType.GlobalTimeScale, PropType.Float, null),
                (TweenType.MainSequence, PropType.Float, null),
                (TweenType.NestedSequence, PropType.Float, null)
            };
            foreach (var tuple in tweenData) {
                utilsText += $"            case TweenType.{tuple.Item1}:\n";
                string typeStr = tuple.Item3 == null ? "null" : $"typeof({tuple.Item3})";
                utilsText += $"                return (PropType.{tuple.Item2}, {typeStr});\n";
            }
            utilsText += @"            default:
                throw new Exception();
        }
    }
}
";
            SaveScript(editorUtilsScript, utilsText);
        }

        text += "    public partial struct Tween {";
        text += generateWithDefines(generate);
        text = addCustomAnimationMethods(text);
        text += additiveMethodsGenerator.Generate();
        text += speedBasedMethodsGenerator.Generate();
        text += @"
    }
}";
        SaveScript(methodsScript, text);
    }

    static string GetTweenTypeEnumName(MethodGenerationData data) {
        string result = "";
        var dependency = data.dependency;
        if (dependency != Dependency.None) {
            result += getMethodPrefix(dependency);
        }
        if (dependency == Dependency.UI_ELEMENTS_MODULE_INSTALLED && !data.methodName.Contains("VisualElement")) {
            result += "VisualElement";
        }
        result += data.methodName;
        if ((data.methodName == "Alpha" || data.methodName == "Color") && data.dependency == Dependency.UNITY_UGUI_INSTALLED) {
            result += getTypeByName(data.targetType).Name;
        } else if (data.methodName == "Scale" && data.propertyType == PropType.Float) {
            result += "Uniform";
        } else if ((data.methodName == "Rotation" || data.methodName == "LocalRotation") && data.propertyType == PropType.Quaternion) {
            result += "Quaternion";
        } else if (data.targetType == "PrimeTween.Sequence" ) {
            result += "Sequence";
        } else if (data.targetType == "UnityEngine.Rigidbody2D") {
            result += "2D";
        }
        return result;
    }

    [NotNull]
    string generateWithDefines([NotNull] Func<MethodGenerationData, string> generator) {
        string result = "";
        foreach (var group in methodsData.GroupBy(_ => _.dependency)) {
            result += generateWithDefines(generator, group);
        }
        return result;
    }

    [NotNull]
    static string generateWithDefines([NotNull] Func<MethodGenerationData, string> generator, [NotNull] IGrouping<Dependency, MethodGenerationData> group) {
        var result = "";
        var dependency = group.Key;
        if (shouldWrapInDefine(dependency)) {
            switch (dependency) {
                case Dependency.PRIME_TWEEN_EXPERIMENTAL:
                case Dependency.UI_ELEMENTS_MODULE_INSTALLED:
                case Dependency.TEXT_MESH_PRO_INSTALLED:
                    result += $"\n        #if {dependency}";
                    break;
                default:
                    result += $"\n        #if !UNITY_2019_1_OR_NEWER || {dependency}";
                    break;
            }
        }
        foreach (var method in group) {
            var generated = generator(method);
            if (!string.IsNullOrEmpty(generated)) {
                result += generated;
                result += "\n";
            }
        }
        if (shouldWrapInDefine(dependency)) {
            result += "\n        #endif";
        }
        return result;
    }

    [NotNull]
    static Type getTypeByName(string targetType) {
        var types = AppDomain.CurrentDomain
            .GetAssemblies()
            .Select(_ => _.GetType(targetType))
            .Where(_ => _ != null)
            .Where(_ => _.FullName == targetType)
            .Distinct()
            .ToArray();
        switch (types.Length) {
            case 0:
                throw new Exception($"target type ({targetType}) not found in any of the assemblies.\n" +
                                    "Please specify the full name of the type. For example, instead of 'Transform', use 'UnityEngine.Transform'.\n" +
                                    "Or install the target package in Package Manager.\n");
            case 1:
                break;
            default:
                throw new Exception($"More than one type found that match {targetType}. Found:\n"
                                    + string.Join("\n", types.Select(_ => $"{_.AssemblyQualifiedName}\n{_.Assembly.GetName().FullName}")));
        }
        var type = types.Single();
        Assert.IsNotNull(type, $"targetType ({targetType}) wasn't found in any assembly.");
        return type;
    }

    [NotNull]
    static string generate([NotNull] MethodGenerationData data) {
        var methodName = data.methodName;
        Assert.IsTrue(System.CodeDom.Compiler.CodeGenerator.IsValidLanguageIndependentIdentifier(methodName), $"Method name is invalid: {methodName}.");
        var propertyName = data.propertyName;

        var overload = populateTemplate(overloadTemplateTo, data);
        var full = populateTemplate(fullTemplate, data);
        const string templatePropName = "orthographicSize";
        string replaced = "";
        if (data.generateOnlyOverloads) {
            replaced += "\n";
            replaced += overload;
        } else if (propertyName.Any()) {
            checkFieldOrProp();
            Assert.IsFalse(data.propertyGetter.Any());
            Assert.IsFalse(data.propertySetter.Any());
            replaced += "\n";
            replaced += overload;
            replaced += "\n";
            replaced += full;
            replaced = replaced.Replace(templatePropName, propertyName);

            void checkFieldOrProp() {
                var type = getTypeByName(data.targetType);
                Assert.IsNotNull(type);
                const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;
                var prop = type.GetProperty(propertyName, flags);
                Type expectedPropType;
                if (data.propertyType == PropType.Float) {
                    expectedPropType = typeof(float);
                } else if (data.propertyType == PropType.Int) {
                    expectedPropType = typeof(int);
                } else {
                    var typeName = $"{data.propertyType.ToFullTypeName()}, UnityEngine.CoreModule";
                    expectedPropType = Type.GetType(typeName);
                    Assert.IsNotNull(expectedPropType, typeName);
                }
                if (prop != null) {
                    Assert.AreEqual(expectedPropType, prop.PropertyType);
                    return;
                }

                var field = type.GetField(propertyName, flags);
                if (field != null) {
                    Assert.AreEqual(expectedPropType, field.FieldType, "Field type is incorrect.");
                    return;
                }

                throw new Exception($"Field or property with name ({propertyName}) not found for type {type.FullName}. Generation data name: {data.description}.");
            }
        } else {
            Assert.IsTrue(data.propertySetter.Any());
            if (data.propertyGetter.Any()) {
                replaced += "\n";
                replaced += replaceGetter(overload);
            }

            replaced += "\n";
            full = replaceSetter(full);
            replaced += replaceGetter(full);

            // ReSharper disable once AnnotateNotNullTypeMember
            string replaceGetter(string str) {
                while (true) {
                    var j = str.IndexOf(templatePropName, StringComparison.Ordinal);
                    if (j == -1) {
                        break;
                    }
                    Assert.AreNotEqual(-1, j);
                    str = str.Remove(j, templatePropName.Length);
                    str = str.Insert(j, data.propertyGetter);
                }
                return str;
            }

            // ReSharper disable once AnnotateNotNullTypeMember
            string replaceSetter(string str) {
                while (true) {
                    var k = str.IndexOf("orthographicSize =", StringComparison.Ordinal);
                    if (k == -1) {
                        break;
                    }
                    Assert.AreNotEqual(-1, k);
                    var endIndex = str.IndexOf(";", k, StringComparison.Ordinal);
                    Assert.AreNotEqual(-1, endIndex);
                    str = str.Remove(k, endIndex - k);
                    str = str.Insert(k, data.propertySetter);
                }
                return str;
            }
        }
        return replaced;
    }

    [NotNull]
    static string addCustomAnimationMethods(string text) {
        const string template = @"        public static Tween Custom_TEMPLATE(Single startValue, Single endValue, float duration, [NotNull] Action<Single> onValueChange, Ease ease = Ease.Default, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => Custom_TEMPLATE(new TweenSettings<float>(startValue, endValue, new TweenSettings(duration, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)), onValueChange);
        public static Tween Custom_TEMPLATE(Single startValue, Single endValue, float duration, [NotNull] Action<Single> onValueChange, Easing ease, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => Custom_TEMPLATE(new TweenSettings<float>(startValue, endValue, new TweenSettings(duration, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)), onValueChange);
        public static Tween Custom_TEMPLATE(Single startValue, Single endValue, TweenSettings settings, [NotNull] Action<Single> onValueChange) => Custom_TEMPLATE(new TweenSettings<float>(startValue, endValue, settings), onValueChange);
        public static Tween Custom_TEMPLATE(TweenSettings<float> settings, [NotNull] Action<Single> onValueChange) {
            Assert.IsNotNull(onValueChange);
            if (settings.startFromCurrent) {
                UnityEngine.Debug.LogWarning(Constants.customTweensDontSupportStartFromCurrentWarning);
            }
            var tween = PrimeTweenManager.fetchTween();
            tween.startValue.CopyFrom(ref settings.startValue);
            tween.endValue.CopyFrom(ref settings.endValue);
            tween.customOnValueChange = onValueChange;
            tween.Setup(PrimeTweenManager.dummyTarget, ref settings.settings, _tween => {
                var _onValueChange = _tween.customOnValueChange as Action<Single>;
                var val = _tween.FloatVal;
                try {
                    _onValueChange(val);
                } catch (Exception e) {
                    UnityEngine.Debug.LogException(e);
                    Assert.LogWarning($""Tween was stopped because of exception in {nameof(onValueChange)} callback, tween: {_tween.GetDescription()}\n"", _tween.id, _tween.target as UnityEngine.Object);
                    _tween.EmergencyStop();
                }
            }, null, false, TweenType.CustomFloat);
            return PrimeTweenManager.Animate(tween);
        }
        public static Tween Custom_TEMPLATE<T>([NotNull] T target, Single startValue, Single endValue, float duration, [NotNull] Action<T, Single> onValueChange, Ease ease = Ease.Default, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) where T : class 
            => Custom_internal(target, new TweenSettings<float>(startValue, endValue, new TweenSettings(duration, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)), onValueChange);
        public static Tween Custom_TEMPLATE<T>([NotNull] T target, Single startValue, Single endValue, float duration, [NotNull] Action<T, Single> onValueChange, Easing ease, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) where T : class 
            => Custom_internal(target, new TweenSettings<float>(startValue, endValue, new TweenSettings(duration, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)), onValueChange);
        public static Tween Custom_TEMPLATE<T>([NotNull] T target, Single startValue, Single endValue, TweenSettings settings, [NotNull] Action<T, Single> onValueChange) where T : class 
            => Custom_internal(target, new TweenSettings<float>(startValue, endValue, settings), onValueChange);
        public static Tween Custom_TEMPLATE<T>([NotNull] T target, TweenSettings<float> settings, [NotNull] Action<T, Single> onValueChange) where T : class 
            => Custom_internal(target, settings, onValueChange);
        #if PRIME_TWEEN_EXPERIMENTAL
        public static Tween CustomAdditive<T>([NotNull] T target, Single deltaValue, TweenSettings settings, [NotNull] Action<T, Single> onDeltaChange) where T : class 
            => Custom_internal(target, new TweenSettings<float>(default, deltaValue, settings), onDeltaChange, true);
        #endif
        static Tween Custom_internal<T>([NotNull] T target, TweenSettings<float> settings, [NotNull] Action<T, Single> onValueChange, bool isAdditive = false) where T : class {
            Assert.IsNotNull(onValueChange);
            if (settings.startFromCurrent) {
                UnityEngine.Debug.LogWarning(Constants.customTweensDontSupportStartFromCurrentWarning);
            }
            var tween = PrimeTweenManager.fetchTween();
            tween.startValue.CopyFrom(ref settings.startValue);
            tween.endValue.CopyFrom(ref settings.endValue);
            tween.customOnValueChange = onValueChange;
            tween.isAdditive = isAdditive;
            tween.Setup(target, ref settings.settings, _tween => {
                var _onValueChange = _tween.customOnValueChange as Action<T, Single>;
                var _target = _tween.target as T;
                Single val;
                if (_tween.isAdditive) {
                    var newVal = _tween.FloatVal;
                    val = newVal.calcDelta(_tween.prevVal);
                    _tween.prevVal.FloatVal = newVal;
                } else {
                    val = _tween.FloatVal;
                }
                try {
                    _onValueChange(_target, val);
                } catch (Exception e) {
                    UnityEngine.Debug.LogException(e, _target as UnityEngine.Object);
                    Assert.LogWarning($""Tween was stopped because of exception in {nameof(onValueChange)} callback, tween: {_tween.GetDescription()}\n"", _tween.id, _tween.target as UnityEngine.Object);
                    _tween.EmergencyStop();
                }
            }, null, false, TweenType.CustomFloat);
            return PrimeTweenManager.Animate(tween);
        }
        static Tween animate(object target, ref TweenSettings<float> settings, [NotNull] Action<ReusableTween> setter, Func<ReusableTween, ValueContainer> getter, TweenType _tweenType) {
            var tween = PrimeTweenManager.fetchTween();
            tween.startValue.CopyFrom(ref settings.startValue);
            tween.endValue.CopyFrom(ref settings.endValue);
            tween.Setup(target, ref settings.settings, setter, getter, settings.startFromCurrent, _tweenType);
            return PrimeTweenManager.Animate(tween);
        }
        static Tween animateWithIntParam([NotNull] object target, int intParam, ref TweenSettings<float> settings, [NotNull] Action<ReusableTween> setter, [NotNull] Func<ReusableTween, ValueContainer> getter, TweenType _tweenType) {
            var tween = PrimeTweenManager.fetchTween();
            tween.intParam = intParam;
            tween.startValue.CopyFrom(ref settings.startValue);
            tween.endValue.CopyFrom(ref settings.endValue);
            tween.Setup(target, ref settings.settings, setter, getter, settings.startFromCurrent, _tweenType);
            return PrimeTweenManager.Animate(tween);
        }";

        var types = new[] { typeof(float), typeof(Color), typeof(Vector2), typeof(Vector3), typeof(Vector4), typeof(Quaternion), typeof(Rect) };
        foreach (var type in types) {
            text += "\n\n";
            var isFloat = type == typeof(float);
            var replaced = template;
            replaced = replaced.Replace("Single", isFloat ? "float" : type.FullName);
            if (!isFloat) {
                replaced = replaced.Replace("TweenSettings<float>", $"TweenSettings<{type.FullName}>");
                replaced = replaced.Replace(".FloatVal", $".{type.Name}Val");
                replaced = replaced.Replace("Single val;", $"{type.Name} val;");
                replaced = replaced.Replace("PropType.Float", $"PropType.{type.Name}");
                replaced = replaced.Replace("TweenType.CustomFloat", $"TweenType.Custom{type.Name}");
            }
            replaced = replaced.Replace("Custom_TEMPLATE", "Custom");
            text += replaced;
        }
        return text;
    }

    [NotNull]
    static string populateTemplate([NotNull] string str, [NotNull] MethodGenerationData data) {
        var methodName = data.methodName;
        var prefix = getMethodPrefix(data.dependency);
        if (prefix != null && !data.placeInGlobalScope) {
            methodName = prefix + methodName;
        }
        var targetType = data.targetType;
        if (string.IsNullOrEmpty(targetType)) {
            str = str.Replace("[NotNull] UnityEngine.Camera target, ", "")
                .Replace("METHOD_NAME(target, ", "METHOD_NAME(");
        } else {
            str = str.Replace("UnityEngine.Camera", targetType);
        }
        str = str.Replace("METHOD_NAME",  methodName);
        str = str.Replace("TweenType.CameraOrthographicSize",  $"TweenType.{GetTweenTypeEnumName(data)}");
        if (data.propertyType != PropType.Float) {
            str = str.Replace("Single", data.propertyType.ToFullTypeName());
            str = str.Replace("_tween.FloatVal", $"_tween.{data.propertyType.ToString()}Val");
            str = str.Replace("TweenSettings<float>", $"TweenSettings<{data.propertyType.ToFullTypeName()}>");
        }
        return str;
    }

    [Serializable]
    internal class SpeedBasedMethodsGenerator {
        [SerializeField] Data[] data;

        [Serializable]
        class Data {
            [SerializeField] internal string methodName;
            [SerializeField] internal PropType propType;
            [SerializeField] internal string propName;
            [SerializeField] internal string speedParamName;
        }

        [NotNull]
        internal string Generate() {
            string result = "";
            foreach (var d in data) {
                const string template = @"
        public static Tween PositionAtSpeed([NotNull] UnityEngine.Transform target, UnityEngine.Vector3 endValue, float averageSpeed, Ease ease = Ease.Default, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => PositionAtSpeed(target, new TweenSettings<UnityEngine.Vector3>(endValue, new TweenSettings(averageSpeed, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)));
        public static Tween PositionAtSpeed([NotNull] UnityEngine.Transform target, UnityEngine.Vector3 endValue, float averageSpeed, Easing ease, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => PositionAtSpeed(target, new TweenSettings<UnityEngine.Vector3>(endValue, new TweenSettings(averageSpeed, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)));
        public static Tween PositionAtSpeed([NotNull] UnityEngine.Transform target, UnityEngine.Vector3 startValue, UnityEngine.Vector3 endValue, float averageSpeed, Ease ease = Ease.Default, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => PositionAtSpeed(target, new TweenSettings<UnityEngine.Vector3>(startValue, endValue, new TweenSettings(averageSpeed, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)));
        public static Tween PositionAtSpeed([NotNull] UnityEngine.Transform target, UnityEngine.Vector3 startValue, UnityEngine.Vector3 endValue, float averageSpeed, Easing ease, int cycles = 1, CycleMode cycleMode = CycleMode.Restart, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = false) 
            => PositionAtSpeed(target, new TweenSettings<UnityEngine.Vector3>(startValue, endValue, new TweenSettings(averageSpeed, ease, cycles, cycleMode, startDelay, endDelay, useUnscaledTime)));
        static Tween PositionAtSpeed([NotNull] UnityEngine.Transform target, TweenSettings<UnityEngine.Vector3> settings) {
            var speed = settings.settings.duration;
            if (speed <= 0) {
                UnityEngine.Debug.LogError($""Invalid speed provided to the Tween.{nameof(PositionAtSpeed)}() method: {speed}."");
                return default;
            }
            if (settings.startFromCurrent) {
                settings.startFromCurrent = false;
                settings.startValue = target.position;
            }
            settings.settings.duration = Extensions.CalcDistance(settings.startValue, settings.endValue) / speed;
            return Tween.Position(target, settings);
        }
";
                result += template.Replace("PositionAtSpeed", $"{d.methodName}AtSpeed")
                    .Replace("UnityEngine.Vector3", d.propType.ToFullTypeName())
                    .Replace("Tween.Position", $"{d.methodName}")
                    .Replace("target.position", $"target.{d.propName}")
                    .Replace("averageSpeed", $"{d.speedParamName}")
                    ;
            }
            return result;
        }
    }
}

[Serializable]
class MethodGenerationData {
    public string description;
    public string methodName;
    public string targetType;
    public PropType propertyType;

    public string propertyName;

    public string propertyGetter;
    public string propertySetter;
    public string dotweenMethodName;
    public Dependency dependency;
    public bool placeInGlobalScope;
    public bool generateOnlyOverloads;
}

[PublicAPI]
enum Dependency {
    None,
    UNITY_UGUI_INSTALLED,
    AUDIO_MODULE_INSTALLED,
    PHYSICS_MODULE_INSTALLED,
    PHYSICS2D_MODULE_INSTALLED,
    Camera,
    Material,
    Light,
    PRIME_TWEEN_EXPERIMENTAL,
    UI_ELEMENTS_MODULE_INSTALLED,
    TEXT_MESH_PRO_INSTALLED
}

static class Ext {
    [NotNull]
    internal static string ToFullTypeName(this PropType type) {
        Assert.AreNotEqual(PropType.Float, type);
        if (type == PropType.Int) {
            return "int";
        }
        return $"UnityEngine.{type}";
    }
}
