using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;

namespace Barotrauma
{
    public enum ActionType
    {
        Always, OnPicked, OnUse, OnSecondaryUse,
        OnWearing, OnContaining, OnContained, OnNotContained,
        OnActive, OnFailure, OnBroken, 
        OnFire, InWater, NotInWater,
        OnImpact,
        OnEating,
        OnDeath = OnBroken
    }

    partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable
    {
        public static List<Item> ItemList = new List<Item>();
        public ItemPrefab Prefab => prefab as ItemPrefab;

        public static bool ShowLinks = true;
        
        private HashSet<string> tags;

        private Hull currentHull;
        public Hull CurrentHull
        {
            get { return currentHull; }
            set
            {
                currentHull = value;
                ParentRuin = currentHull?.ParentRuin;
            }
        }
        
        public bool Visible = true;

        public SpriteEffects SpriteEffects = SpriteEffects.None;

        //components that determine the functionality of the item
        private Dictionary<Type, ItemComponent> componentsByType = new Dictionary<Type, ItemComponent>();
        private List<ItemComponent> components;
        private List<IDrawableComponent> drawableComponents;

        public PhysicsBody body;

        public readonly XElement StaticBodyConfig;

        private float lastSentCondition;
        private float sendConditionUpdateTimer;
        private bool conditionUpdatePending;

        private float condition;

        private bool inWater;
                
        private Inventory parentInventory;
        private Inventory ownInventory;
        
        private Dictionary<string, Connection> connections;

        private List<Repairable> repairables;

        //a dictionary containing lists of the status effects in all the components of the item
        private Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
        
        public Dictionary<string, SerializableProperty> SerializableProperties { get; protected set; }

        private bool? hasInGameEditableProperties;
        bool HasInGameEditableProperties
        {
            get
            {
                if (hasInGameEditableProperties == null)
                {
                    hasInGameEditableProperties = false;
                    if (SerializableProperties.Values.Any(p => p.Attributes.OfType<InGameEditable>().Any()))
                    {
                        hasInGameEditableProperties = true;
                    }
                    else
                    {
                        foreach (ItemComponent component in components)
                        {
                            if (!component.AllowInGameEditing) { continue; }
                            if (component.SerializableProperties.Values.Any(p => p.Attributes.OfType<InGameEditable>().Any()))
                            {
                                hasInGameEditableProperties = true;
                                break;
                            }
                        }
                    }
                }
                return (bool)hasInGameEditableProperties;
            }
        }

        //the inventory in which the item is contained in
        public Inventory ParentInventory
        {
            get
            {
                return parentInventory;
            }
            set
            {
                parentInventory = value;

                if (parentInventory != null) Container = parentInventory.Owner as Item;                
            }
        }

        private Item container;
        public Item Container
        {
            get { return container; }
            private set
            {
                if (value != container)
                {
                    container = value;
                    SetActiveSprite();
                }
            }
        }
                
        public override string Name
        {
            get { return prefab.Name; }
        }

        private string description;
        public string Description
        {
            get { return description ?? prefab.Description; }
            set { description = value; }
        }

        private bool hiddenInGame;
        [Editable, Serialize(false, true)]
        public bool HiddenInGame
        {
            get { return hiddenInGame; }
            set { hiddenInGame = value; }
        }

        public float ImpactTolerance
        {
            get { return Prefab.ImpactTolerance; }
        }
        
        public float InteractDistance
        {
            get { return Prefab.InteractDistance; }
        }

        public float InteractPriority
        {
            get { return Prefab.InteractPriority; }
        }

        public override Vector2 Position
        {
            get
            {
                return (body == null) ? base.Position : ConvertUnits.ToDisplayUnits(SimPosition);
            }
        }

        public override Vector2 SimPosition
        {
            get
            {
                return (body == null) ? base.SimPosition : body.SimPosition;
            }
        }

        public Rectangle InteractionRect
        {
            get
            {
                return WorldRect;
            }
        }

        public float PositionUpdateInterval
        {
            get;
            private set;
        } = float.PositiveInfinity;

        protected Color spriteColor;
        [Editable, Serialize("1.0,1.0,1.0,1.0", true)]
        public Color SpriteColor
        {
            get { return spriteColor; }
            set { spriteColor = value; }
        }

        [Serialize("1.0,1.0,1.0,1.0", true), Editable]
        public Color InventoryIconColor
        {
            get;
            protected set;
        }
        
        [Serialize("1.0,1.0,1.0,1.0", true), Editable(ToolTip = "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")]
        public Color ContainerColor
        {
            get;
            protected set;
        }

        [Serialize("", false)]
        /// <summary>
        /// Can be used by status effects or conditionals to check what item this item is contained inside
        /// </summary>
        public string ContainerIdentifier
        {
            get { return Container?.prefab.Identifier ?? ""; }
            set { /*do nothing*/ }
        }

        /// <summary>
        /// Should the item's Use method be called with the "Use" or with the "Shoot" key?
        /// </summary>
        [Serialize(false, false)]
        public bool IsShootable { get; set; }

        /// <summary>
        /// If true, the user has to hold the "aim" key before use is registered. False by default.
        /// </summary>
        [Serialize(false, false)]
        public bool RequireAimToUse
        {
            get; set;
        }

        /// <summary>
        /// If true, the user has to hold the "aim" key before secondary use is registered. True by default.
        /// </summary>
        [Serialize(true, false)]
        public bool RequireAimToSecondaryUse
        {
            get; set;
        }

        public Color Color
        {
            get { return spriteColor; }
        }

        public bool IsFullCondition => Condition >= MaxCondition;
        public float MaxCondition => Prefab.Health;
        public float ConditionPercentage => MathUtils.Percentage(Condition, MaxCondition);

        //the default value should be Prefab.Health, but because we can't use it in the attribute, 
        //we'll just use NaN (which does nothing) and set the default value in the constructor/load
        [Serialize(float.NaN, false), Editable]
        public float Condition
        {
            get { return condition; }
            set 
            {
#if CLIENT
                if (GameMain.Client != null) return;
#endif
                if (!MathUtils.IsValid(value)) return;
                if (Indestructible) return;

                float prev = condition;
                condition = MathHelper.Clamp(value, 0.0f, Prefab.Health);
                if (condition == 0.0f && prev > 0.0f)
                {
#if CLIENT
                    foreach (ItemComponent ic in components)
                    {
                        ic.PlaySound(ActionType.OnBroken, WorldPosition);
                    }
                    if (Screen.Selected == GameMain.SubEditorScreen) return;
#endif
                    ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
                }
                
                SetActiveSprite();

                if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !MathUtils.NearlyEqual(lastSentCondition, condition))
                {
                    if (Math.Abs(lastSentCondition - condition) > 1.0f || condition == 0.0f || condition == Prefab.Health)
                    {
                        conditionUpdatePending = true;
                    }
                }
            }
        }

        public float Health
        {
            get { return condition; }
        }

        private bool? indestructible;
        /// <summary>
        /// Per-instance value - if not set, the value of the prefab is used.
        /// </summary>
        public bool Indestructible
        {
            get { return indestructible ?? Prefab.Indestructible; }
            set { indestructible = value; }
        }

        [Editable, Serialize("", true)]
        public string Tags
        {
            get { return string.Join(",", tags); }
            set
            {
                tags.Clear();
                // Always add prefab tags
                prefab.Tags.ForEach(t => tags.Add(t));
                if (!string.IsNullOrWhiteSpace(value))
                {
                    string[] splitTags = value.Split(',');
                    foreach (string tag in splitTags)
                    {
                        string[] splitTag = tag.Split(':');
                        splitTag[0] = splitTag[0].ToLowerInvariant();
                        tags.Add(string.Join(":", splitTag));
                    }
                }
            }
        }

        public bool FireProof
        {
            get { return Prefab.FireProof; }
        }

        public bool WaterProof
        {
            get { return Prefab.WaterProof; }
        }

        public bool UseInHealthInterface
        {
            get { return Prefab.UseInHealthInterface; }
        }

        public bool InWater
        {
            get 
            { 
                //if the item has an active physics body, inWater is updated in the Update method
                if (body != null && body.Enabled) return inWater;

                //if not, we'll just have to check
                return IsInWater();
            }
        }

        /// <summary>
        /// A list of items the last signal sent by this item went through
        /// </summary>
        public List<Item> LastSentSignalRecipients
        {
            get;
            private set;
        } = new List<Item>();

        public string ConfigFile
        {
            get { return Prefab.ConfigFile; }
        }

        //which type of inventory slots (head, torso, any, etc) the item can be placed in
        public List<InvSlotType> AllowedSlots
        {
            get
            {
                Pickable p = GetComponent<Pickable>();
                return (p == null) ? new List<InvSlotType>() { InvSlotType.Any } : p.AllowedSlots;
            }
        }

        public List<Connection> Connections
        {
            get
            {
                ConnectionPanel panel = GetComponent<ConnectionPanel>();
                if (panel == null) return null;
                return panel.Connections;
            }
        }

        public IEnumerable<Item> ContainedItems
        {
            get
            {
                return ownInventory?.Items.Where(i => i != null);
            }
        }

        public Inventory OwnInventory
        {
            get { return ownInventory; }
        }

        [Serialize(false, true), Editable(ToolTip =
            "Enable if you want to display the item HUD side by side with another item's HUD, when linked together. " +
            "Disclaimer: It's possible or even likely that the views block each other, if they were not designed to be viewed together!")]
        public bool DisplaySideBySideWhenLinked { get; set; }

        public IEnumerable<Repairable> Repairables
        {
            get { return repairables; }
        }

        public IEnumerable<ItemComponent> Components
        {
            get { return components; }
        }

        public override bool Linkable
        {
            get { return Prefab.Linkable; }
        }

        public override string ToString()
        {
#if CLIENT
            return (GameMain.DebugDraw) ? Name + " (ID: " + ID + ")" : Name;
#elif SERVER
            return Name + " (ID: " + ID + ")";
#endif
        }

        private readonly List<ISerializableEntity> allPropertyObjects = new List<ISerializableEntity>();
        public IEnumerable<ISerializableEntity> AllPropertyObjects
        {
            get { return allPropertyObjects; }
        }

        public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine)
            : this(new Rectangle(
                (int)(position.X - itemPrefab.sprite.size.X / 2 * itemPrefab.Scale), 
                (int)(position.Y + itemPrefab.sprite.size.Y / 2 * itemPrefab.Scale), 
                (int)(itemPrefab.sprite.size.X * itemPrefab.Scale), 
                (int)(itemPrefab.sprite.size.Y * itemPrefab.Scale)), 
            itemPrefab, submarine)
        {

        }

        /// <summary>
        /// Creates a new item
        /// </summary>
        /// <param name="callOnItemLoaded">Should the OnItemLoaded methods of the ItemComponents be called. Use false if the item needs additional initialization before it can be considered fully loaded (e.g. when loading an item from a sub file or cloning an item).</param>
        public Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool callOnItemLoaded = true)
            : base(itemPrefab, submarine)
        {
            spriteColor = prefab.SpriteColor;

            linkedTo            = new ObservableCollection<MapEntity>();
            components          = new List<ItemComponent>();
            drawableComponents  = new List<IDrawableComponent>();
            tags                = new HashSet<string>();
            repairables         = new List<Repairable>();
                       
            rect = newRect;
                        
            condition = itemPrefab.Health;
            lastSentCondition = condition;

            allPropertyObjects.Add(this);

            XElement element = itemPrefab.ConfigElement;
            if (element == null) return;

            SerializableProperties = SerializableProperty.DeserializeProperties(this, element);

            if (submarine == null || !submarine.Loading) FindHull();

            SetActiveSprite();

            foreach (XElement subElement in element.Elements())
            {
                switch (subElement.Name.ToString().ToLowerInvariant())
                {
                    case "body":
                        body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale);
                        body.FarseerBody.AngularDamping = 0.2f;
                        body.FarseerBody.LinearDamping  = 0.1f;
                        break;
                    case "trigger":
                    case "inventoryicon":
                    case "sprite":
                    case "deconstruct":
                    case "brokensprite":
                    case "decorativesprite":
                    case "price":
                    case "levelcommonness":
                    case "suitabletreatment":
                    case "containedsprite":
                    case "fabricate":
                    case "fabricable":
                    case "fabricableitem":
                        break;
                    case "staticbody":
                        StaticBodyConfig = subElement;
                        break;
                    case "aitarget":
                        aiTarget = new AITarget(this, subElement);
                        break;
                    default:
                        ItemComponent ic = ItemComponent.Load(subElement, this, itemPrefab.ConfigFile);
                        if (ic == null) break;

                        AddComponent(ic);

                        if (ic is IDrawableComponent && ic.Drawable) drawableComponents.Add(ic as IDrawableComponent);
                        if (ic is Repairable) repairables.Add((Repairable)ic);
                        break;
                }
            }

            foreach (ItemComponent ic in components)
            {
                if (ic.statusEffectLists == null) continue;

                if (statusEffectLists == null)
                    statusEffectLists = new Dictionary<ActionType, List<StatusEffect>>();

                //go through all the status effects of the component 
                //and add them to the corresponding statuseffect list
                foreach (List<StatusEffect> componentEffectList in ic.statusEffectLists.Values)
                {
                    ActionType actionType = componentEffectList.First().type;
                    if (!statusEffectLists.TryGetValue(actionType, out List<StatusEffect> statusEffectList))
                    {
                        statusEffectList = new List<StatusEffect>();
                        statusEffectLists.Add(actionType, statusEffectList);
                    }

                    foreach (StatusEffect effect in componentEffectList)
                    {
                        statusEffectList.Add(effect);
                    }
                }
            }
            
            if (body != null)
            {
                body.Submarine = submarine;
            }

            //cache connections into a dictionary for faster lookups
            var connectionPanel = GetComponent<ConnectionPanel>();
            if (connectionPanel != null)
            {
                connections = new Dictionary<string, Connection>();
                foreach (Connection c in connectionPanel.Connections)
                {
                    if (!connections.ContainsKey(c.Name))
                        connections.Add(c.Name, c);
                }
            }

            //containers need to handle collision events to notify items inside them about the impact
            var itemContainer = GetComponent<ItemContainer>();
            if (ImpactTolerance > 0.0f || itemContainer != null)
            {
                if (body != null) body.FarseerBody.OnCollision += OnCollision;
            }

            if (itemContainer != null)
            {
                ownInventory = itemContainer.Inventory;
            }

            InitProjSpecific();
                        
            InsertToList();
            ItemList.Add(this);

            if (callOnItemLoaded)
            {
                foreach (ItemComponent ic in components)
                {
                    ic.OnItemLoaded();
                }
            }
        }

        partial void InitProjSpecific();

        public override MapEntity Clone()
        {
            Item clone = new Item(rect, Prefab, Submarine, callOnItemLoaded: false);
            foreach (KeyValuePair<string, SerializableProperty> property in SerializableProperties)
            {
                if (!property.Value.Attributes.OfType<Editable>().Any()) continue;
                clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
            }

            if (components.Count != clone.components.Count)
            {
                string errorMsg = "Error while cloning item \"" + Name + "\" - clone does not have the same number of components. ";
                errorMsg += "Original components: " + string.Join(", ", components.Select(c => c.GetType().ToString()));
                errorMsg += ", cloned components: " + string.Join(", ", clone.components.Select(c => c.GetType().ToString()));
                DebugConsole.ThrowError(errorMsg);
                GameAnalyticsManager.AddErrorEventOnce("Item.Clone:" + Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
            }

            for (int i = 0; i < components.Count && i < clone.components.Count; i++)
            {
                foreach (KeyValuePair<string, SerializableProperty> property in components[i].SerializableProperties)
                {
                    if (!property.Value.Attributes.OfType<Editable>().Any()) continue;
                    clone.components[i].SerializableProperties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i]));
                }

                //clone requireditem identifiers
                foreach (var kvp in components[i].requiredItems)
                {
                    for (int j = 0; j < kvp.Value.Count; j++)
                    {
                        if (!clone.components[i].requiredItems.ContainsKey(kvp.Key) ||
                            clone.components[i].requiredItems[kvp.Key].Count <= j)
                        {
                            continue;
                        }

                        clone.components[i].requiredItems[kvp.Key][j].JoinedIdentifiers = 
                            kvp.Value[j].JoinedIdentifiers;
                    }
                }
            }

            if (FlippedX) clone.FlipX(false);
            if (FlippedY) clone.FlipY(false);
            
            foreach (ItemComponent component in clone.components)
            {
                component.OnItemLoaded();
            }

            if (ContainedItems != null)
            {
                foreach (Item containedItem in ContainedItems)
                {
                    var containedClone = containedItem.Clone();
                    clone.ownInventory.TryPutItem(containedClone as Item, null);
                }
            }            
            return clone;
        }

        public void AddComponent(ItemComponent component)
        {
            allPropertyObjects.Add(component);
            components.Add(component);
            Type type = component.GetType();
            if (!componentsByType.ContainsKey(type))
            {
                componentsByType.Add(type, component);
                Type baseType = type.BaseType;
                while (baseType != null && baseType != typeof(ItemComponent))
                {
                    if (!componentsByType.ContainsKey(baseType))
                    {
                        componentsByType.Add(baseType, component);
                    }
                    baseType = baseType.BaseType;
                }
            }
        }

        public void EnableDrawableComponent(IDrawableComponent drawable)
        {
            if (!drawableComponents.Contains(drawable))
            {
                drawableComponents.Add(drawable);
            }
        }

        public void DisableDrawableComponent(IDrawableComponent drawable)
        {
            drawableComponents.Remove(drawable);            
        }

        public int GetComponentIndex(ItemComponent component)
        {
            return components.IndexOf(component);
        }

        public T GetComponent<T>() where T : ItemComponent
        {
            if (componentsByType.TryGetValue(typeof(T), out ItemComponent component))
            {
                return (T)component;
            }
            
            return default(T);
        }

        public IEnumerable<T> GetComponents<T>()
        {
            if (!componentsByType.ContainsKey(typeof(T))) { return Enumerable.Empty<T>(); }

            return components.Where(c => c is T).Cast<T>();
        }
        
        public void RemoveContained(Item contained)
        {
            if (ownInventory != null)
            {
                ownInventory.RemoveItem(contained);
            }

            contained.Container = null;            
        }


        public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = true)
        {
            if (!MathUtils.IsValid(simPosition))
            {
                string errorMsg =
                    "Attempted to move the item " + Name +
                    " to an invalid position (" + simPosition + ")\n" + Environment.StackTrace;

                DebugConsole.ThrowError(errorMsg);
                GameAnalyticsManager.AddErrorEventOnce(
                    "Item.SetPosition:InvalidPosition" + ID,
                    GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
                    errorMsg);
                return;
            }

            if (body != null)
            {
#if DEBUG
                try
                {
#endif
                    if (body.Enabled)
                    {
                        body.SetTransform(simPosition, rotation);
                    }
                    else
                    {
                        body.SetTransformIgnoreContacts(simPosition, rotation);
                    }
#if DEBUG
                }
                catch (Exception e)
                {
                    DebugConsole.ThrowError("Failed to set item transform", e);
                }
#endif
            }

            Vector2 displayPos = ConvertUnits.ToDisplayUnits(simPosition);

            rect.X = (int)(displayPos.X - rect.Width / 2.0f);
            rect.Y = (int)(displayPos.Y + rect.Height / 2.0f);

            if (findNewHull) FindHull();
        }

        partial void SetActiveSprite();

        public override void Move(Vector2 amount)
        {
            base.Move(amount);

            if (ItemList != null && body != null)
            {
                body.SetTransform(body.SimPosition + ConvertUnits.ToSimUnits(amount), body.Rotation);
            }
            foreach (ItemComponent ic in components)
            {
                ic.Move(amount);
            }

            if (body != null && (Submarine==null || !Submarine.Loading)) FindHull();
        }

        public Rectangle TransformTrigger(Rectangle trigger, bool world = false)
        {
            return world ? 
                new Rectangle(
                    WorldRect.X + trigger.X,
                    WorldRect.Y + trigger.Y,
                    (trigger.Width == 0) ? Rect.Width : trigger.Width,
                    (trigger.Height == 0) ? Rect.Height : trigger.Height)
                    :
                new Rectangle(
                    Rect.X + trigger.X,
                    Rect.Y + trigger.Y,
                    (trigger.Width == 0) ? Rect.Width : trigger.Width,
                    (trigger.Height == 0) ? Rect.Height : trigger.Height);
        }

        /// <summary>
        /// goes through every item and re-checks which hull they are in
        /// </summary>
        public static void UpdateHulls()
        {
            foreach (Item item in ItemList) item.FindHull();
        }
        
        public Hull FindHull()
        {
            if (parentInventory != null && parentInventory.Owner != null)
            {
                if (parentInventory.Owner is Character)
                {
                    CurrentHull = ((Character)parentInventory.Owner).AnimController.CurrentHull;
                }
                else if (parentInventory.Owner is Item)
                {
                    CurrentHull = ((Item)parentInventory.Owner).CurrentHull;
                }

                Submarine = parentInventory.Owner.Submarine;
                if (body != null) body.Submarine = Submarine;

                return CurrentHull;
            }


            CurrentHull = Hull.FindHull(WorldPosition, CurrentHull);
            if (body != null && body.Enabled)
            {
                Submarine = CurrentHull?.Submarine;
                body.Submarine = Submarine;
            }

            return CurrentHull;
        }

        public Item GetRootContainer()
        {
            if (Container == null) return null;

            Item rootContainer = Container;
            while (rootContainer.Container != null)
            {
                rootContainer = rootContainer.Container;
            }

            return rootContainer;
        }
                
        public void SetContainedItemPositions()
        {
            foreach (ItemComponent component in components)
            {
                (component as ItemContainer)?.SetContainedItemPositions();
            }
        }
        
        public void AddTag(string tag)
        {
            if (tags.Contains(tag)) return;
            tags.Add(tag);
        }

        public bool HasTag(string tag)
        {
            if (tag == null) return true;
            return tags.Contains(tag) || prefab.Tags.Contains(tag);
        }

        public bool HasTag(IEnumerable<string> allowedTags)
        {
            if (allowedTags == null) return true;
            foreach (string tag in allowedTags)
            {
                if (tags.Contains(tag)) return true;
            }
            return false;
        }
        
        public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb limb = null, bool isNetworkEvent = false)
        {
            if (statusEffectLists == null) return;

            if (!statusEffectLists.TryGetValue(type, out List<StatusEffect> statusEffects)) return;

            bool broken = condition <= 0.0f;
            foreach (StatusEffect effect in statusEffects)
            {
                if (broken && effect.type != ActionType.OnBroken) continue;
                ApplyStatusEffect(effect, type, deltaTime, character, limb, isNetworkEvent, false);
            }
        }
        
        readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();

        public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, bool isNetworkEvent = false, bool checkCondition = true)
        {
            if (!isNetworkEvent && checkCondition)
            {
                if (condition == 0.0f && effect.type != ActionType.OnBroken) return;
            }
            if (effect.type != type) return;
            
            bool hasTargets = (effect.TargetIdentifiers == null);

            targets.Clear();
            
            if (effect.HasTargetType(StatusEffect.TargetType.Contained))
            {
                var containedItems = ContainedItems;
                if (containedItems != null)
                {
                    foreach (Item containedItem in containedItems)
                    {
                        if (containedItem == null) continue;
                        if (effect.TargetIdentifiers != null &&
                            !effect.TargetIdentifiers.Contains(containedItem.prefab.Identifier) &&
                            !effect.TargetIdentifiers.Any(id => containedItem.HasTag(id)))
                        {
                            continue;
                        }

                        hasTargets = true;
                        targets.Add(containedItem);
                    }
                }
            }

            if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters) || effect.HasTargetType(StatusEffect.TargetType.NearbyItems))
            {
                effect.GetNearbyTargets(WorldPosition, targets);
                if (targets.Count > 0) { hasTargets = true; }
            }

            if (!hasTargets) { return; }

            if (effect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null)
            {
                targets.Add(CurrentHull);
            }

            if (effect.HasTargetType(StatusEffect.TargetType.This))
            {
                foreach (var pobject in AllPropertyObjects)
                {
                    targets.Add(pobject);
                }
            }

            if (effect.HasTargetType(StatusEffect.TargetType.Character))
            {
                if (type == ActionType.OnContained && ParentInventory is CharacterInventory characterInventory)
                {
                    targets.Add(characterInventory.Owner as ISerializableEntity);
                }
                else
                {
                    targets.Add(character);
                }
            }

            if (effect.HasTargetType(StatusEffect.TargetType.Limb))
            {
                targets.Add(limb);
            }
            if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
            {
                targets.AddRange(character.AnimController.Limbs.ToList());
            }
            
            if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) targets.Add(Container);
            
            effect.Apply(type, deltaTime, this, targets);            
        }


        public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true)
        {
            if (Indestructible) return new AttackResult();

            float damageAmount = attack.GetItemDamage(deltaTime);
            Condition -= damageAmount;

            return new AttackResult(damageAmount, null);
        }

        private bool IsInWater()
        {
            if (CurrentHull == null) return true;
                        
            float surfaceY = CurrentHull.Surface;

            return CurrentHull.WaterVolume > 0.0f && Position.Y < surfaceY;
        }


        public override void Update(float deltaTime, Camera cam)
        {
            //aitarget goes silent/invisible if the components don't keep it active
            if (aiTarget != null)
            {
                aiTarget.SightRange -= deltaTime * 1000.0f;
                aiTarget.SoundRange -= deltaTime * 1000.0f;
            }

            if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
            {
                sendConditionUpdateTimer -= deltaTime;
                if (conditionUpdatePending)
                {
                    if (sendConditionUpdateTimer <= 0.0f)
                    {
                        GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status });
                        lastSentCondition = condition;
                        sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
                        conditionUpdatePending = false;
                    }
                }
            }
            
            ApplyStatusEffects(ActionType.Always, deltaTime, null);

            foreach (ItemComponent ic in components)
            {
                if (ic.Parent != null) ic.IsActive = ic.Parent.IsActive;

#if CLIENT
                if (!ic.WasUsed)
                {
                    ic.StopSounds(ActionType.OnUse);
                    ic.StopSounds(ActionType.OnSecondaryUse);
                }
#endif
                ic.WasUsed = false;

                ic.ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime);

                if (!ic.IsActive) continue;

                if (condition > 0.0f)
                {
                    ic.Update(deltaTime, cam);

#if CLIENT
                    if (ic.IsActive) ic.PlaySound(ActionType.OnActive, WorldPosition);
#endif
                }
                else
                {
                    ic.UpdateBroken(deltaTime, cam);
                }
            }

            if (body != null && body.Enabled)
            {
                System.Diagnostics.Debug.Assert(body.FarseerBody.FixtureList != null);

                if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f)
                {
                    UpdateTransform();
                    if (CurrentHull == null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth))
                    {
                        Spawner.AddToRemoveQueue(this);
                        return;
                    }
                }
            }

            UpdateNetPosition(deltaTime);

            inWater = IsInWater();
            bool waterProof = WaterProof;
            if (inWater)
            {
                Item container = this.Container;
                while (!waterProof && container != null)
                {
                    waterProof = container.WaterProof;
                    container = container.Container;
                }
            }
            ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);

            if (body == null || !body.Enabled || !inWater || ParentInventory != null || Removed) { return; }

            ApplyWaterForces();
            CurrentHull?.ApplyFlowForces(deltaTime, this);
        }
                
        public void UpdateTransform()
        {
            Submarine prevSub = Submarine;

            FindHull();

            if (Submarine == null && prevSub != null)
            {
                body.SetTransform(body.SimPosition + prevSub.SimPosition, body.Rotation);
            }
            else if (Submarine != null && prevSub == null)
            {
                body.SetTransform(body.SimPosition - Submarine.SimPosition, body.Rotation);
            }

            Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition);
            rect.X = (int)(displayPos.X - rect.Width / 2.0f);
            rect.Y = (int)(displayPos.Y + rect.Height / 2.0f);

            if (Math.Abs(body.LinearVelocity.X) > NetConfig.MaxPhysicsBodyVelocity || 
                Math.Abs(body.LinearVelocity.Y) > NetConfig.MaxPhysicsBodyVelocity)
            {
                body.LinearVelocity = new Vector2(
                    MathHelper.Clamp(body.LinearVelocity.X, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity),
                    MathHelper.Clamp(body.LinearVelocity.Y, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity));
            }
        }

        /// <summary>
        /// Applies buoyancy, drag and angular drag caused by water
        /// </summary>
        private void ApplyWaterForces()
        {
            if (body.Mass <= 0.0f)
            {
                return;
            }

            float forceFactor = 1.0f;
            if (CurrentHull != null)
            {
                float floor = CurrentHull.Rect.Y - CurrentHull.Rect.Height;
                float waterLevel = floor + CurrentHull.WaterVolume / CurrentHull.Rect.Width;

                //forceFactor is 1.0f if the item is completely submerged, 
                //and goes to 0.0f as the item goes through the surface
                forceFactor = Math.Min((waterLevel - Position.Y) / rect.Height, 1.0f);
                if (forceFactor <= 0.0f) return;
            }

            float volume = body.Mass / body.Density;

            var uplift = -GameMain.World.Gravity * forceFactor * volume;

            Vector2 drag = body.LinearVelocity * volume;

            body.ApplyForce((uplift - drag) * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);

            //apply simple angular drag
            body.ApplyTorque(body.AngularVelocity * volume * -0.05f);                    
        }

        private bool OnCollision(Fixture f1, Fixture f2, Contact contact)
        {
#if CLIENT
            if (GameMain.Client != null) return true;
#endif

            Vector2 normal = contact.Manifold.LocalNormal;
            
            float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);

            if (ImpactTolerance > 0.0f && impact > ImpactTolerance)
            {
                ApplyStatusEffects(ActionType.OnImpact, 1.0f);
#if SERVER
                GameMain.Server?.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact });
#endif
            }

            var containedItems = ContainedItems;
            if (containedItems != null)
            {
                foreach (Item contained in containedItems)
                {
                    if (contained.body == null) continue;
                    contained.OnCollision(f1, f2, contact);
                }
            }

            return true;
        }

        public override void FlipX(bool relativeToSub)
        {
            base.FlipX(relativeToSub);
            
            if (Prefab.CanSpriteFlipX)
            {
                SpriteEffects ^= SpriteEffects.FlipHorizontally;
            }

            foreach (ItemComponent component in components)
            {
                component.FlipX(relativeToSub);
            }            
        }

        public override void FlipY(bool relativeToSub)
        {
            base.FlipY(relativeToSub);

            if (Prefab.CanSpriteFlipY)
            {
                SpriteEffects ^= SpriteEffects.FlipVertically;
            }

            foreach (ItemComponent component in components)
            {
                component.FlipY(relativeToSub);
            }
        }

        public List<T> GetConnectedComponents<T>(bool recursive = false) where T : ItemComponent
        {
            List<T> connectedComponents = new List<T>();

            if (recursive)
            {
                List<Item> alreadySearched = new List<Item>() { this };
                GetConnectedComponentsRecursive(alreadySearched, connectedComponents);

                return connectedComponents;
            }

            ConnectionPanel connectionPanel = GetComponent<ConnectionPanel>();
            if (connectionPanel == null) return connectedComponents;

            foreach (Connection c in connectionPanel.Connections)
            {
                var recipients = c.Recipients;
                foreach (Connection recipient in recipients)
                {
                    var component = recipient.Item.GetComponent<T>();
                    if (component != null) connectedComponents.Add(component);
                }
            }

            return connectedComponents;
        }

        private void GetConnectedComponentsRecursive<T>(List<Item> alreadySearched, List<T> connectedComponents) where T : ItemComponent
        {
            alreadySearched.Add(this);

            ConnectionPanel connectionPanel = GetComponent<ConnectionPanel>();
            if (connectionPanel == null) return;

            foreach (Connection c in connectionPanel.Connections)
            {
                var recipients = c.Recipients;
                foreach (Connection recipient in recipients)
                {
                    if (alreadySearched.Contains(recipient.Item)) continue;

                    var component = recipient.Item.GetComponent<T>();
                    
                    if (component != null)
                    {
                        connectedComponents.Add(component);
                    }

                    recipient.Item.GetConnectedComponentsRecursive<T>(alreadySearched, connectedComponents);                   
                }
            }
        }

        public List<T> GetConnectedComponentsRecursive<T>(Connection c) where T : ItemComponent
        {
            List<T> connectedComponents = new List<T>();            
            List<Item> alreadySearched = new List<Item>() { this };
            GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents);

            return connectedComponents;
        }

        private void GetConnectedComponentsRecursive<T>(Connection c, List<Item> alreadySearched, List<T> connectedComponents) where T : ItemComponent
        {
            alreadySearched.Add(this);
                        
            var recipients = c.Recipients;
            foreach (Connection recipient in recipients)
            {
                if (alreadySearched.Contains(recipient.Item)) continue;

                var component = recipient.Item.GetComponent<T>();                    
                if (component != null)
                {
                    connectedComponents.Add(component);
                }

                recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents);                   
            }            
        }


        public void SendSignal(int stepsTaken, string signal, string connectionName, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f)
        {
            LastSentSignalRecipients.Clear();
            if (connections == null) return;

            stepsTaken++;

            if (!connections.TryGetValue(connectionName, out Connection c)) return;

            if (stepsTaken > 10)
            {
                //use a coroutine to prevent infinite loops by creating a one 
                //frame delay if the "signal chain" gets too long
                CoroutineManager.StartCoroutine(SendSignal(signal, c, sender, power, signalStrength));
            }
            else
            {
                c.SendSignal(stepsTaken, signal, source ?? this, sender, power, signalStrength);
            }            
        }

        private IEnumerable<object> SendSignal(string signal, Connection connection, Character sender, float power = 0.0f, float signalStrength = 1.0f)
        {
            //wait one frame
            yield return CoroutineStatus.Running;

            connection.SendSignal(0, signal, this, sender, power, signalStrength);

            yield return CoroutineStatus.Success;
        }

        public float GetDrawDepth()
        {
            return SpriteDepth + ((ID % 255) * 0.000001f);
        }

        public bool IsInsideTrigger(Vector2 worldPosition)
        {
            return IsInsideTrigger(worldPosition, out _);
        }

        public bool IsInsideTrigger(Vector2 worldPosition, out Rectangle transformedTrigger)
        {
            foreach (Rectangle trigger in Prefab.Triggers)
            {
                transformedTrigger = TransformTrigger(trigger, true);
                if (Submarine.RectContains(transformedTrigger, worldPosition)) return true;
            }

            transformedTrigger = Rectangle.Empty;
            return false;
        }

        public bool CanClientAccess(Client c)
        {
            return c != null && c.Character != null && c.Character.CanInteractWith(this);
        }

        public bool TryInteract(Character picker, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceActionKey = false)
        {
            bool hasRequiredSkills = true;

            bool picked = false, selected = false;

            Skill requiredSkill = null;
            
            foreach (ItemComponent ic in components)
            {
                bool pickHit = false, selectHit = false;
                if (Screen.Selected == GameMain.SubEditorScreen)
                {
                    pickHit = picker.IsKeyHit(InputType.Select);
                    selectHit = picker.IsKeyHit(InputType.Select);
                }
                else
                {
                    if (picker.IsKeyDown(InputType.Aim))
                    {
                        pickHit = false;
                        selectHit = false;
                    }
                    else
                    {
                        if (forceSelectKey)
                        {
                            if (ic.PickKey == InputType.Select) pickHit = true;
                            if (ic.SelectKey == InputType.Select) selectHit = true;
                        }
                        else if (forceActionKey)
                        {
                            if (ic.PickKey == InputType.Use) pickHit = true;
                            if (ic.SelectKey == InputType.Use) selectHit = true;
                        }
                        else
                        {
                            pickHit = picker.IsKeyHit(ic.PickKey);
                            selectHit = picker.IsKeyHit(ic.SelectKey);

#if CLIENT
                        //if the cursor is on a UI component, disable interaction with the left mouse button
                        //to prevent accidentally selecting items when clicking UI elements
                        if (picker == Character.Controlled && GUI.MouseOn != null)
                        {
                            if (GameMain.Config.KeyBind(ic.PickKey).MouseButton == 0) pickHit = false;
                            if (GameMain.Config.KeyBind(ic.SelectKey).MouseButton == 0) selectHit = false;
                        }
#endif
                        }
                    }
                }

                if (!pickHit && !selectHit) continue;

                if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) hasRequiredSkills = false;
                
                bool showUiMsg = false;
#if CLIENT
                showUiMsg = picker == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen;
#endif
                if (!ignoreRequiredItems && !ic.HasRequiredItems(picker, showUiMsg)) continue;
                if ((ic.CanBePicked && pickHit && ic.Pick(picker)) ||
                    (ic.CanBeSelected && selectHit && ic.Select(picker)))
                {
                    picked = true;
                    ic.ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
#if CLIENT
                    if (picker == Character.Controlled) GUI.ForceMouseOn(null);
#endif
                    if (tempRequiredSkill != null) requiredSkill = tempRequiredSkill;

                    if (ic.CanBeSelected) selected = true;
                }
            }

            if (!picked) return false;

            if (picker.SelectedConstruction == this)
            {
                if (picker.IsKeyHit(InputType.Select) || forceSelectKey) picker.SelectedConstruction = null;
            }
            else if (selected)
            {
                picker.SelectedConstruction = this;
            }

#if CLIENT
            if (!hasRequiredSkills && Character.Controlled == picker && Screen.Selected != GameMain.SubEditorScreen)
            {
                if (requiredSkill != null)
                {
                    GUI.AddMessage(TextManager.Get("InsufficientSkills")
                        .Replace("[requiredskill]", TextManager.Get("SkillName." + requiredSkill.Identifier))
                        .Replace("[requiredlevel]", ((int)requiredSkill.Level).ToString()), Color.Red);
                }
            }
#endif

            if (Container != null) Container.RemoveContained(this);

            return true;         
        }

        public void Use(float deltaTime, Character character = null, Limb targetLimb = null)
        {
            if (RequireAimToUse && !character.IsKeyDown(InputType.Aim))
            {
                return;
            }

            if (condition == 0.0f) return;

            bool remove = false;

            foreach (ItemComponent ic in components)
            {
                bool isControlled = false;
#if CLIENT
                isControlled = character == Character.Controlled;
#endif
                if (!ic.HasRequiredContainedItems(isControlled)) continue;
                if (ic.Use(deltaTime, character))
                {
                    ic.WasUsed = true;

#if CLIENT
                    ic.PlaySound(ActionType.OnUse, WorldPosition, character);
#endif
    
                    ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb);

                    if (ic.DeleteOnUse) remove = true;
                }
            }

            if (remove)
            {
                Spawner.AddToRemoveQueue(this);
            }
        }

        public void SecondaryUse(float deltaTime, Character character = null)
        {
            if (condition == 0.0f) return;

            bool remove = false;

            foreach (ItemComponent ic in components)
            {
                bool isControlled = false;
#if CLIENT
                isControlled = character == Character.Controlled;
#endif
                if (!ic.HasRequiredContainedItems(isControlled)) continue;
                if (ic.SecondaryUse(deltaTime, character))
                {
                    ic.WasUsed = true;

#if CLIENT
                    ic.PlaySound(ActionType.OnSecondaryUse, WorldPosition, character);
#endif

                    ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character);

                    if (ic.DeleteOnUse) remove = true;
                }
            }

            if (remove)
            {
                Spawner.AddToRemoveQueue(this);
            }
        }

        public void ApplyTreatment(Character user, Character character, Limb targetLimb)
        {
            //can't apply treatment to dead characters
            if (character.IsDead) return;
            if (!UseInHealthInterface) return;

#if CLIENT
            if (GameMain.Client != null)
            {
                GameMain.Client.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Treatment, character.ID, targetLimb });
                return;
            }
#endif

            bool remove = false;
            foreach (ItemComponent ic in components)
            {
                if (!ic.HasRequiredContainedItems(user == Character.Controlled)) continue;

                bool success = Rand.Range(0.0f, 1.0f) < ic.DegreeOfSuccess(user);
                ActionType actionType = success ? ActionType.OnUse : ActionType.OnFailure;

#if CLIENT
                ic.PlaySound(actionType, user.WorldPosition, user);
#endif
                ic.WasUsed = true;
                ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user);

                if (GameMain.NetworkMember!=null && GameMain.NetworkMember.IsServer)
                {
                    GameMain.NetworkMember.CreateEntityEvent(this, new object[]
                    {
                        NetEntityEvent.Type.ApplyStatusEffect, actionType, ic, character.ID, targetLimb
                    });
                }

                if (ic.DeleteOnUse) remove = true;
            }

            if (remove) { Spawner?.AddToRemoveQueue(this); }
        }

        public List<ColoredText> GetHUDTexts(Character character)
        {
            List<ColoredText> texts = new List<ColoredText>();
            
            foreach (ItemComponent ic in components)
            {
                if (string.IsNullOrEmpty(ic.DisplayMsg)) continue;
                if (!ic.CanBePicked && !ic.CanBeSelected) continue;
                if (ic is Holdable holdable && !holdable.CanBeDeattached()) continue;
               
                Color color = Color.Red;
                if (ic.HasRequiredSkills(character) && ic.HasRequiredItems(character, false)) color = Color.Orange;

                texts.Add(new ColoredText(ic.DisplayMsg, color, false));
            }

            return texts;
        }

        public bool Combine(Item item)
        {
            bool isCombined = false;
            foreach (ItemComponent ic in components)
            {
                if (ic.Combine(item)) isCombined = true;
            }
            return isCombined;
        }

        public void Drop(Character dropper, bool createNetworkEvent = true)
        {
            if (createNetworkEvent)
            {
                if (parentInventory != null && !parentInventory.Owner.Removed && !Removed &&
                    GameMain.NetworkMember != null && (GameMain.NetworkMember.IsServer || Character.Controlled == dropper))
                {
                    parentInventory.CreateNetworkEvent();
                    //send frequent updates after the item has been dropped
                    PositionUpdateInterval = 0.0f;
                }
            }

            foreach (ItemComponent ic in components) { ic.Drop(dropper); }

            if (Container != null)
            {
                if (body != null)
                {
                    body.Enabled = true;
                    body.LinearVelocity = Vector2.Zero;
                }
                SetTransform(Container.SimPosition, 0.0f);

                Container.RemoveContained(this);
                Container = null;
            }

            if (parentInventory != null)
            {
                parentInventory.RemoveItem(this);
                parentInventory = null;
            }
        }

        public void Equip(Character character)
        {
            foreach (ItemComponent ic in components) ic.Equip(character);
        }

        public void Unequip(Character character)
        {
            character.DeselectItem(this);
            foreach (ItemComponent ic in components) ic.Unequip(character);
        }


        public List<Pair<object, SerializableProperty>> GetProperties<T>()
        {
            List<Pair<object, SerializableProperty>> allProperties = new List<Pair<object, SerializableProperty>>();

            List<SerializableProperty> itemProperties = SerializableProperty.GetProperties<T>(this);
            foreach (var itemProperty in itemProperties)
            {
                allProperties.Add(new Pair<object, SerializableProperty>(this, itemProperty));
            }            
            foreach (ItemComponent ic in components)
            {
                List<SerializableProperty> componentProperties = SerializableProperty.GetProperties<T>(ic);
                foreach (var componentProperty in componentProperties)
                {
                    allProperties.Add(new Pair<object, SerializableProperty>(ic, componentProperty));
                }
            }
            return allProperties;
        }

        private void WritePropertyChange(NetBuffer msg, object[] extraData, bool inGameEditableOnly)
        {
            var allProperties = inGameEditableOnly ? GetProperties<InGameEditable>() : GetProperties<Editable>();
            SerializableProperty property = extraData[1] as SerializableProperty;
            if (property != null)
            {
                var propertyOwner = allProperties.Find(p => p.Second == property);
                if (allProperties.Count > 1)
                {
                    msg.WriteRangedInteger(0, allProperties.Count - 1, allProperties.FindIndex(p => p.Second == property));
                }

                object value = property.GetValue(propertyOwner.First);
                if (value is string)
                {
                    msg.Write((string)value);
                }
                else if (value is float)
                {
                    msg.Write((float)value);
                }
                else if (value is int)
                {
                    msg.Write((int)value);
                }
                else if (value is bool)
                {
                    msg.Write((bool)value);
                }
                else if (value is Color color)
                {
                    msg.Write(color.R);
                    msg.Write(color.G);
                    msg.Write(color.B);
                    msg.Write(color.A);
                }
                else if (value is Vector2)
                {
                    msg.Write(((Vector2)value).X);
                    msg.Write(((Vector2)value).Y);
                }
                else if (value is Vector3)
                {
                    msg.Write(((Vector3)value).X);
                    msg.Write(((Vector3)value).Y);
                    msg.Write(((Vector3)value).Z);
                }
                else if (value is Vector4)
                {
                    msg.Write(((Vector4)value).X);
                    msg.Write(((Vector4)value).Y);
                    msg.Write(((Vector4)value).Z);
                    msg.Write(((Vector4)value).W);
                }
                else if (value is Point)
                {
                    msg.Write(((Point)value).X);
                    msg.Write(((Point)value).Y);
                }
                else if (value is Rectangle)
                {
                    msg.Write(((Rectangle)value).X);
                    msg.Write(((Rectangle)value).Y);
                    msg.Write(((Rectangle)value).Width);
                    msg.Write(((Rectangle)value).Height);
                }
                else if (value is Enum)
                {
                    msg.Write((int)value);
                }
                else
                {
                    throw new NotImplementedException("Serializing item properties of the type \"" + value.GetType() + "\" not supported");
                }
            }
            else
            {
                throw new ArgumentException("Failed to write propery value - property \"" + (property == null ? "null" : property.Name) + "\" is not serializable.");
            }
        }

        private void ReadPropertyChange(NetBuffer msg, bool inGameEditableOnly, Client sender = null)
        {
            var allProperties = inGameEditableOnly ? GetProperties<InGameEditable>() : GetProperties<Editable>();
            if (allProperties.Count == 0) { return; }

            int propertyIndex = 0;
            if (allProperties.Count > 1)
            {
                propertyIndex = msg.ReadRangedInteger(0, allProperties.Count - 1);
            }

            bool allowEditing = true;
            object parentObject = allProperties[propertyIndex].First;
            SerializableProperty property = allProperties[propertyIndex].Second;
            if (inGameEditableOnly && parentObject is ItemComponent ic)
            {
                if (!ic.AllowInGameEditing) allowEditing = false;
            }

            if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !CanClientAccess(sender))
            {
                allowEditing = false;
            }

            Type type = property.PropertyType;
            if (type == typeof(string))
            {
                string val = msg.ReadString();
                if (allowEditing) property.TrySetValue(parentObject, val);
            }
            else if (type == typeof(float))
            {
                float val = msg.ReadFloat();
                if (allowEditing) property.TrySetValue(parentObject, val);
            }
            else if (type == typeof(int))
            {
                int val = msg.ReadInt32();
                if (allowEditing) property.TrySetValue(parentObject, val);
            }
            else if (type == typeof(bool))
            {
                bool val = msg.ReadBoolean();
                if (allowEditing) property.TrySetValue(parentObject, val);
            }
            else if (type == typeof(Color))
            {
                Color val = new Color(msg.ReadByte(), msg.ReadByte(), msg.ReadByte(), msg.ReadByte());
                if (allowEditing) property.TrySetValue(parentObject, val);
            }
            else if (type == typeof(Vector2))
            {
                Vector2 val = new Vector2(msg.ReadFloat(), msg.ReadFloat());
                if (allowEditing) property.TrySetValue(parentObject, val);
            }
            else if (type == typeof(Vector3))
            {
                Vector3 val = new Vector3(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat());
                if (allowEditing) property.TrySetValue(parentObject, val);
            }
            else if (type == typeof(Vector4))
            {
                Vector4 val = new Vector4(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat());
                if (allowEditing) property.TrySetValue(parentObject, val);
            }
            else if (type == typeof(Point))
            {
                Point val = new Point(msg.ReadInt32(), msg.ReadInt32());
                if (allowEditing) property.TrySetValue(parentObject, val);
            }
            else if (type == typeof(Rectangle))
            {
                Rectangle val = new Rectangle(msg.ReadInt32(), msg.ReadInt32(), msg.ReadInt32(), msg.ReadInt32());
                if (allowEditing) property.TrySetValue(parentObject, val);
            }
            else if (typeof(Enum).IsAssignableFrom(type))
            {
                int intVal = msg.ReadInt32();
                try
                {
                    if (allowEditing) property.TrySetValue(parentObject, Enum.ToObject(type, intVal));
                }
                catch (Exception e)
                {
#if DEBUG
                    DebugConsole.ThrowError("Failed to convert the int value \"" + intVal + "\" to " + type, e);
#endif
                    GameAnalyticsManager.AddErrorEventOnce(
                        "Item.ReadPropertyChange:" + Name + ":" + type,
                        GameAnalyticsSDK.Net.EGAErrorSeverity.Warning,
                        "Failed to convert the int value \"" + intVal + "\" to " + type + " (item " + Name + ")");
                }
            }
            else
            {
                return;
            }
            
            if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
            {
                GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ChangeProperty, property });
            }
        }

        partial void UpdateNetPosition(float deltaTime);

        public static Item Load(XElement element, Submarine submarine)
        {
            return Load(element, submarine, createNetworkEvent: false);
        }

        /// <summary>
        /// Instantiate a new item and load its data from the XML element.
        /// </summary>
        /// <param name="element">The element containing the data of the item</param>
        /// <param name="submarine">The submarine to spawn the item in (can be null)</param>
        /// <param name="createNetworkEvent">Should an EntitySpawner event be created to notify clients about the item being created.</param>
        /// <returns></returns>
        public static Item Load(XElement element, Submarine submarine, bool createNetworkEvent)
        {
            string name = element.Attribute("name").Value;            
            string identifier = element.GetAttributeString("identifier", "");

            ItemPrefab prefab;
            if (string.IsNullOrEmpty(identifier))
            {
                //legacy support: 
                //1. attempt to find a prefab with an empty identifier and a matching name
                prefab = MapEntityPrefab.Find(name, "", showErrorMessages: false) as ItemPrefab;
                //2. not found, attempt to find a prefab with a matching name
                if (prefab == null) prefab = MapEntityPrefab.Find(name) as ItemPrefab;
            }
            else
            {
                prefab = MapEntityPrefab.Find(null, identifier, showErrorMessages: false) as ItemPrefab;

                //not found, see if we can find a prefab with a matching alias
                if (prefab == null)
                {
                    string lowerCaseName = name.ToLowerInvariant();
                    prefab = MapEntityPrefab.List.Find(me => me.Aliases != null && me.Aliases.Contains(lowerCaseName)) as ItemPrefab;
                }
            }

            if (prefab == null)
            {
                DebugConsole.ThrowError("Error loading item - item prefab \"" + name + "\" (identifier \"" + identifier + "\") not found.");
                return null;
            }

            Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty);
            if (rect.Width == 0 && rect.Height == 0)
            {
                rect.Width = (int)(prefab.Size.X * prefab.Scale);
                rect.Height = (int)(prefab.Size.Y * prefab.Scale);
            }

            Item item = new Item(rect, prefab, submarine, callOnItemLoaded: false)
            {
                Submarine = submarine,
                ID = (ushort)int.Parse(element.Attribute("ID").Value),
                linkedToID = new List<ushort>()
            };

#if SERVER
            if (createNetworkEvent)
            {
                Spawner.CreateNetworkEvent(item, remove: false);
            }
#endif

            foreach (XAttribute attribute in element.Attributes())
            {
                if (!item.SerializableProperties.TryGetValue(attribute.Name.ToString(), out SerializableProperty property)) continue;
                bool shouldBeLoaded = false;
                foreach (var propertyAttribute in property.Attributes.OfType<Serialize>())
                {
                    if (propertyAttribute.isSaveable)
                    {
                        shouldBeLoaded = true;
                        break;
                    }
                }

                if (shouldBeLoaded) { property.TrySetValue(item, attribute.Value); }
            }

            string linkedToString = element.GetAttributeString("linked", "");
            if (linkedToString != "")
            {
                string[] linkedToIds = linkedToString.Split(',');
                for (int i = 0; i < linkedToIds.Length; i++)
                {
                    item.linkedToID.Add((ushort)int.Parse(linkedToIds[i]));
                }
            }

            List<ItemComponent> unloadedComponents = new List<ItemComponent>(item.components);
            foreach (XElement subElement in element.Elements())
            {
                ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString());
                if (component == null) continue;

                component.Load(subElement);
                unloadedComponents.Remove(component);
            }

            if (element.GetAttributeBool("flippedx", false)) item.FlipX(false);
            if (element.GetAttributeBool("flippedy", false)) item.FlipY(false);

            item.condition = element.GetAttributeFloat("condition", item.Prefab.Health);
            item.SetActiveSprite();

            foreach (ItemComponent component in item.components)
            {
                component.OnItemLoaded();
            }
            
            return item;
        }

        public override XElement Save(XElement parentElement)
        {
            XElement element = new XElement("Item");

            element.Add(
                new XAttribute("name", prefab.Name),
                new XAttribute("identifier", prefab.Identifier),
                new XAttribute("ID", ID));

            if (FlippedX) element.Add(new XAttribute("flippedx", true));
            if (FlippedY) element.Add(new XAttribute("flippedy", true));

            if (condition < Prefab.Health)
            {
                element.Add(new XAttribute("condition", condition.ToString("G", CultureInfo.InvariantCulture)));
            }

            Item rootContainer = GetRootContainer() ?? this;
            System.Diagnostics.Debug.Assert(Submarine != null || rootContainer.ParentInventory?.Owner is Character);

            Vector2 subPosition = Submarine == null ? Vector2.Zero : Submarine.HiddenSubPosition;

            element.Add(new XAttribute("rect",
                (int)(rect.X - subPosition.X) + "," +
                (int)(rect.Y - subPosition.Y) + "," +
                rect.Width + "," + rect.Height));
            
            if (linkedTo != null && linkedTo.Count > 0)
            {
                var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved).ToList();
                element.Add(new XAttribute("linked", string.Join(",", saveableLinked.Select(l => l.ID.ToString()))));
            }

            SerializableProperty.SerializeProperties(this, element);

            foreach (ItemComponent ic in components)
            {
                ic.Save(element);
            }

            parentElement.Add(element);

            return element;
        }

        public virtual void Reset()
        {
            SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement);
            components.ForEach(c => c.Reset());
        }

        public override void OnMapLoaded()
        {
            FindHull();

            foreach (ItemComponent ic in components)
            {
                ic.OnMapLoaded();
            }
        }
        
        /// <summary>
        /// Remove the item so that it doesn't appear to exist in the game world (stop sounds, remove bodies etc)
        /// but don't reset anything that's required for cloning the item
        /// </summary>
        public override void ShallowRemove()
        {
            base.ShallowRemove();
            
            foreach (ItemComponent ic in components)
            {
                ic.ShallowRemove();
            }
            ItemList.Remove(this);

            if (body != null)
            {
                body.Remove();
                body = null;
            }
        }

        public override void Remove()
        {
            if (Removed)
            {
                DebugConsole.ThrowError("Attempting to remove an already removed item (" + Name + ")\n" + Environment.StackTrace);
                return;
            }
            DebugConsole.Log("Removing item " + Name + " (ID: " + ID + ")");

            base.Remove();

            foreach (Character character in Character.CharacterList)
            {
                if (character.SelectedConstruction == this) character.SelectedConstruction = null;
                for (int i = 0; i < character.SelectedItems.Length; i++)
                {
                    if (character.SelectedItems[i] == this) character.SelectedItems[i] = null;
                }
            }

            if (parentInventory != null)
            {
                parentInventory.RemoveItem(this);
                parentInventory = null;
            }

            foreach (ItemComponent ic in components)
            {
                ic.Remove();
            }
            ItemList.Remove(this);

            if (body != null)
            {
                body.Remove();
                body = null;
            }

            foreach (Item it in ItemList)
            {
                if (it.linkedTo.Contains(this))
                {
                    it.linkedTo.Remove(this);
                }
            }

            RemoveProjSpecific();
        }

        partial void RemoveProjSpecific();
    }
}