using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;

namespace Getly.Publisher
{
    /// <summary>
    /// Gathers what <see cref="GetlyValidator"/> needs from the editor.
    ///
    /// This is the only part of the validator that requires Unity, and it is
    /// kept thin on purpose: it answers questions, it does not decide anything.
    /// Everything it learns — a prefab's broken script reference, a texture's
    /// import settings — is put on a plain object the rules can be tested
    /// against without an editor running.
    ///
    /// These are also the things a server can never see. A .unitypackage
    /// carries a prefab as serialised YAML referencing a script GUID; only a
    /// project that has imported it knows whether that GUID resolves. That
    /// asymmetry is the reason the tool exists at all rather than being a
    /// server-side upload check.
    /// </summary>
    public static class GetlyCollector
    {
        /// <summary>Formats we read as text. Anything else is left unread.</summary>
        private static readonly HashSet<string> TextExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            ".cs", ".txt", ".md", ".json", ".xml", ".shader", ".cginc", ".hlsl", ".asmdef", ".yaml", ".yml"
        };

        /// <summary>Read at most this much of any one file. Big generated files exist.</summary>
        private const int TextReadLimit = 256 * 1024;

        public static List<GetlyAsset> Collect(string rootFolder)
        {
            var assets = new List<GetlyAsset>();
            if (string.IsNullOrEmpty(rootFolder) || !AssetDatabase.IsValidFolder(rootFolder)) return assets;

            var guids = AssetDatabase.FindAssets(string.Empty, new[] { rootFolder });
            var seen = new HashSet<string>(StringComparer.Ordinal);

            foreach (var guid in guids)
            {
                var path = AssetDatabase.GUIDToAssetPath(guid);
                if (string.IsNullOrEmpty(path) || !seen.Add(path)) continue;

                var isFolder = AssetDatabase.IsValidFolder(path);
                var asset = new GetlyAsset
                {
                    Path = path,
                    IsFolder = isFolder,
                    HasMetaFile = File.Exists(path + ".meta"),
                };

                if (isFolder)
                {
                    asset.IsEmptyFolder = IsEmptyFolder(path);
                    assets.Add(asset);
                    continue;
                }

                try
                {
                    var info = new FileInfo(path);
                    if (info.Exists) asset.SizeBytes = info.Length;
                }
                catch (Exception)
                {
                    // An unreadable file is reported by its other properties;
                    // failing the whole scan over one of them helps nobody.
                }

                var extension = Path.GetExtension(path);
                if (TextExtensions.Contains(extension) && asset.SizeBytes <= TextReadLimit)
                {
                    asset.Text = ReadTextSafely(path);
                }

                CollectTextureInfo(path, asset);
                asset.HasMissingScriptReference = HasMissingScript(path, extension);

                assets.Add(asset);
            }

            return assets;
        }

        private static string ReadTextSafely(string path)
        {
            try { return File.ReadAllText(path); }
            catch (Exception) { return null; }
        }

        /// <summary>
        /// Empty as the BUYER will receive it, not as the disk sees it.
        ///
        /// Unity ignores dotfiles, so a folder holding only .gitkeep exports as
        /// an empty folder even though the filesystem says otherwise. Asking
        /// the AssetDatabase asks the same question the exporter will.
        /// </summary>
        private static bool IsEmptyFolder(string folderPath)
        {
            try
            {
                // FindAssets on a folder returns the assets INSIDE it, including
                // nested folders — so anything at all means it is not empty.
                return AssetDatabase.FindAssets(string.Empty, new[] { folderPath }).Length == 0;
            }
            catch (Exception)
            {
                return false;
            }
        }

        private static void CollectTextureInfo(string path, GetlyAsset asset)
        {
            var importer = AssetImporter.GetAtPath(path) as TextureImporter;
            if (importer == null) return;

            var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
            if (texture != null)
            {
                // The imported dimensions, which is what actually costs the
                // buyer memory — not the dimensions of the file on disk.
                asset.TextureWidth = texture.width;
                asset.TextureHeight = texture.height;
            }

            var settings = importer.GetDefaultPlatformTextureSettings();
            asset.TextureCompressed = settings.format != TextureImporterFormat.RGBA32
                                      && settings.format != TextureImporterFormat.RGB24
                                      && settings.format != TextureImporterFormat.ARGB32
                                      && settings.textureCompression != TextureImporterCompression.Uncompressed;
        }

        /// <summary>
        /// Does this prefab or scene reference a script that will not resolve?
        ///
        /// This is the defect that produces the refund: the buyer imports the
        /// asset, opens the demo, and every object reads "Missing (Mono Script)".
        /// A null entry in the component list is exactly that state.
        /// </summary>
        private static bool HasMissingScript(string path, string extension)
        {
            if (!string.Equals(extension, ".prefab", StringComparison.OrdinalIgnoreCase)) return false;

            try
            {
                var root = AssetDatabase.LoadAssetAtPath<GameObject>(path);
                if (root == null) return false;

                foreach (var transform in root.GetComponentsInChildren<Transform>(true))
                {
                    var components = transform.GetComponents<Component>();
                    foreach (var component in components)
                    {
                        // Unity leaves a null in the array where the script
                        // used to be. This is the documented way to detect it.
                        if (component == null) return true;
                    }
                }
            }
            catch (Exception)
            {
                // A prefab we cannot load is not evidence of a missing script.
            }

            return false;
        }
    }
}
