using System;
using System.Collections.Generic;

namespace Unity.VisualScripting.FullSerializer.Internal
{
    public static class fsVersionManager
    {
        private static readonly Dictionary<Type, fsOption<fsVersionedType>> _cache = new Dictionary<Type, fsOption<fsVersionedType>>();

        public static fsResult GetVersionImportPath(string currentVersion, fsVersionedType targetVersion, out List<fsVersionedType> path)
        {
            path = new List<fsVersionedType>();

            if (GetVersionImportPathRecursive(path, currentVersion, targetVersion) == false)
            {
                return fsResult.Fail("There is no migration path from \"" + currentVersion + "\" to \"" + targetVersion.VersionString + "\"");
            }

            path.Add(targetVersion);
            return fsResult.Success;
        }

        private static bool GetVersionImportPathRecursive(List<fsVersionedType> path, string currentVersion, fsVersionedType current)
        {
            for (var i = 0; i < current.Ancestors.Length; ++i)
            {
                var ancestor = current.Ancestors[i];

                if (ancestor.VersionString == currentVersion ||
                    GetVersionImportPathRecursive(path, currentVersion, ancestor))
                {
                    path.Add(ancestor);
                    return true;
                }
            }

            return false;
        }

        public static fsOption<fsVersionedType> GetVersionedType(Type type)
        {
            fsOption<fsVersionedType> optionalVersionedType;

            if (_cache.TryGetValue(type, out optionalVersionedType) == false)
            {
                var attr = fsPortableReflection.GetAttribute<fsObjectAttribute>(type);

                if (attr != null)
                {
                    if (string.IsNullOrEmpty(attr.VersionString) == false || attr.PreviousModels != null)
                    {
                        // Version string must be provided
                        if (attr.PreviousModels != null && string.IsNullOrEmpty(attr.VersionString))
                        {
                            throw new Exception("fsObject attribute on " + type + " contains a PreviousModels specifier - it must also include a VersionString modifier");
                        }

                        // Map the ancestor types into versioned types
                        var ancestors = new fsVersionedType[attr.PreviousModels != null ? attr.PreviousModels.Length : 0];
                        for (var i = 0; i < ancestors.Length; ++i)
                        {
                            var ancestorType = GetVersionedType(attr.PreviousModels[i]);
                            if (ancestorType.IsEmpty)
                            {
                                throw new Exception("Unable to create versioned type for ancestor " + ancestorType + "; please add an [fsObject(VersionString=\"...\")] attribute");
                            }
                            ancestors[i] = ancestorType.Value;
                        }

                        // construct the actual versioned type instance
                        var versionedType = new fsVersionedType
                        {
                            Ancestors = ancestors,
                            VersionString = attr.VersionString,
                            ModelType = type
                        };

                        // finally, verify that the versioned type passes some
                        // sanity checks
                        VerifyUniqueVersionStrings(versionedType);
                        VerifyConstructors(versionedType);

                        optionalVersionedType = fsOption.Just(versionedType);
                    }
                }

                _cache[type] = optionalVersionedType;
            }

            return optionalVersionedType;
        }

        /// <summary>
        /// Verifies that the given type has constructors to migrate from all
        /// ancestor types.
        /// </summary>
        private static void VerifyConstructors(fsVersionedType type)
        {
            var publicConstructors = type.ModelType.GetDeclaredConstructors();

            for (var i = 0; i < type.Ancestors.Length; ++i)
            {
                var requiredConstructorType = type.Ancestors[i].ModelType;

                var found = false;
                for (var j = 0; j < publicConstructors.Length; ++j)
                {
                    var parameters = publicConstructors[j].GetParameters();
                    if (parameters.Length == 1 && parameters[0].ParameterType == requiredConstructorType)
                    {
                        found = true;
                        break;
                    }
                }

                if (found == false)
                {
                    throw new fsMissingVersionConstructorException(type.ModelType, requiredConstructorType);
                }
            }
        }

        /// <summary>
        /// Verifies that the given version graph contains only unique versions.
        /// </summary>
        private static void VerifyUniqueVersionStrings(fsVersionedType type)
        {
            // simple tree traversal

            var found = new Dictionary<string, Type>();

            var remaining = new Queue<fsVersionedType>();
            remaining.Enqueue(type);

            while (remaining.Count > 0)
            {
                var item = remaining.Dequeue();

                // Verify we do not already have the version string. Take into
                // account that we're not just comparing the same model twice,
                // since we can have a valid import graph that has the same model
                // multiple times.
                if (found.ContainsKey(item.VersionString) && found[item.VersionString] != item.ModelType)
                {
                    throw new fsDuplicateVersionNameException(found[item.VersionString], item.ModelType, item.VersionString);
                }
                found[item.VersionString] = item.ModelType;

                // scan the ancestors as well
                foreach (var ancestor in item.Ancestors)
                {
                    remaining.Enqueue(ancestor);
                }
            }
        }
    }
}
