using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;

namespace Barotrauma
{
    partial class LinkedSubmarinePrefab : MapEntityPrefab
    {
        public override void Dispose() { }

        public readonly SubmarineInfo subInfo;

        public override Sprite Sprite => null;

        public override string OriginalName => Name.Value;

        public override LocalizedString Name => subInfo.Name;

        public override ImmutableHashSet<Identifier> Tags => null;

        public override ImmutableHashSet<Identifier> AllowedLinks => null;

        public override MapEntityCategory Category => MapEntityCategory.Misc;

        public override ImmutableHashSet<string> Aliases { get; }

        public LinkedSubmarinePrefab(SubmarineInfo subInfo) : base(subInfo.Name.ToIdentifier())
        {
            this.subInfo = subInfo;

            Aliases = Name.Value.ToEnumerable().ToImmutableHashSet();
        }

        protected override void CreateInstance(Rectangle rect)
        {
            System.Diagnostics.Debug.Assert(Submarine.MainSub != null);
            LinkedSubmarine.CreateDummy(Submarine.MainSub, subInfo.FilePath, rect.Location.ToVector2());
        }
    }

    partial class LinkedSubmarine : MapEntity
    {
        private List<Vector2> wallVertices;

        private string filePath;

        private bool loadSub;
        public bool LoadSub => loadSub;
        private Submarine sub;

        private ushort originalMyPortID;

        //the ID of the docking port the sub was docked to in the original sub file
        //(needed when replacing a lost sub)
        private ushort originalLinkedToID;
        public ushort OriginalLinkedToID => originalLinkedToID;
        private DockingPort originalLinkedPort;

        private bool purchasedLostShuttles;

        public Submarine Sub
        {
            get
            {
                return sub;
            }
        }

        private XElement saveElement;

        private Vector2? positionRelativeToMainSub;

        public override bool Linkable
        {
            get
            {
                return true;
            }
        }

        public int CargoCapacity { get; private set; }
        
        public LinkedSubmarine(Submarine submarine, ushort id = Entity.NullEntityID)
            : base(null, submarine, id)
        {
            linkedToID = new List<ushort>();

            InsertToList();

            DebugConsole.Log("Created linked submarine (" + ID + ")");
        }

        public static LinkedSubmarine CreateDummy(Submarine mainSub, Submarine linkedSub)
        {
            LinkedSubmarine sl = new LinkedSubmarine(mainSub)
            {
                sub = linkedSub
            };

            return sl;
        }
        
        public static LinkedSubmarine CreateDummy(Submarine mainSub, string filePath, Vector2 position)
        {
            XDocument doc = SubmarineInfo.OpenFile(filePath);
            if (doc == null || doc.Root == null) return null;

            LinkedSubmarine sl = CreateDummy(mainSub, doc.Root, position);
            sl.filePath = filePath;
            sl.saveElement = doc.Root;
            sl.saveElement.Name = "LinkedSubmarine";
            sl.saveElement.SetAttributeValue("filepath", filePath);

            return sl;
        }

        public static LinkedSubmarine CreateDummy(Submarine mainSub, XElement element, Vector2 position, ushort id = Entity.NullEntityID)
        {
            LinkedSubmarine sl = new LinkedSubmarine(mainSub, id);
            sl.GenerateWallVertices(element);
            sl.CargoCapacity = element.GetAttributeInt("cargocapacity", 0);
            if (sl.wallVertices.Any())
            {
                sl.Rect = new Rectangle(
                    (int)sl.wallVertices.Min(v => v.X + position.X),
                    (int)sl.wallVertices.Max(v => v.Y + position.Y),
                    (int)sl.wallVertices.Max(v => v.X + position.X),
                    (int)sl.wallVertices.Min(v => v.Y + position.Y));

                int width = sl.rect.Width - sl.rect.X;
                int height = sl.rect.Y - sl.rect.Height;
                sl.Rect = new Rectangle((int)(position.X - width / 2), (int)(position.Y + height / 2), width, height);
            }
            else
            {
                sl.Rect = new Rectangle((int)position.X, (int)position.Y, 10, 10);
            }
            return sl;
        }

        public override bool IsMouseOn(Vector2 position)
        {
            return Vector2.Distance(position, WorldPosition) < 50.0f;
        }

        public override MapEntity Clone()
        {
            XElement cloneElement = new XElement(saveElement);
            LinkedSubmarine sl = CreateDummy(Submarine, cloneElement, Position);
            sl.saveElement = cloneElement;
            sl.filePath = filePath;
            return sl;
        }

        private void GenerateWallVertices(XElement rootElement)
        {
            List<Vector2> points = new List<Vector2>();

            foreach (XElement element in rootElement.Elements())
            {
                if (element.Name != "Structure") { continue; }

                string name = element.GetAttributeString("name", "");
                Identifier identifier = element.GetAttributeIdentifier("identifier", "");

                StructurePrefab prefab = Structure.FindPrefab(name, identifier);
                if (prefab == null) { continue; }

                float scale = element.GetAttributeFloat("scale", prefab.Scale);

                var rect = element.GetAttributeVector4("rect", Vector4.Zero);
                if (!prefab.ResizeHorizontal) { rect.Z *= scale / prefab.Scale; }
                if (!prefab.ResizeVertical) { rect.W *= scale / prefab.Scale; }

                points.Add(new Vector2(rect.X, rect.Y));
                points.Add(new Vector2(rect.X + rect.Z, rect.Y));
                points.Add(new Vector2(rect.X, rect.Y - rect.W));
                points.Add(new Vector2(rect.X + rect.Z, rect.Y - rect.W));
            }

            wallVertices = MathUtils.GiftWrap(points);
        }

        // LinkedSubmarine.Load() is called from MapEntity.LoadAll()
        public static LinkedSubmarine Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
        {
            Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero);
            LinkedSubmarine linkedSub;
            idRemap.AssignMaxId(out ushort id);
            if (Screen.Selected == GameMain.SubEditorScreen)
            {
                linkedSub = CreateDummy(submarine, element, pos, id);
                linkedSub.saveElement = new XElement(element);
                linkedSub.purchasedLostShuttles = false;
            }
            else
            {
                string levelSeed = element.GetAttributeString("location", "");
                LevelData levelData = GameMain.GameSession?.Campaign?.NextLevel ?? GameMain.GameSession?.LevelData;
                linkedSub = new LinkedSubmarine(submarine, id)
                {
                    purchasedLostShuttles = 
                        (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles) || 
                        element.GetAttributeBool("purchasedlostshuttle", false),
                    saveElement = new XElement(element)
                };

                bool levelMatches = string.IsNullOrWhiteSpace(levelSeed) || levelData == null || levelData.Seed == levelSeed;

                //don't load a sub that was left in this level if we have a submarine switch pending
                //to make sure it gets ignored during the submarine switch and item transfer (reloading and saving it during the switch makes it not considered "left behind")
                if ((levelMatches || linkedSub.purchasedLostShuttles) && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null)
                {
                    linkedSub.loadSub = true;
                    linkedSub.rect.Location = MathUtils.ToPoint(pos);
                }
                else
                {
                    linkedSub.loadSub = false;
                }
            }

            #warning TODO: revise
            linkedSub.filePath = element.GetAttributeContentPath("filepath")?.Value ?? string.Empty;
            int[] linkedToIds = element.GetAttributeIntArray("linkedto", Array.Empty<int>());
            for (int i = 0; i < linkedToIds.Length; i++)
            {
                linkedSub.linkedToID.Add(idRemap.GetOffsetId(linkedToIds[i]));
            }
            linkedSub.originalLinkedToID = idRemap.GetOffsetId(element.GetAttributeInt("originallinkedto", 0));
            linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0);
            linkedSub.CargoCapacity = element.GetAttributeInt("cargocapacity", 0);

            return linkedSub.loadSub ? linkedSub : null;
        }

        public void LinkDummyToMainSubmarine()
        {
            if (Screen.Selected != GameMain.SubEditorScreen) { return; }            
            for (int i = 0; i < linkedToID.Count; i++)
            {
                if (FindEntityByID(linkedToID[i]) is MapEntity linked)
                {
                    linkedTo.Add(linked);
                }
            }            
        }

        public void SetPositionRelativeToMainSub()
        {
            if (positionRelativeToMainSub.HasValue)
            {
                Sub.SetPosition(Submarine.WorldPosition + positionRelativeToMainSub.Value);
            }
            positionRelativeToMainSub = null;
        }

        public override void OnMapLoaded()
        {
            if (!loadSub) { return; }

            SubmarineInfo info = new SubmarineInfo(Submarine.Info.FilePath, "", saveElement);
            if (!info.SubmarineElement.HasElements)
            {
                DebugConsole.ThrowError("Failed to load a linked submarine (empty XML element). The save file may be corrupted.");
                return;
            }
            if (!info.SubmarineElement.Elements().Any(e => e.Name.ToString().Equals("hull", StringComparison.OrdinalIgnoreCase)))
            {
                DebugConsole.ThrowError("Failed to load a linked submarine (the submarine contains no hulls).");
                return;
            }

            saveElement.Attribute("purchasedlostshuttle")?.Remove();

            IdRemap parentRemap = new IdRemap(Submarine.Info.SubmarineElement, Submarine.IdOffset);
            sub = Submarine.Load(info, false, parentRemap);
            sub.Info.SubmarineClass = Submarine.Info.SubmarineClass;
            if (Submarine.Info.IsOutpost && Submarine.TeamID == CharacterTeamType.FriendlyNPC)
            {
                sub.TeamID = CharacterTeamType.FriendlyNPC;
            }

            IdRemap childRemap = new IdRemap(saveElement, sub.IdOffset);

            Vector2 worldPos = saveElement.GetAttributeVector2("worldpos", Vector2.Zero);
            if (worldPos != Vector2.Zero)
            {
                if (GameMain.GameSession != null && GameMain.GameSession.MirrorLevel)
                {
                    worldPos.X = GameMain.GameSession.LevelData.Size.X - worldPos.X;
                }
                sub.SetPosition(worldPos);
            }
            else
            {
                sub.SetPosition(WorldPosition);
            }

            DockingPort linkedPort = null;
            DockingPort myPort = null;
            
            MapEntity linkedItem = linkedTo.FirstOrDefault(lt => (lt as Item)?.GetComponent<DockingPort>() != null);
            if (linkedItem == null)
            {
                linkedPort = DockingPort.List.FirstOrDefault(dp => dp.DockingTarget != null && dp.DockingTarget.Item.Submarine == sub);
            }
            else
            {
                linkedPort = ((Item)linkedItem).GetComponent<DockingPort>();
            }

            if (linkedPort == null)
            {
                if (purchasedLostShuttles)
                {
                    linkedPort = (FindEntityByID(originalLinkedToID) as Item)?.GetComponent<DockingPort>();
                }
            }
                
            if (linkedPort == null) 
            {
                if (worldPos == Vector2.Zero)
                {
                    Vector2 relativePos = saveElement.GetAttributeVector2("posrelativetomainsub", Vector2.Zero);
                    if (relativePos != Vector2.Zero)
                    {
                        positionRelativeToMainSub = relativePos;
                    }
                    else
                    {
                        DebugConsole.ThrowError("Something went wrong when loading a linked submarine - the save didn't include a world position, a linked port or position relative to the main sub.");
                    }
                }
                else
                {
                    sub.Submarine = Submarine;
                }
                return; 
            }

            originalLinkedPort = linkedPort;

            ushort originalMyId = childRemap.GetOffsetId(originalMyPortID);
            myPort = (FindEntityByID(originalMyId) as Item)?.GetComponent<DockingPort>();
            if (myPort == null)
            {
                float closestDistance = 0.0f;
                foreach (DockingPort port in DockingPort.List)
                {
                    if (port.Item.Submarine != sub) { continue; }
                    if (port.IsHorizontal != linkedPort.IsHorizontal) { continue; }
                    if (port.ForceDockingDirection != DockingPort.DirectionType.None && port.ForceDockingDirection == linkedPort.ForceDockingDirection) { continue; }
                    float dist = Vector2.Distance(port.Item.WorldPosition, linkedPort.Item.WorldPosition);
                    if (myPort == null || dist < closestDistance)
                    {
                        myPort = port;
                        closestDistance = dist;
                    }
                }
            }

            if (myPort != null)
            {
                originalMyPortID = myPort.Item.ID;

                myPort.Undock(applyEffects: false);
                myPort.DockingDir = 0;

                //something else is already docked to the port this sub should be docked to
                //may happen if a shuttle is lost, another vehicle docked to where the shuttle used to be,
                //and the shuttle is then restored in the campaign mode
                //or if the user connects multiple subs to the same docking ports in the sub editor
                if (linkedPort.Docked && linkedPort.DockingTarget != null && linkedPort.DockingTarget != myPort)
                {
                    //just spawn below the main sub
                    sub.SetPosition(
                        linkedPort.Item.Submarine.WorldPosition - 
                        new Vector2(0, linkedPort.Item.Submarine.GetDockedBorders().Height / 2 + sub.GetDockedBorders().Height / 2));
                }
                else
                {
                    Vector2 portDiff = myPort.Item.WorldPosition - sub.WorldPosition;
                    Vector2 offset = myPort.IsHorizontal ?
                        Vector2.UnitX * myPort.GetDir(linkedPort) :
                        Vector2.UnitY * myPort.GetDir(linkedPort);
                    offset *= myPort.DockedDistance;

                    sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset);

                    myPort.Dock(linkedPort);
                    myPort.Lock(isNetworkMessage: true, applyEffects: false);
                }
            }

            if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles)
            {
                foreach (Structure wall in Structure.WallList)
                {
                    if (wall.Submarine != sub) { continue; }
                    for (int i = 0; i < wall.SectionCount; i++)
                    {
                        wall.SetDamage(i, 0, createNetworkEvent: false, createExplosionEffect: false);
                    }                    
                }
                foreach (Hull hull in Hull.HullList)
                {
                    if (hull.Submarine != sub) { continue; }
                    hull.WaterVolume = 0.0f;
                    hull.OxygenPercentage = 100.0f;
                    hull.BallastFlora?.Kill();
                }
            }

            sub.SetPosition(sub.WorldPosition - Submarine.WorldPosition, forceUndockFromStaticSubmarines: false);
            sub.Submarine = Submarine;
        }

        public override XElement Save(XElement parentElement)
        {
            XElement saveElement = null;

            if (sub == null)
            {
                if (this.saveElement == null)
                {
                    var doc = SubmarineInfo.OpenFile(filePath);
                    saveElement = doc.Root;
                    saveElement.Add(new XAttribute("filepath", filePath));
                }
                else
                {
                    saveElement = this.saveElement;
                }
                saveElement.Name = "LinkedSubmarine";

                if (saveElement.Attribute("previewimage") != null)
                {
                    saveElement.Attribute("previewimage").Remove();
                }

                if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles)
                {
                    saveElement.SetAttributeValue("purchasedlostshuttle", true);
                }

                saveElement.SetAttributeValue("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition));

            }
            else
            {
                saveElement = new XElement("LinkedSubmarine");
                sub.SaveToXElement(saveElement);
            }
            if (linkedTo.Any() || linkedToID.Any())
            {
                var linkedPort = 
                    linkedTo.FirstOrDefault(lt => (lt is Item item) && item.GetComponent<DockingPort>() != null) ?? 
                    FindEntityByID(linkedToID.First()) as MapEntity;
                if (linkedPort != null)
                {
                    saveElement.SetAttributeValue("linkedto", linkedPort.ID);
                }
            }

            saveElement.SetAttributeValue("originallinkedto", originalLinkedPort != null ? originalLinkedPort.Item.ID : originalLinkedToID);
            saveElement.SetAttributeValue("originalmyport", originalMyPortID);

            if (sub != null)
            {
                bool leaveBehind = false;
                if (sub.Submarine != null && !sub.DockedTo.Contains(sub.Submarine))
                {
                    System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEitherExit);
                    if (Submarine.MainSub.AtEndExit)
                    {
                        leaveBehind = sub.AtEndExit != Submarine.MainSub.AtEndExit;
                    }
                    else
                    {
                        leaveBehind = sub.AtStartExit != Submarine.MainSub.AtStartExit;
                    }
                }

                if (leaveBehind)
                {
                    saveElement.SetAttributeValue("location", Level.Loaded.Seed);
                    Vector2 position = sub.SubBody.Position;
                    if (Level.Loaded.Mirrored)
                    {
                        position.X = Level.Loaded.Size.X - position.X;
                    }
                    saveElement.SetAttributeValue("worldpos", XMLExtensions.Vector2ToString(position));
                }
                else
                {
                    if (saveElement.Attribute("location") != null) { saveElement.Attribute("location").Remove(); }
                    if (saveElement.Attribute("worldpos") != null) { saveElement.Attribute("worldpos").Remove(); }
                    saveElement.SetAttributeValue("posrelativetomainsub", XMLExtensions.Vector2ToString(sub.WorldPosition - Submarine.WorldPosition));
                }
                saveElement.SetAttributeValue("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition));
            }

            parentElement.Add(saveElement);

            return saveElement;
        }
    }
}
