using Barotrauma.Extensions;
using Barotrauma.Lights;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Linq;

namespace Barotrauma.Items.Components
{
    partial class Door : Pickable, IDrawableComponent, IServerSerializable
    {
        private ConvexHull convexHull;
        private ConvexHull convexHull2;

        private float shake;
        private float shakeTimer;
        private Vector2 shakePos;

        //openState when the vertices of the convex hull were last calculated
        private float lastConvexHullState;

        [Serialize("1,1", IsPropertySaveable.No, description: "The scale of the shadow-casting area of the door (relative to the actual size of the door).")]
        public Vector2 ShadowScale
        {
            get;
            set;
        }

        public Vector2 DrawSize
        {
            //use the extents of the item as the draw size
            get { return Vector2.Zero; }
        }

        private Vector2[] GetConvexHullCorners(Rectangle rect)
        {
            Point shadowSize = rect.Size.Multiply(ShadowScale);
            Vector2 center = new Vector2(rect.Center.X, rect.Y - rect.Height / 2);

            Vector2[] corners = new Vector2[4];
            corners[0] = center + new Vector2(-shadowSize.X, -shadowSize.Y) / 2;
            corners[1] = center + new Vector2(-shadowSize.X, shadowSize.Y) / 2;
            corners[2] = center + new Vector2(shadowSize.X, shadowSize.Y) / 2;
            corners[3] = center + new Vector2(shadowSize.X, -shadowSize.Y) / 2;

            if (IsHorizontal)
            {
                if (item.FlippedX)
                {
                    Vector2 itemCenter = new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height / 2);
                    for (int i = 0; i < corners.Length; i++)
                    {
                        corners[i].X = itemCenter.X * 2 - corners[i].X;
                    }
                    Array.Reverse(corners);
                }
            }
            else
            {
                if (item.FlippedY)
                {
                    Vector2 itemCenter = new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height / 2);
                    for (int i = 0; i < corners.Length; i++)
                    {
                        corners[i].Y = itemCenter.Y * 2 - corners[i].Y;
                    }
                    Array.Reverse(corners);
                }
            }

            return corners;
        }

        private void UpdateConvexHulls()
        {
            if (item.Removed) { return; }

            doorRect = new Rectangle(
                item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale),
                item.Rect.Y - item.Rect.Height / 2 + (int)(doorSprite.size.Y / 2.0f * item.Scale),
                (int)(doorSprite.size.X * item.Scale),
                (int)(doorSprite.size.Y * item.Scale));

            Rectangle rect = doorRect;
            if (IsHorizontal)
            {
                rect.Width = (int)(rect.Width * (1.0f - openState));
            }
            else
            {
                rect.Height = (int)(rect.Height * (1.0f - openState));
            }

            //only merge the door's convex hull with overlapping wall segments if it's fully open or fully closed
            //it's the heaviest part of changing the convex hull, and doesn't need to be done while the door is still in motion
            bool mergeOverlappingSegments = openState <= 0.0f || openState >= 1.0f;
            if (Window.Height > 0 && Window.Width > 0)
            {
                if (IsHorizontal)
                {
                    rect.Width = (int)(Window.X * item.Scale);
                    rect.X -= (int)(doorRect.Width * openState);
                    rect.Width = Math.Max(rect.Width - (doorRect.X - rect.X), 0);
                    rect.X = Math.Max(doorRect.X, rect.X);
                    if (convexHull2 != null)
                    {
                        Rectangle rect2 = doorRect;
                        rect2.X += (int)(Window.Right * item.Scale);
                        rect2.X -= (int)(doorRect.Width * openState);
                        rect2.X = Math.Max(doorRect.X, rect2.X);
                        rect2.Width = doorRect.Right - (int)(doorRect.Width * openState) - rect2.X;
                        if (rect2.Width == 0)
                        {
                            convexHull2.Enabled = false;
                        }
                        else
                        {
                            convexHull2.Enabled = true;
                            convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments);
                        }
                    }
                }
                else
                {
                    rect.Height = -(int)(Window.Y * item.Scale);
                    rect.Y += (int)(doorRect.Height * openState);
                    rect.Height = Math.Max(rect.Height - (rect.Y - doorRect.Y), 0);
                    rect.Y = Math.Min(doorRect.Y, rect.Y);                
                    if (convexHull2 != null)
                    {
                        Rectangle rect2 = doorRect;
                        rect2.Y += (int)(Window.Y * item.Scale - Window.Height * item.Scale);
                        rect2.Y += (int)(doorRect.Height * openState);
                        rect2.Y = Math.Min(doorRect.Y, rect2.Y);
                        rect2.Height = rect2.Y - (doorRect.Y - (int)(doorRect.Height * (1.0f - openState)));
                        if (rect2.Height == 0)
                        {
                            convexHull2.Enabled = false;
                        }
                        else
                        {
                            convexHull2.Enabled = true;
                            convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments);
                        }
                    }
                }
            }

            if (convexHull == null) { return; }

            if (rect.Height == 0 || rect.Width == 0)
            {
                convexHull.Enabled = false;
            }
            else
            {
                convexHull.Enabled = true;
                convexHull.SetVertices(GetConvexHullCorners(rect), mergeOverlappingSegments);
            }
        }


        partial void UpdateProjSpecific(float deltaTime)
        {
            if (shakeTimer > 0.0f)
            {				
                shakeTimer -= deltaTime;
                Vector2 noisePos = new Vector2((float)PerlinNoise.CalculatePerlin(shakeTimer * 10.0f, shakeTimer * 10.0f, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(shakeTimer * 10.0f, shakeTimer * 10.0f, 0.5f) - 0.5f);
                shakePos = noisePos * shake * 2.0f;
                shake = Math.Min(shake, shakeTimer * 10.0f);
            }
            else
            {
                shakePos = Vector2.Zero;
            }
        }

        public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1)
        {
            Color color = item.GetSpriteColor(withHighlight: true);
            if (brokenSprite == null)
            {
                //broken doors turn black if no broken sprite has been configured
                color = color.Multiply(item.Condition / item.MaxCondition);
                color.A = 255;
            }
            
            if (stuck > 0.0f && weldedSprite != null)
            {
                Vector2 weldSpritePos = new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height / 2.0f) + shakePos;
                if (item.Submarine != null) { weldSpritePos += item.Submarine.DrawPosition; }
                weldSpritePos.Y = -weldSpritePos.Y;

                weldedSprite.Draw(spriteBatch,
                    weldSpritePos, item.SpriteColor * (stuck / 100.0f), scale: item.Scale);
            }

            if (openState >= 1.0f) { return; }

            Vector2 pos;
            if (IsHorizontal)
            {
                pos = new Vector2(item.Rect.X, item.Rect.Y - item.Rect.Height / 2);
                if (item.FlippedX) { pos.X += (int)(doorSprite.size.X * item.Scale * openState); }
            }
            else
            {
                pos = new Vector2(item.Rect.Center.X, item.Rect.Y);
                if (item.FlippedY) { pos.Y -= (int)(doorSprite.size.Y * item.Scale * openState); }
            }

            pos += shakePos;
            if (item.Submarine != null) { pos += item.Submarine.DrawPosition; }
            pos.Y = -pos.Y;

            if (brokenSprite == null || !IsBroken)
            {
                if (doorSprite?.Texture != null)
                {
                    spriteBatch.Draw(doorSprite.Texture, pos,
                        getSourceRect(doorSprite, openState, IsHorizontal),
                        color, 0.0f, doorSprite.Origin, item.Scale, item.SpriteEffects, doorSprite.Depth);
                }
            }

            float maxCondition = item.Repairables.Any() ? 
                item.Repairables.Min(r => r.RepairThreshold) / 100.0f * item.MaxCondition : 
                item.MaxCondition;
            float healthRatio = item.Health / maxCondition;
            if (brokenSprite?.Texture != null && healthRatio < 1.0f)
            {
                Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - healthRatio) : Vector2.One;
                if (IsHorizontal) { scale.X = 1; } else { scale.Y = 1; }
                float alpha = fadeBrokenSprite ? 1.0f - healthRatio : 1.0f;
                spriteBatch.Draw(brokenSprite.Texture, pos,
                    getSourceRect(brokenSprite, openState, IsHorizontal),
                    color * alpha, 0.0f, brokenSprite.Origin, scale * item.Scale, item.SpriteEffects,
                    brokenSprite.Depth);
            }

            static Rectangle getSourceRect(Sprite sprite, float openState, bool horizontal)
            {
                if (horizontal)
                {
                    return new Rectangle(
                        (int)(sprite.SourceRect.X + sprite.size.X * openState),
                        sprite.SourceRect.Y,
                        (int)(sprite.size.X * (1.0f - openState)),
                        (int)sprite.size.Y);
                }
                else
                {
                    return new Rectangle(
                        sprite.SourceRect.X,
                        (int)(sprite.SourceRect.Y + sprite.size.Y * openState),
                        (int)sprite.size.X,
                        (int)(sprite.size.Y * (1.0f - openState)));
                }
            }            
        }

        partial void OnFailedToOpen()
        {
            if (shakeTimer <= 0.0f)
            {
                PlaySound(ActionType.OnFailure);
                shake = 5.0f;
                shakeTimer = 1.0f;
            }
        }

        partial void SetState(bool open, bool isNetworkMessage, bool sendNetworkMessage, bool forcedOpen)
        {
            if ((IsStuck && !isNetworkMessage) ||
                (PredictedState == null && isOpen == open) ||
                (PredictedState != null && isOpen == PredictedState.Value && isOpen == open))
            {
                return;
            }

            if (GameMain.Client != null && !isNetworkMessage)
            {
                bool stateChanged = open != PredictedState;

                //clients can "predict" that the door opens/closes when a signal is received
                //the prediction will be reset after 1 second, setting the door to a state
                //sent by the server, or reverting it back to its old state if no msg from server was received
                PredictedState = open;
                resetPredictionTimer = CorrectionDelay;
                if (stateChanged && !IsBroken)
                {
                    PlayInteractionSound();
                }
            }
            else
            {
                isOpen = open;
                if (!isNetworkMessage || open != PredictedState)
                {
                    StopPicking(null); 
                    if (!IsBroken)
                    {
                        PlayInteractionSound();
                    }
                    if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); }
                }
            }
            
            void PlayInteractionSound()
            {
                ActionType actionType = ActionType.OnUse;
                if (forcedOpen)
                {
                    actionType = ActionType.OnPicked;
                }
                else
                {
                    if (open && HasSoundsOfType[(int)ActionType.OnOpen])
                    {
                        actionType = ActionType.OnOpen;
                    }
                    else if (!open && HasSoundsOfType[(int)ActionType.OnClose])
                    {
                        actionType = ActionType.OnClose;
                    }
                }
                PlaySound(actionType);
            }
        }

        public override void ClientEventRead(IReadMessage msg, float sendingTime)
        {
            base.ClientEventRead(msg, sendingTime);

            bool open       = msg.ReadBoolean();
            bool broken     = msg.ReadBoolean();
            bool forcedOpen = msg.ReadBoolean();
            bool isStuck    = msg.ReadBoolean();
            bool isJammed   = msg.ReadBoolean();
            SetState(open, isNetworkMessage: true, sendNetworkMessage: false, forcedOpen: forcedOpen);
            stuck = msg.ReadRangedSingle(0.0f, 100.0f, 8);
            UInt16 lastUserID = msg.ReadUInt16();
            Character user = lastUserID == 0 ? null : Entity.FindEntityByID(lastUserID) as Character;
            if (user != lastUser)
            {
                lastUser = user;
                toggleCooldownTimer = ToggleCoolDown;
            }
            this.isStuck = isStuck;
            this.isJammed = isJammed;
            if (isStuck) { OpenState = 0.0f; }
            IsBroken = broken;
            PredictedState = null;
        }
    }
}
