using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Script.Serialization;
using StardewModdingAPI.Common;

namespace StardewModdingAPI.ModBuildConfig.Framework
{
    /// <summary>Manages the files that are part of a mod package.</summary>
    internal class ModFileManager
    {
        /*********
        ** Properties
        *********/
        /// <summary>The name of the manifest file.</summary>
        private readonly string ManifestFileName = "manifest.json";

        /// <summary>The files that are part of the package.</summary>
        private readonly IDictionary<string, FileInfo> Files;


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="projectDir">The folder containing the project files.</param>
        /// <param name="targetDir">The folder containing the build output.</param>
        /// <exception cref="UserErrorException">The mod package isn't valid.</exception>
        public ModFileManager(string projectDir, string targetDir)
        {
            this.Files = new Dictionary<string, FileInfo>(StringComparer.InvariantCultureIgnoreCase);

            // validate paths
            if (!Directory.Exists(projectDir))
                throw new UserErrorException("Could not create mod package because the project folder wasn't found.");
            if (!Directory.Exists(targetDir))
                throw new UserErrorException("Could not create mod package because no build output was found.");

            // project manifest
            bool hasProjectManifest = false;
            {
                FileInfo manifest = new FileInfo(Path.Combine(projectDir, "manifest.json"));
                if (manifest.Exists)
                {
                    this.Files[this.ManifestFileName] = manifest;
                    hasProjectManifest = true;
                }
            }

            // project i18n files
            bool hasProjectTranslations = false;
            DirectoryInfo translationsFolder = new DirectoryInfo(Path.Combine(projectDir, "i18n"));
            if (translationsFolder.Exists)
            {
                foreach (FileInfo file in translationsFolder.EnumerateFiles())
                    this.Files[Path.Combine("i18n", file.Name)] = file;
                hasProjectTranslations = true;
            }

            // build output
            DirectoryInfo buildFolder = new DirectoryInfo(targetDir);
            foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories))
            {
                // get relative paths
                string relativePath = file.FullName.Replace(buildFolder.FullName, "");
                string relativeDirPath = file.Directory.FullName.Replace(buildFolder.FullName, "");

                // prefer project manifest/i18n files
                if (hasProjectManifest && this.EqualsInvariant(relativePath, this.ManifestFileName))
                    continue;
                if (hasProjectTranslations && this.EqualsInvariant(relativeDirPath, "i18n"))
                    continue;

                // ignore release zips
                if (this.EqualsInvariant(file.Extension, ".zip"))
                    continue;

                // ignore Json.NET (bundled into SMAPI)
                if (this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") || this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml"))
                    continue;

                // add file
                this.Files[relativePath] = file;
            }

            // check for missing manifest
            if (!this.Files.ContainsKey(this.ManifestFileName))
                throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output.");

            // check for missing DLL
            // ReSharper disable once SimplifyLinqExpression
            if (!this.Files.Any(p => !p.Key.EndsWith(".dll")))
                throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output.");
        }

        /// <summary>Get the files in the mod package.</summary>
        public IDictionary<string, FileInfo> GetFiles()
        {
            return new Dictionary<string, FileInfo>(this.Files, StringComparer.InvariantCultureIgnoreCase);
        }

        /// <summary>Get a semantic version from the mod manifest.</summary>
        /// <exception cref="UserErrorException">The manifest is missing or invalid.</exception>
        public string GetManifestVersion()
        {
            // get manifest file
            if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile))
                throw new InvalidOperationException($"The mod does not have a {this.ManifestFileName} file."); // shouldn't happen since we validate in constructor

            // read content
            string json = File.ReadAllText(manifestFile.FullName);
            if (string.IsNullOrWhiteSpace(json))
                throw new UserErrorException("The mod's manifest must not be empty.");

            // parse JSON
            IDictionary<string, object> data;
            try
            {
                data = this.Parse(json);
            }
            catch (Exception ex)
            {
                throw new UserErrorException($"The mod's manifest couldn't be parsed. It doesn't seem to be valid JSON.\n{ex}");
            }

            // get version field
            object versionObj = data.ContainsKey("Version") ? data["Version"] : null;
            if (versionObj == null)
                throw new UserErrorException("The mod's manifest must have a version field.");

            // get version string
            if (versionObj is IDictionary<string, object> versionFields) // SMAPI 1.x
            {
                int major = versionFields.ContainsKey("MajorVersion") ? (int)versionFields["MajorVersion"] : 0;
                int minor = versionFields.ContainsKey("MinorVersion") ? (int)versionFields["MinorVersion"] : 0;
                int patch = versionFields.ContainsKey("PatchVersion") ? (int)versionFields["PatchVersion"] : 0;
                string tag = versionFields.ContainsKey("Build") ? (string)versionFields["Build"] : null;
                return new SemanticVersionImpl(major, minor, patch, tag).ToString();
            }
            return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Get a case-insensitive dictionary matching the given JSON.</summary>
        /// <param name="json">The JSON to parse.</param>
        private IDictionary<string, object> Parse(string json)
        {
            IDictionary<string, object> MakeCaseInsensitive(IDictionary<string, object> dict)
            {
                foreach (var field in dict.ToArray())
                {
                    if (field.Value is IDictionary<string, object> value)
                        dict[field.Key] = MakeCaseInsensitive(value);
                }
                return new Dictionary<string, object>(dict, StringComparer.InvariantCultureIgnoreCase);
            }

            IDictionary<string, object> data = (IDictionary<string, object>)new JavaScriptSerializer().DeserializeObject(json);
            return MakeCaseInsensitive(data);
        }

        /// <summary>Get whether a string is equal to another case-insensitively.</summary>
        /// <param name="str">The string value.</param>
        /// <param name="other">The string to compare with.</param>
        private bool EqualsInvariant(string str, string other)
        {
            return str.Equals(other, StringComparison.InvariantCultureIgnoreCase);
        }
    }
}