using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
#if DEBUG
using System.IO;
#else
using Barotrauma.IO;
#endif
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace Barotrauma
{
    [Flags]
    public enum SubmarineTag
    {
        [Description("Shuttle")]
        Shuttle = 1,
        [Description("Hide in menus")]
        HideInMenus = 2
    }

    public enum SubmarineType { Player, Outpost, OutpostModule, Wreck, BeaconStation, EnemySubmarine, Ruin }
    public enum SubmarineClass { Undefined, Scout, Attack, Transport }

    partial class SubmarineInfo : IDisposable
    {
        private static List<SubmarineInfo> savedSubmarines = new List<SubmarineInfo>();
        public static IEnumerable<SubmarineInfo> SavedSubmarines => savedSubmarines;

        private Task hashTask;
        private Md5Hash hash;

        public readonly DateTime LastModifiedTime;

        public SubmarineTag Tags { get; private set; }

        public int RecommendedCrewSizeMin = 1, RecommendedCrewSizeMax = 2;
        
        public enum CrewExperienceLevel
        {
            Unknown,
            CrewExperienceLow,
            CrewExperienceMid,
            CrewExperienceHigh
        }
        public CrewExperienceLevel RecommendedCrewExperience;

        public int Tier
        {
            get;
            set;
        }

        /// <summary>
        /// A random int that gets assigned when saving the sub. Used in mp campaign to verify that sub files match
        /// </summary>
        public int EqualityCheckVal { get; private set; }

        public HashSet<string> RequiredContentPackages = new HashSet<string>();

        public string Name
        {
            get;
            set;
        }

        public LocalizedString DisplayName
        {
            get;
            set;
        }

        public LocalizedString Description
        {
            get;
            set;
        }

        public int Price
        {
            get;
            set;
        }

        public bool InitialSuppliesSpawned
        {
            get;
            set;
        }

        public bool NoItems
        {
            get;
            set;
        }

        /// <summary>
        /// Note: Refreshed for loaded submarines when they are saved, when they are loaded, and on round end. If you need to refresh it, please use Submarine.CheckFuel() method!
        /// </summary>
        public bool LowFuel
        {
            get;
            set;
        }

        public Version GameVersion
        {
            get;
            set;
        }

        public SubmarineType Type { get; set; }

        public bool IsManuallyOutfitted { get; set; }

        public SubmarineClass SubmarineClass;

        public OutpostModuleInfo OutpostModuleInfo { get; set; }
        public BeaconStationInfo BeaconStationInfo { get; set; }

        public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule;

        public bool IsWreck => Type == SubmarineType.Wreck;
        public bool IsBeacon => Type == SubmarineType.BeaconStation;
        public bool IsPlayer => Type == SubmarineType.Player;
        public bool IsRuin => Type == SubmarineType.Ruin;

        public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined;
        public bool IsCampaignCompatibleIgnoreClass => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus);

        public bool AllowPreviewImage => Type == SubmarineType.Player;

        public Md5Hash MD5Hash
        {
            get
            {
                if (hash == null)
                {
                    if (hashTask == null)
                    {
                        XDocument doc = OpenFile(FilePath);
                        StartHashDocTask(doc);
                    }
                    hashTask.Wait();
                    hashTask = null;
                }

                return hash;
            }
        }

        public bool CalculatingHash
        {
            get { return hashTask != null && !hashTask.IsCompleted; }
        }

        public Vector2 Dimensions
        {
            get;
            private set;
        }

        public int CargoCapacity
        {
            get;
            private set;
        }

        public string FilePath
        {
            get;
            set;
        }

        public XElement SubmarineElement
        {
            get;
            private set;
        }

        public override string ToString()
        {
            return "Barotrauma.SubmarineInfo (" + Name + ")";
        }

        public bool IsFileCorrupted
        {
            get;
            private set;
        }

        private bool? requiredContentPackagesInstalled;
        public bool RequiredContentPackagesInstalled
        {
            get
            {
                if (requiredContentPackagesInstalled.HasValue) { return requiredContentPackagesInstalled.Value; }
                return RequiredContentPackages.All(reqName => ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.NameMatches(reqName)));
            }
            set
            {
                requiredContentPackagesInstalled = value;
            }
        }

        private bool? subsLeftBehind;
        public bool SubsLeftBehind
        {
            get
            {
                if (subsLeftBehind.HasValue) { return subsLeftBehind.Value; }
                CheckSubsLeftBehind(SubmarineElement);
                return subsLeftBehind.Value;
            }
        }
        
        public readonly List<ushort> LeftBehindDockingPortIDs = new List<ushort>();
        public readonly List<ushort> BlockedDockingPortIDs = new List<ushort>();

        public bool LeftBehindSubDockingPortOccupied
        {
            get; private set;
        }

        public OutpostGenerationParams OutpostGenerationParams;

        public readonly Dictionary<Identifier, List<Character>> OutpostNPCs = new Dictionary<Identifier, List<Character>>();

        //constructors & generation ----------------------------------------------------
        public SubmarineInfo()
        {
            FilePath = null;
            DisplayName = TextManager.Get("UnspecifiedSubFileName");
            Name = DisplayName.Value;
            IsFileCorrupted = false;
            RequiredContentPackages = new HashSet<string>();
        }

        public SubmarineInfo(string filePath, string hash = "", XElement element = null, bool tryLoad = true)
        {
            FilePath = filePath;
            if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
            {
                LastModifiedTime = File.GetLastWriteTime(filePath);
            }
            try
            {
                DisplayName = Path.GetFileNameWithoutExtension(filePath);
                Name = DisplayName.Value;
            }
            catch (Exception e)
            {
                DebugConsole.ThrowError("Error loading submarine " + filePath + "!", e);
            }

            if (!string.IsNullOrWhiteSpace(hash))
            {
                this.hash = Md5Hash.StringAsHash(hash);
            }

            IsFileCorrupted = false;

            RequiredContentPackages = new HashSet<string>();

            if (element == null && tryLoad)
            {
                Reload();
            }
            else
            {
                SubmarineElement = element;
            }

            Name = SubmarineElement.GetAttributeString("name", null) ?? Name;

            Init();
        }

        public SubmarineInfo(Submarine sub) : this(sub.Info)
        {
            GameVersion = GameMain.Version;
            SubmarineElement = new XElement("Submarine");
            sub.SaveToXElement(SubmarineElement);
            Init();
        }

        public SubmarineInfo(SubmarineInfo original)
        {
            Name = original.Name;
            DisplayName = original.DisplayName;
            Description = original.Description;
            Price = original.Price;
            InitialSuppliesSpawned = original.InitialSuppliesSpawned;
            NoItems = original.NoItems;
            LowFuel = original.LowFuel;
            GameVersion = original.GameVersion;
            Type = original.Type;
            SubmarineClass = original.SubmarineClass;
            hash = !string.IsNullOrEmpty(original.FilePath) && File.Exists(original.FilePath) ? original.MD5Hash : null;
            Dimensions = original.Dimensions;
            CargoCapacity = original.CargoCapacity;
            FilePath = original.FilePath;
            RequiredContentPackages = new HashSet<string>(original.RequiredContentPackages);
            IsFileCorrupted = original.IsFileCorrupted;
            SubmarineElement = original.SubmarineElement;
            EqualityCheckVal = original.EqualityCheckVal;
            RecommendedCrewExperience = original.RecommendedCrewExperience;
            RecommendedCrewSizeMin = original.RecommendedCrewSizeMin;
            RecommendedCrewSizeMax = original.RecommendedCrewSizeMax;
            Tier = original.Tier;
            IsManuallyOutfitted = original.IsManuallyOutfitted;
            Tags = original.Tags;
            OutpostGenerationParams = original.OutpostGenerationParams;
            if (original.OutpostModuleInfo != null)
            {
                OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo);
            }
            if (original.BeaconStationInfo != null)
            {
                BeaconStationInfo = new BeaconStationInfo(original.BeaconStationInfo);
            }
#if CLIENT
            PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage) : null;
#endif
        }

        public void Reload()
        {
            XDocument doc = null;
            int maxLoadRetries = 4;
            for (int i = 0; i <= maxLoadRetries; i++)
            {
                doc = OpenFile(FilePath, out Exception e);
                if (e != null && !(e is System.IO.IOException)) { break; }
                if (doc != null || i == maxLoadRetries || !File.Exists(FilePath)) { break; }
                DebugConsole.NewMessage("Opening submarine file \"" + FilePath + "\" failed, retrying in 250 ms...");
                Thread.Sleep(250);
            }
            if (doc?.Root == null)
            {
                IsFileCorrupted = true;
                return;
            }
            if (hash == null)
            {
                StartHashDocTask(doc);
            }
            SubmarineElement = doc.Root;
        }

        private void Init()
        {
            DisplayName = TextManager.Get("Submarine.Name." + Name).Fallback(Name);

            Description = TextManager.Get("Submarine.Description." + Name).Fallback(SubmarineElement.GetAttributeString("description", ""));

            EqualityCheckVal = SubmarineElement.GetAttributeInt("checkval", 0);

            Price = SubmarineElement.GetAttributeInt("price", 1000);

            InitialSuppliesSpawned = SubmarineElement.GetAttributeBool("initialsuppliesspawned", false);
            NoItems = SubmarineElement.GetAttributeBool("noitems", false);
            LowFuel = SubmarineElement.GetAttributeBool("lowfuel", false);
            IsManuallyOutfitted = SubmarineElement.GetAttributeBool("ismanuallyoutfitted", false);

            GameVersion = new Version(SubmarineElement.GetAttributeString("gameversion", "0.0.0.0"));
            if (Enum.TryParse(SubmarineElement.GetAttributeString("tags", ""), out SubmarineTag tags))
            {
                Tags = tags;
            }
            Dimensions = SubmarineElement.GetAttributeVector2("dimensions", Vector2.Zero);
            CargoCapacity = SubmarineElement.GetAttributeInt("cargocapacity", -1);
            RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0);
            RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0);
            var recommendedCrewExperience = SubmarineElement.GetAttributeIdentifier("recommendedcrewexperience", CrewExperienceLevel.Unknown.ToIdentifier());
            // Backwards compatibility
            if (recommendedCrewExperience == "Beginner")
            {
                RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceLow;
            }
            else if (recommendedCrewExperience == "Intermediate")
            {
                RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceMid;
            }
            else if (recommendedCrewExperience == "Experienced")
            {
                RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceHigh;
            }
            else
            {
                Enum.TryParse(recommendedCrewExperience.Value, ignoreCase: true, out RecommendedCrewExperience);
            }
            Tier = SubmarineElement.GetAttributeInt("tier", GetDefaultTier(Price));

            if (SubmarineElement?.Attribute("type") != null)
            {
                if (Enum.TryParse(SubmarineElement.GetAttributeString("type", ""), out SubmarineType type))
                {
                    Type = type;
                    if (Type == SubmarineType.OutpostModule)
                    {
                        OutpostModuleInfo = new OutpostModuleInfo(this, SubmarineElement);
                    }
                    else if (Type == SubmarineType.BeaconStation)
                    {
                        BeaconStationInfo = new BeaconStationInfo(this, SubmarineElement);
                    }
                }
            }

            if (Type == SubmarineType.Player)
            {
                if (SubmarineElement?.Attribute("class") != null)
                {
                    string classStr = SubmarineElement.GetAttributeString("class", "Undefined");
                    if (classStr == "DeepDiver")
                    {
                        //backwards compatibility
                        SubmarineClass = SubmarineClass.Scout;
                    }
                    else if (Enum.TryParse(classStr, out SubmarineClass submarineClass))
                    {
                        SubmarineClass = submarineClass;
                    }
                }
            }
            else
            {
                SubmarineClass = SubmarineClass.Undefined;
            }

            RequiredContentPackages.Clear();
            string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", Array.Empty<string>());
            foreach (string contentPackageName in contentPackageNames)
            {
                RequiredContentPackages.Add(contentPackageName);
            }

            InitProjectSpecific();
        }

        partial void InitProjectSpecific();

        public void Dispose()
        {
#if CLIENT
            PreviewImage?.Remove();
            PreviewImage = null;
#endif
            if (savedSubmarines.Contains(this)) { savedSubmarines.Remove(this); }
        }

        public bool IsVanillaSubmarine()
        {
            if (FilePath == null) { return false; }
            var vanilla = GameMain.VanillaContent;
            if (vanilla != null)
            {
                var vanillaSubs = vanilla.GetFiles<BaseSubFile>();
                string pathToCompare = FilePath.CleanUpPath();
                if (vanillaSubs.Any(sub => sub.Path == pathToCompare))
                {
                    return true;
                }
            }
            return false;
        }

        public void StartHashDocTask(XDocument doc)
        {
            if (hash != null) { return; }
            if (hashTask != null) { return; }

            hashTask = new Task(() =>
            {
                hash = Md5Hash.CalculateForString(doc.ToString(), Md5Hash.StringHashOptions.IgnoreWhitespace);
            });
            hashTask.Start();
        }

        public bool HasTag(SubmarineTag tag)
        {
            return Tags.HasFlag(tag);
        }

        public void AddTag(SubmarineTag tag)
        {
            if (Tags.HasFlag(tag)) return;

            Tags |= tag;
        }

        public void RemoveTag(SubmarineTag tag)
        {
            if (!Tags.HasFlag(tag)) return;

            Tags &= ~tag;
        }

        public void CheckSubsLeftBehind(XElement element = null)
        {
            if (element == null) { element = SubmarineElement; }

            subsLeftBehind = false;
            LeftBehindSubDockingPortOccupied = false;
            LeftBehindDockingPortIDs.Clear();
            BlockedDockingPortIDs.Clear();
            foreach (var subElement in element.Elements())
            {
                if (!subElement.Name.ToString().Equals("linkedsubmarine", StringComparison.OrdinalIgnoreCase)) { continue; }
                if (subElement.Attribute("location") == null) { continue; }

                subsLeftBehind = true;
                ushort targetDockingPortID = (ushort)subElement.GetAttributeInt("originallinkedto", 0);
                LeftBehindDockingPortIDs.Add(targetDockingPortID);
                XElement targetPortElement = targetDockingPortID == 0 ? null :
                    element.Elements().FirstOrDefault(e => e.GetAttributeInt("ID", 0) == targetDockingPortID);
                if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", Array.Empty<int>()).Length > 0)
                {
                    BlockedDockingPortIDs.Add(targetDockingPortID);
                    LeftBehindSubDockingPortOccupied = true;
                }
            }
        }

        /// <summary>
        /// Calculated from <see cref="SubmarineElement"/>. Can be used when the sub hasn't been loaded and we can't access <see cref="Submarine.RealWorldCrushDepth"/>.
        /// </summary>
        public bool IsCrushDepthDefinedInStructures(out float realWorldCrushDepth)
        {
            if (SubmarineElement == null)
            {
                realWorldCrushDepth = Level.DefaultRealWorldCrushDepth;
                return false;
            }
            bool structureCrushDepthsDefined = false;
            realWorldCrushDepth = float.PositiveInfinity;
            foreach (var structureElement in SubmarineElement.GetChildElements("structure"))
            {
                string name = structureElement.Attribute("name")?.Value ?? "";
                Identifier identifier = structureElement.GetAttributeIdentifier("identifier", "");
                var structurePrefab = Structure.FindPrefab(name, identifier);
                if (structurePrefab == null || !structurePrefab.Body) { continue; }
                if (!structureCrushDepthsDefined && structureElement.Attribute("crushdepth") != null)
                {
                    structureCrushDepthsDefined = true;
                }
                float structureCrushDepth = structureElement.GetAttributeFloat("crushdepth", float.PositiveInfinity);
                realWorldCrushDepth = Math.Min(structureCrushDepth, realWorldCrushDepth);
            }
            if (!structureCrushDepthsDefined)
            {
                realWorldCrushDepth = Level.DefaultRealWorldCrushDepth;
            }
            return structureCrushDepthsDefined;
        }
        public void AddOutpostNPCIdentifierOrTag(Character npc, Identifier idOrTag)
        {
            if (!OutpostNPCs.ContainsKey(idOrTag))
            {
                OutpostNPCs.Add(idOrTag, new List<Character>());
            }
            OutpostNPCs[idOrTag].Add(npc);
        }

        //saving/loading ----------------------------------------------------
        public void SaveAs(string filePath, System.IO.MemoryStream previewImage = null)
        {
            var newElement = new XElement(
                SubmarineElement.Name, 
                SubmarineElement.Attributes()
                    .Where(a => 
                        !string.Equals(a.Name.LocalName, "previewimage", StringComparison.InvariantCultureIgnoreCase) &&
                        !string.Equals(a.Name.LocalName, "name", StringComparison.InvariantCultureIgnoreCase)), 
                SubmarineElement.Elements());

            if (Type == SubmarineType.OutpostModule)
            {
                OutpostModuleInfo.Save(newElement);
                OutpostModuleInfo = new OutpostModuleInfo(this, newElement);
            }
            else if (Type == SubmarineType.BeaconStation)
            {
                BeaconStationInfo.Save(newElement);
                BeaconStationInfo = new BeaconStationInfo(this, newElement);
            }
            XDocument doc = new XDocument(newElement);

            doc.Root.Add(new XAttribute("name", Name));
            if (previewImage != null && AllowPreviewImage)
            {
                doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray())));
            }

            SaveUtil.CompressStringToFile(filePath, doc.ToString());
        }

        public static void AddToSavedSubs(SubmarineInfo subInfo)
        {
            savedSubmarines.Add(subInfo);
        }

        public static void RemoveSavedSub(string filePath)
        {
            string fullPath = Path.GetFullPath(filePath);
            for (int i = savedSubmarines.Count - 1; i >= 0; i--)
            {
                if (Path.GetFullPath(savedSubmarines[i].FilePath) == fullPath)
                {
                    savedSubmarines[i].Dispose();
                }
            }
        }

        public static void RefreshSavedSub(string filePath)
        {
            RemoveSavedSub(filePath);
            if (File.Exists(filePath))
            {
                var subInfo = new SubmarineInfo(filePath);
                if (!subInfo.IsFileCorrupted)
                {
                    savedSubmarines.Add(subInfo);
                }
                savedSubmarines = savedSubmarines.OrderBy(s => s.FilePath ?? "").ToList();
            }
        }

        public static void RefreshSavedSubs()
        {
            var contentPackageSubs = ContentPackageManager.EnabledPackages.All.SelectMany(c => c.GetFiles<BaseSubFile>());

            for (int i = savedSubmarines.Count - 1; i >= 0; i--)
            {
                if (File.Exists(savedSubmarines[i].FilePath))
                {
                    bool isDownloadedSub = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SaveUtil.SubmarineDownloadFolder);
                    bool isInContentPackage = contentPackageSubs.Any(f => f.Path == savedSubmarines[i].FilePath);
                    if (isDownloadedSub) { continue; }
                    if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && isInContentPackage) { continue; }
                }
                savedSubmarines[i].Dispose();
            }

            List<string> filePaths = new List<string>();
            foreach (BaseSubFile subFile in contentPackageSubs)
            {
                if (!File.Exists(subFile.Path.Value)) { continue; }
                if (!filePaths.Any(fp => fp == subFile.Path))
                {
                    filePaths.Add(subFile.Path.Value);
                }
            }

            filePaths.RemoveAll(p => savedSubmarines.Any(sub => sub.FilePath == p));

            foreach (string path in filePaths)
            {
                var subInfo = new SubmarineInfo(path);
                if (!subInfo.IsFileCorrupted)
                {
                    savedSubmarines.Add(subInfo);
                }
            }
        }

        public static XDocument OpenFile(string file)
        {
            return OpenFile(file, out _);
        }

        public static XDocument OpenFile(string file, out Exception exception)
        {
            XDocument doc = null;
            string extension = "";
            exception = null;

            try
            {
                extension = System.IO.Path.GetExtension(file);
            }
            catch
            {
                //no file extension specified: try using the default one
                file += ".sub";
            }

            if (string.IsNullOrWhiteSpace(extension))
            {
                extension = ".sub";
                file += ".sub";
            }

            if (extension == ".sub")
            {
                System.IO.Stream stream;
                try
                {
                    stream = SaveUtil.DecompressFileToStream(file);
                }
                catch (System.IO.FileNotFoundException e)
                {
                    exception = e;
                    DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (File not found) " + Environment.StackTrace.CleanupStackTrace(), e);
                    return null;
                }
                catch (Exception e)
                {
                    exception = e;
                    DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed!", e);
                    return null;
                }

                try
                {
                    stream.Position = 0;
                    using (var reader = XMLExtensions.CreateReader(stream))
                    {
                        doc = XDocument.Load(reader);
                    }
                    stream.Close();
                    stream.Dispose();
                }

                catch (Exception e)
                {
                    exception = e;
                    DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (" + e.Message + ")");
                    return null;
                }
            }
            else if (extension == ".xml")
            {
                try
                {
                    ToolBox.IsProperFilenameCase(file);
                    using var stream = File.Open(file, System.IO.FileMode.Open, System.IO.FileAccess.Read);
                    using var reader = XMLExtensions.CreateReader(stream);
                    doc = XDocument.Load(reader);
                }
                catch (Exception e)
                {
                    exception = e;
                    DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (" + e.Message + ")");
                    return null;
                }
            }
            else
            {
                DebugConsole.ThrowError("Couldn't load submarine \"" + file + "! (Unrecognized file extension)");
                return null;
            }

            return doc;
        }

        public int GetPrice(Location location = null, ImmutableHashSet<Character> characterList = null)
        {
            if (location is null)
            {
                if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation is { } currentLocation)
                {
                    location = currentLocation;
                }
                else
                {

                    return Price;
                }
            }

            characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both);

            float price = Price;

            if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
            {
                price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated));
            }
            price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier));

            return (int)price;
        }

        public static int GetDefaultTier(int price) => price > 20000 ? HighestTier : price > 10000 ? 2 : 1;

        public const int HighestTier = 3;
    }
}
