using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.Serialisation;

namespace StardewModdingAPI.Framework.ModLoading
{
    /// <summary>Finds and processes mod metadata.</summary>
    internal class ModResolver
    {
        /*********
        ** Public methods
        *********/
        /// <summary>Get manifest metadata for each folder in the given root path.</summary>
        /// <param name="rootPath">The root path to search for mods.</param>
        /// <param name="jsonHelper">The JSON helper with which to read manifests.</param>
        /// <param name="compatibilityRecords">Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param>
        /// <returns>Returns the manifests by relative folder.</returns>
        public IEnumerable<IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable<ModCompatibility> compatibilityRecords)
        {
            compatibilityRecords = compatibilityRecords.ToArray();
            foreach (DirectoryInfo modDir in this.GetModFolders(rootPath))
            {
                // read file
                Manifest manifest = null;
                string path = Path.Combine(modDir.FullName, "manifest.json");
                string error = null;
                try
                {
                    // read manifest
                    manifest = jsonHelper.ReadJsonFile<Manifest>(path);

                    // validate
                    if (manifest == null)
                    {
                        error = File.Exists(path)
                            ? "its manifest is invalid."
                            : "it doesn't have a manifest.";
                    }
                    else if (string.IsNullOrWhiteSpace(manifest.EntryDll))
                        error = "its manifest doesn't set an entry DLL.";
                }
                catch (Exception ex)
                {
                    error = $"parsing its manifest failed:\n{ex.GetLogSummary()}";
                }

                // get compatibility record
                ModCompatibility compatibility = null;
                if (manifest != null)
                {
                    string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll;
                    compatibility = (
                        from mod in compatibilityRecords
                        where
                            mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase)
                            && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion))
                            && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion)
                        select mod
                    ).FirstOrDefault();
                }
                // build metadata
                string displayName = !string.IsNullOrWhiteSpace(manifest?.Name)
                    ? manifest.Name
                    : modDir.FullName.Replace(rootPath, "").Trim('/', '\\');
                ModMetadataStatus status = error == null
                    ? ModMetadataStatus.Found
                    : ModMetadataStatus.Failed;

                yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility).SetStatus(status, error);
            }
        }

        /// <summary>Validate manifest metadata.</summary>
        /// <param name="mods">The mod manifests to validate.</param>
        /// <param name="apiVersion">The current SMAPI version.</param>
        public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion)
        {
            foreach (IModMetadata mod in mods)
            {
                // skip if already failed
                if (mod.Status == ModMetadataStatus.Failed)
                    continue;

                // validate compatibility
                {
                    ModCompatibility compatibility = mod.Compatibility;
                    if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
                    {
                        bool hasOfficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UpdateUrl);
                        bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UnofficialUpdateUrl);

                        string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game";
                        string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion} here:";
                        if (hasOfficialUrl)
                            error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
                        if (hasUnofficialUrl)
                            error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";

                        mod.SetStatus(ModMetadataStatus.Failed, error);
                        continue;
                    }
                }

                // validate SMAPI version
                if (!string.IsNullOrWhiteSpace(mod.Manifest.MinimumApiVersion))
                {
                    if (!SemanticVersion.TryParse(mod.Manifest.MinimumApiVersion, out ISemanticVersion minVersion))
                    {
                        mod.SetStatus(ModMetadataStatus.Failed, $"it has an invalid minimum SMAPI version '{mod.Manifest.MinimumApiVersion}'. This should be a semantic version number like {apiVersion}.");
                        continue;
                    }
                    if (minVersion.IsNewerThan(apiVersion))
                    {
                        mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.");
                        continue;
                    }
                }

                // validate DLL path
                string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll);
                if (!File.Exists(assemblyPath))
                    mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
            }
        }

#if EXPERIMENTAL
        /// <summary>Sort the given mods by the order they should be loaded.</summary>
        /// <param name="mods">The mods to process.</param>
        public IEnumerable<IModMetadata> ProcessDependencies(IEnumerable<IModMetadata> mods)
        {
            // initialise metadata
            mods = mods.ToArray();
            var sortedMods = new Stack<IModMetadata>();
            var states = mods.ToDictionary(mod => mod, mod => ModDependencyStatus.Queued);

            // handle failed mods
            foreach (IModMetadata mod in mods.Where(m => m.Status == ModMetadataStatus.Failed))
            {
                states[mod] = ModDependencyStatus.Failed;
                sortedMods.Push(mod);
            }
            
            // sort mods
            foreach (IModMetadata mod in mods)
                this.ProcessDependencies(mods.ToArray(), mod, states, sortedMods, new List<IModMetadata>());

            return sortedMods.Reverse();
        }
#endif


        /*********
        ** Private methods
        *********/
#if EXPERIMENTAL
        /// <summary>Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary>
        /// <param name="mods">The full list of mods being validated.</param>
        /// <param name="mod">The mod whose dependencies to process.</param>
        /// <param name="states">The dependency state for each mod.</param>
        /// <param name="sortedMods">The list in which to save mods sorted by dependency order.</param>
        /// <param name="currentChain">The current change of mod dependencies.</param>
        /// <returns>Returns the mod dependency status.</returns>
        private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, IModMetadata mod, IDictionary<IModMetadata, ModDependencyStatus> states, Stack<IModMetadata> sortedMods, ICollection<IModMetadata> currentChain)
        {
            // check if already visited
            switch (states[mod])
            {
                // already sorted or failed
                case ModDependencyStatus.Sorted:
                case ModDependencyStatus.Failed:
                    return states[mod];

                // dependency loop
                case ModDependencyStatus.Checking:
                    // This should never happen. The higher-level mod checks if the dependency is
                    // already being checked, so it can fail without visiting a mod twice. If this
                    // case is hit, that logic didn't catch the dependency loop for some reason.
                    throw new InvalidModStateException($"A dependency loop was not caught by the calling iteration ({string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {mod.DisplayName})).");

                // not visited yet, start processing
                case ModDependencyStatus.Queued:
                    break;

                // sanity check
                default:
                    throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'.");
            }

            // no dependencies, mark sorted
            if (mod.Manifest.Dependencies == null || !mod.Manifest.Dependencies.Any())
            {
                sortedMods.Push(mod);
                return states[mod] = ModDependencyStatus.Sorted;
            }

            // missing required dependencies, mark failed
            {
                string[] missingModIDs =
                    (
                        from dependency in mod.Manifest.Dependencies
                        where mods.All(m => m.Manifest.UniqueID != dependency.UniqueID)
                        orderby dependency.UniqueID
                        select dependency.UniqueID
                    )
                    .ToArray();
                if (missingModIDs.Any())
                {
                    sortedMods.Push(mod);
                    mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", missingModIDs)}).");
                    return states[mod] = ModDependencyStatus.Failed;
                }
            }

            // process dependencies
            {
                states[mod] = ModDependencyStatus.Checking;

                // get mods to load first
                IModMetadata[] modsToLoadFirst =
                    (
                        from other in mods
                        where mod.Manifest.Dependencies.Any(required => required.UniqueID == other.Manifest.UniqueID)
                        select other
                    )
                    .ToArray();

                // recursively sort dependencies
                foreach (IModMetadata requiredMod in modsToLoadFirst)
                {
                    var subchain = new List<IModMetadata>(currentChain) { mod };

                    // detect dependency loop
                    if (states[requiredMod] == ModDependencyStatus.Checking)
                    {
                        sortedMods.Push(mod);
                        mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName}).");
                        return states[mod] = ModDependencyStatus.Failed;
                    }

                    // recursively process each dependency
                    var substatus = this.ProcessDependencies(mods, requiredMod, states, sortedMods, subchain);
                    switch (substatus)
                    {
                        // sorted successfully
                        case ModDependencyStatus.Sorted:
                            break;

                        // failed, which means this mod can't be loaded either
                        case ModDependencyStatus.Failed:
                            sortedMods.Push(mod);
                            mod.SetStatus(ModMetadataStatus.Failed, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded.");
                            return states[mod] = ModDependencyStatus.Failed;

                        // unexpected status
                        case ModDependencyStatus.Queued:
                        case ModDependencyStatus.Checking:
                            throw new InvalidModStateException($"Something went wrong sorting dependencies: mod '{requiredMod.DisplayName}' unexpectedly stayed in the '{substatus}' status.");

                        // sanity check
                        default:
                            throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'.");
                    }
                }

                // all requirements sorted successfully
                sortedMods.Push(mod);
                return states[mod] = ModDependencyStatus.Sorted;
            }
        }
#endif

        /// <summary>Get all mod folders in a root folder, passing through empty folders as needed.</summary>
        /// <param name="rootPath">The root folder path to search.</param>
        private IEnumerable<DirectoryInfo> GetModFolders(string rootPath)
        {
            foreach (string modRootPath in Directory.GetDirectories(rootPath))
            {
                DirectoryInfo directory = new DirectoryInfo(modRootPath);

                // if a folder only contains another folder, check the inner folder instead
                while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
                    directory = directory.GetDirectories().First();

                yield return directory;
            }
        }
    }
}