using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

namespace Barotrauma
{
    partial class MultiPlayerCampaign : CampaignMode
    {
        [Flags]
        public enum NetFlags : UInt16
        {
            Misc = 0x1,
            MapAndMissions = 0x2,
            UpgradeManager = 0x4,
            SubList = 0x8,
            ItemsInBuyCrate = 0x10,
            ItemsInSellFromSubCrate = 0x20,
            PurchasedItems = 0x80,
            SoldItems = 0x100,
            Reputation = 0x200,
            CharacterInfo = 0x800
        }

        private readonly Dictionary<NetFlags, UInt16> lastUpdateID;

        public UInt16 GetLastUpdateIdForFlag(NetFlags flag)
        {
            if (!ValidateFlag(flag)) { return 0; }
            return lastUpdateID[flag];
        }
        public void SetLastUpdateIdForFlag(NetFlags flag, UInt16 id)
        {
            if (!ValidateFlag(flag)) { return; }
            lastUpdateID[flag] = id;
        }

        public void IncrementLastUpdateIdForFlag(NetFlags flag)
        {
            if (!ValidateFlag(flag)) { return; }
            if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; }
            lastUpdateID[flag]++;
        }
        public void IncrementAllLastUpdateIds()
        {
            foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags)))
            {
                if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; }
                lastUpdateID[flag]++;
            }
        }

        private static bool ValidateFlag(NetFlags flag)
        {
            if (MathHelper.IsPowerOfTwo((int)flag)) { return true; }
#if DEBUG
            throw new InvalidOperationException($"\"{flag}\" is not a valid campaign update flag.");
#else
            return false;
#endif       
        }


        private UInt16 lastSaveID;
        public UInt16 LastSaveID
        {
            get
            {
#if SERVER
                if (GameMain.Server != null && lastSaveID < 1) { lastSaveID++; }
#endif
                return lastSaveID;
            }
            set
            {
#if SERVER
                //trigger a campaign update to notify the clients of the changed save ID
                IncrementLastUpdateIdForFlag(NetFlags.Misc);
#endif
                lastSaveID = value; 
            }
        }
        
        private static byte currentCampaignID;

        public byte CampaignID
        {
            get; set;
        }

        public byte RoundID
        {
            get; set;
        }

        private MultiPlayerCampaign(CampaignSettings settings) : base(GameModePreset.MultiPlayerCampaign, settings)
        {
            currentCampaignID++;
            lastUpdateID = new Dictionary<NetFlags, ushort>();
            foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags)))
            {
#if SERVER
                //server starts from a higher ID to ensure we send the initial state
                lastUpdateID[flag] = 1;
#else
                lastUpdateID[flag] = 0;
#endif
            }
            CampaignID = currentCampaignID;
            UpgradeManager = new UpgradeManager(this);
            InitFactions();
        }

        public static MultiPlayerCampaign StartNew(string mapSeed, CampaignSettings settings)
        {
            MultiPlayerCampaign campaign = new MultiPlayerCampaign(settings);
            //only the server generates the map, the clients load it from a save file
            if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
            {
                campaign.map = new Map(campaign, mapSeed);
            }
            campaign.InitProjSpecific();
            return campaign;
        }

        public static MultiPlayerCampaign LoadNew(XElement element)
        {
            MultiPlayerCampaign campaign = new MultiPlayerCampaign(CampaignSettings.Empty);
            campaign.Load(element);
            campaign.InitProjSpecific();
            campaign.IsFirstRound = false;
            return campaign;
        }

        partial void InitProjSpecific();

        public static string GetCharacterDataSavePath(string loadPath)
        {
            string directory = Path.GetDirectoryName(loadPath);
            string fileName = Path.GetFileNameWithoutExtension(loadPath);

            if (CampaignDataPath.IsBackupPath(loadPath, out uint backupIndex))
            {
                string trimmedFileName = Path.GetFileNameWithoutExtension(fileName);
                return Path.Combine(directory, $"{trimmedFileName}_CharacterData{SaveUtil.BackupCharacterDataExtensionStart}{backupIndex}");
            }
            return Path.Combine(directory, $"{fileName}_CharacterData.xml");
        }

        public static string GetCharacterDataPathForLoading()
            => GetCharacterDataSavePath(GameMain.GameSession.DataPath.LoadPath);
        public static string GetCharacterDataPathForSaving()
            => GetCharacterDataSavePath(GameMain.GameSession.DataPath.SavePath);

        /// <summary>
        /// Loads the campaign from an XML element. Creates the map if it hasn't been created yet, otherwise updates the state of the map.
        /// </summary>
        private void Load(XElement element)
        {
            LoadSaveSharedSingleAndMultiplayer(element);

            foreach (var subElement in element.Elements())
            {
                switch (subElement.Name.ToString().ToLowerInvariant())
                {
                    case CampaignSettings.LowerCaseSaveElementName:
                        Settings = new CampaignSettings(subElement);
#if CLIENT
                        GameMain.NetworkMember.ServerSettings.CampaignSettings = Settings;
#endif
                        break;
                    case "map":
                        if (map == null)
                        {
                            //map not created yet, loading this campaign for the first time
                            map = Map.Load(this, subElement);
                        }
                        else
                        {
                            //map already created, update it
                            //if we're not downloading the initial save file (LastSaveID > 0), 
                            //show notifications about location type changes
                            map.LoadState(this, subElement, LastSaveID > 0);
                        }
                        break;
                    case "metadata":
                        var prevReputations = Factions.ToDictionary(k => k, v => v.Reputation.Value);
                        CampaignMetadata.Load(subElement);
                        foreach (var faction in Factions)
                        {
                            if (!MathUtils.NearlyEqual(prevReputations[faction], faction.Reputation.Value))
                            {
                                faction.Reputation.OnReputationValueChanged?.Invoke(faction.Reputation);
                                Reputation.OnAnyReputationValueChanged.Invoke(faction.Reputation);
                            }
                        }
                        break;
                    case "bots" when GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer:
                        CrewManager.HasBots = subElement.GetAttributeBool("hasbots", false);
                        CrewManager.AddCharacterElements(subElement);
                        ActiveOrdersElement = subElement.GetChildElement("activeorders");
                        break;
#if SERVER
                    case "traitormanager":
                        GameMain.Server?.TraitorManager?.Load(subElement);
                        break;
                    case "savedexperiencepoints":
                        foreach (XElement savedExp in subElement.Elements())
                        {
                            savedExperiencePoints.Add(new SavedExperiencePoints(savedExp));
                        }
                        break;
#endif
                }
            }

            UpgradeManager ??= new UpgradeManager(this);

#if SERVER
            characterData.Clear();
            string characterDataPath = GetCharacterDataPathForLoading();
            if (!File.Exists(characterDataPath))
            {
                DebugConsole.ThrowError($"Failed to load the character data for the campaign. Could not find the file \"{characterDataPath}\".");
            }
            else
            {
                var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath);
                if (characterDataDoc?.Root == null) { return; }
                foreach (var subElement in characterDataDoc.Root.Elements())
                {
                    characterData.Add(new CharacterCampaignData(subElement));
                }
            }
#endif
        }
        
        public static List<SubmarineInfo> GetCampaignSubs()
        {
            bool isSubmarineVisible(SubmarineInfo s)
                => !GameMain.NetworkMember.ServerSettings.HiddenSubs.Any(h
                    => s.Name.Equals(h, StringComparison.OrdinalIgnoreCase));

            var availableSubs = SubmarineInfo.SavedSubmarines;
#if CLIENT
            if (GameMain.Client != null)
            {
                availableSubs = GameMain.Client.ServerSubmarines;
            }
#endif

            List<SubmarineInfo> campaignSubs =
                availableSubs
                    .Where(s =>
                        s.IsCampaignCompatible
                        && isSubmarineVisible(s))
                    .ToList();

            if (!campaignSubs.Any())
            {
                //None of the available subs were marked as campaign-compatible, just include all visible subs
                campaignSubs.AddRange(availableSubs.Where(isSubmarineVisible));
            }

            if (!campaignSubs.Any())
            {
                //No subs are visible at all! Just make the selected one available
                campaignSubs.Add(GameMain.NetLobbyScreen.SelectedSub);
            }

            return campaignSubs;
        }

        private static void WriteItems(IWriteMessage msg, Dictionary<Identifier, List<PurchasedItem>> purchasedItems)
        {
            msg.WriteByte((byte)purchasedItems.Count);
            foreach (var storeItems in purchasedItems)
            {
                msg.WriteIdentifier(storeItems.Key);
                msg.WriteUInt16((UInt16)storeItems.Value.Count);
                foreach (var item in storeItems.Value)
                {
                    msg.WriteIdentifier(item.ItemPrefabIdentifier);
                    msg.WriteBoolean(item.DeliverImmediately);
                    msg.WriteRangedInteger(item.Quantity, 0, CargoManager.MaxQuantity);
                }
            }
        }

        private static Dictionary<Identifier, List<PurchasedItem>> ReadPurchasedItems(IReadMessage msg, Client sender)
        {
            var items = new Dictionary<Identifier, List<PurchasedItem>>();
            byte storeCount = msg.ReadByte();
            for (int i = 0; i < storeCount; i++)
            {
                Identifier storeId = msg.ReadIdentifier();
                items.Add(storeId, new List<PurchasedItem>());
                UInt16 itemCount = msg.ReadUInt16();
                for (int j = 0; j < itemCount; j++)
                {
                    Identifier itemId = msg.ReadIdentifier();
                    bool deliverImmediately = msg.ReadBoolean();
#if SERVER
                    if (!AllowImmediateItemDelivery(sender)) { deliverImmediately = false; }
#endif
                    int quantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity);
                    items[storeId].Add(new PurchasedItem(itemId, quantity, sender) { DeliverImmediately = deliverImmediately });
                }
            }
            return items;
        }

        private static void WriteItems(IWriteMessage msg, Dictionary<Identifier, List<SoldItem>> soldItems)
        {
            msg.WriteByte((byte)soldItems.Count);
            foreach (var storeItems in soldItems)
            {
                msg.WriteIdentifier(storeItems.Key);
                msg.WriteUInt16((UInt16)storeItems.Value.Count);
                foreach (var item in storeItems.Value)
                {
                    msg.WriteIdentifier(item.ItemPrefab.Identifier);
                    msg.WriteUInt16((UInt16)item.ID);
                    msg.WriteBoolean(item.Removed);
                    msg.WriteByte(item.SellerID);
                    msg.WriteByte((byte)item.Origin);
                }
            }
        }

        private static Dictionary<Identifier, List<SoldItem>> ReadSoldItems(IReadMessage msg)
        {
            var soldItems = new Dictionary<Identifier, List<SoldItem>>();
            byte storeCount = msg.ReadByte();
            for (int i = 0; i < storeCount; i++)
            {
                Identifier storeId = msg.ReadIdentifier();
                soldItems.Add(storeId, new List<SoldItem>());
                UInt16 itemCount = msg.ReadUInt16();
                for (int j = 0; j < itemCount; j++)
                {
                    Identifier prefabId = msg.ReadIdentifier();
                    UInt16 itemId = msg.ReadUInt16();
                    bool removed = msg.ReadBoolean();
                    byte sellerId = msg.ReadByte();
                    byte origin = msg.ReadByte();
                    soldItems[storeId].Add(new SoldItem(ItemPrefab.Prefabs[prefabId], itemId, removed, sellerId, (SoldItem.SellOrigin)origin));
                }
            }
            return soldItems;
        }
    }
}
