using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using MonoGame.Extended;
using MonoGame.Extended.Sprites;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace DestinedToFall
{
    // 1m = 64px

    public class DestinedToFall : Game
    {
        // Graphics

        const int ZOOM = 4;

        Vector2 InternalResolution = new Vector2(320, 180);
        GraphicsDeviceManager graphics;
        SpriteBatch sb;
        Random r;

        OrthographicCamera Camera;


        // Sprites + Visuals

        SpriteFont Font;

        Texture2D MenuTexture;

        Sprite Wheel;
        Sprite Stem;
        Sprite PedalStick;
        Sprite Pedal;
        Sprite Background;
        Sprite Middleground;
        Sprite Foreground;

        IList<Sprite> CloudSprites = new List<Sprite>();
        IList<Cloud> Clouds = new List<Cloud>();

        float WheelAngle = 0;
        float PedalAngle = 0;

        const int CLOUD_PARALLAX_FACTOR = 2;


        // Audio

        Song ForestAmbience;
        IList<SoundEffect> CrashSounds = new List<SoundEffect>();
        SoundEffectInstance CurrentCycleSound;
        SoundEffectInstance CycleGrass;
        SoundEffectInstance CycleStone;
        SoundEffectInstance CycleSand;
        SoundEffectInstance WinSound;


        // Game Data

        GameState State = GameState.Menu;
        bool CheatMode = false;
        bool IKLegs = false;

        Vector2 WheelPosition = Vector2.Zero;
        float Velocity;
        const float MaxVelocity = 5;

        float Tilt = 0;
        bool isCrashed = false;
        bool Win = false;


        public DestinedToFall()
        {
            Window.Title = "Destined to Fall";

            graphics = new GraphicsDeviceManager(this)
            {
                GraphicsProfile = GraphicsProfile.HiDef,
                PreferMultiSampling = true
            };
            Content.RootDirectory = "Content";

            r = new Random();
        }

        protected override void Initialize()
        {
            // lock to 60fps
            IsFixedTimeStep = true;
            TargetElapsedTime = TimeSpan.FromSeconds(1 / 60f);

            graphics.PreferredBackBufferWidth = (int)InternalResolution.X * ZOOM;
            graphics.PreferredBackBufferHeight = (int)InternalResolution.Y * ZOOM;

            Camera = new OrthographicCamera(GraphicsDevice)
            {
                Zoom = ZOOM,
                // i have no idea why 1.5 works, but i can't be bothered to figure it out
                Origin = (-InternalResolution / 1.5f) - new Vector2(0, 32)
            };

            graphics.ApplyChanges();


            base.Initialize();
        }

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            sb = new SpriteBatch(GraphicsDevice);

            Font = Content.Load<SpriteFont>("OpenMine");

            MenuTexture = Content.Load<Texture2D>("menu");

            Wheel = new Sprite(Content.Load<Texture2D>("wheel"));
            Stem = new Sprite(Content.Load<Texture2D>("stem"))
            {
                Origin = new Vector2(16, 63)
            };
            PedalStick = new Sprite(Content.Load<Texture2D>("pedal-stick"));
            Pedal = new Sprite(Content.Load<Texture2D>("pedal"));
            Background = new Sprite(Content.Load<Texture2D>("bg"))
            {
                Origin = InternalResolution / 2
            };
            Middleground = new Sprite(Content.Load<Texture2D>("mg"))
            {
                Origin = new Vector2(320, InternalResolution.Y / 2)
            };
            Foreground = new Sprite(Content.Load<Texture2D>("fg"))
            {
                Origin = new Vector2(320, InternalResolution.Y / 2)
            };

            CloudSprites.Add(new Sprite(Content.Load<Texture2D>("cloud1")));
            CloudSprites.Add(new Sprite(Content.Load<Texture2D>("cloud2")));

            // make random clouds
            for (var i = 0; i < 50; i++)
                Clouds.Add(new Cloud(
                        CloudSprites[r.Next(0, 2)], 
                        new Vector3(r.Next(0, 7000), r.Next(-90, -60), r.Next(30, 240))));


            // Audio

            ForestAmbience = Content.Load<Song>("ambience-forest");
            CrashSounds.Add(Content.Load<SoundEffect>("crash0"));
            CrashSounds.Add(Content.Load<SoundEffect>("crash1"));
            CycleGrass = Content.Load<SoundEffect>("cycle-grass").CreateInstance();
            CycleStone = Content.Load<SoundEffect>("cycle-stone").CreateInstance();
            CycleSand = Content.Load<SoundEffect>("cycle-sand").CreateInstance();
            WinSound = Content.Load<SoundEffect>("win").CreateInstance();
        }

        protected override void Update(GameTime gameTime)
        {
            switch (State)
            {
                case GameState.Menu:
                    MenuUpdate();
                    break;
                case GameState.Playing:
                    GameLoop();
                    break;
                default:
                    throw new Exception("what the fuck");
            }

            base.Update(gameTime);
        }

        void MenuUpdate()
        {
            if (Keyboard.GetState().IsKeyDown(Keys.Space))
                State = GameState.Playing;
        }

        bool isFirstRun = true;
        int timesGoneLeft = 0;
        string[] spookLines = new[]
        {
            "...",
            "don't.", 
            "stop.", 
            "stop it.",
            "there is nothing there.", 
            "...",
            "why are you like this?",
            "what do you hope to achieve?",
            "what do you think is behind there?",
            "forbidden knowledge? untold riches?",
            "a good episode of the Big Bang Theory?",
            "lmao",
            "why aren't you playing the game?",
            "just go right",
            "please",
            //"is this fun for you?",
            //"i'm not laughing."
        };
        float spookAlpha = 0;
        int spookFrameTimer = 0;

        void GameLoop()
        {
            if (isFirstRun)
            {
                MediaPlayer.Play(ForestAmbience);
                MediaPlayer.Volume = 0.5f;

                isFirstRun = false;
            }

            var ks = Keyboard.GetState();
            var gs = GamePad.GetState(PlayerIndex.One);

            if (gs.Buttons.Back == ButtonState.Pressed || ks.IsKeyDown(Keys.Escape))
                Exit();

#if DEBUG
            if (ks.IsKeyDown(Keys.D1))
            {
                CheatMode = !ks.IsKeyDown(Keys.LeftShift);
            }
            if (ks.IsKeyDown(Keys.D2))
            {
                IKLegs = !ks.IsKeyDown(Keys.LeftShift);
            }
#endif

            var oldPosition = WheelPosition;

            if (!isCrashed)
            {
                // user add velocity
                if (ks.IsKeyDown(Keys.A) || gs.ThumbSticks.Left.X < -0.1f)
                {
                    Velocity += -0.1f;

                }
                if (ks.IsKeyDown(Keys.D) || gs.ThumbSticks.Left.X > 0.1f)
                {
                    Velocity += 0.1f;
                }
            }

            // check for win
            if (WheelPosition.X >= 64 * 100)
            {
                if (!Win)
                {
                    Win = true;
                    WinSound.Play();
                }
            }

            // cap and apply velocity
            Velocity = Velocity > MaxVelocity ? MaxVelocity : Velocity;
            Velocity = Velocity < -MaxVelocity ? -MaxVelocity : Velocity;
            WheelPosition += new Vector2(Velocity, 0);

            // calculate current sound from player position
            var oldCycleSound = CurrentCycleSound;
            if (WheelPosition.X < 64 * 50 || WheelPosition.X >= 64 * 98)
            {
                CurrentCycleSound = CycleGrass;
            }
            else if (WheelPosition.X >= 64 * 50 && WheelPosition.X < 64 * 75)
            {
                CurrentCycleSound = CycleStone;
            }
            else if (WheelPosition.X >= 64 * 75 && WheelPosition.X < 64 * 98)
            {
                CurrentCycleSound = CycleSand;
            }
            if (oldCycleSound != CurrentCycleSound)
            {
                oldCycleSound?.Stop();
                CurrentCycleSound?.Play();
            }

            // if moving, play sound
            if (oldPosition != WheelPosition)
            {
                if (CurrentCycleSound?.State == SoundState.Playing)
                    CurrentCycleSound.Volume = (Math.Abs(Velocity) / MaxVelocity) * 0.4f;
                else
                    CurrentCycleSound?.Play();
            }
            else
            {
                CurrentCycleSound?.Stop();
            }

            if (!CheatMode)
            {
                if (!isCrashed)
                {
                    // tilt in the opposite direction to movement
                    Tilt += -Velocity * 0.3f;
                    // tilt faster the further we are tilted
                    Tilt *= 1.025f;
                    // create tiny feedback loop between tilt and velocity
                    // this creates the effect of the wheel slipping from under the rider
                    Velocity += -(Tilt / 2000);
                }
            }

            // slow down over time
            Velocity *= 0.99f;

            // smooth move camera to look at wheel
            Camera.Position += (WheelPosition - Camera.Position) * 0.1f;
            // stop camera from going too far left or right when you hit the edge of the map
            Camera.Position = Camera.Position.X <= -320 / 2 
                ? new Vector2(-320 / 2, Camera.Position.Y) 
                : Camera.Position;
            Camera.Position = Camera.Position.X >= (64 * 100) + (320/2)
                ? new Vector2((64 * 100) + (320 / 2), Camera.Position.Y)
                : Camera.Position;

            WheelAngle = ToDeg(WheelPosition.X / (Wheel.TextureRegion.Height / 2f));
            PedalAngle = WheelAngle * 0.5f;


            // check if lose
            if (Math.Abs(Tilt) >= 100)
            {
                // Lost
                if (CheatMode)
                {
                    Tilt = 0;
                    Velocity = 0;
                }
                else
                {
                    if (!isCrashed)
                    {
                        isCrashed = true;
                        Velocity = 0;
                        CrashSounds[r.Next(0, 2)].Play(0.3f, 0, 0);
                    
                        CycleStone.Stop();

                        Task.Run(() =>
                        {
                            Thread.Sleep(1500);
                            Reset();
                        });
                    }
                }
            }

            // check if left
            if (WheelPosition.X <= -320)
            {
                SoftReset();
                timesGoneLeft++;
                spookAlpha = 1;

                var spookText = timesGoneLeft >= spookLines.Length 
                    ? spookLines[0] 
                    : spookLines[timesGoneLeft];

                // average person reads ~3 words per second
                spookFrameTimer =
                    Math.Max(
                        60,
                        (int)(((spookText.Count(x => x == ' ') + 1) / 3f) * 60)
                    );
            }

            // clamp to right
            if (WheelPosition.X > 64 * 105)
            {
                WheelPosition = new Vector2(64 * 105, WheelPosition.Y);
                Velocity = 0;
            }
        }

        void Reset()
        {
            MediaPlayer.Play(ForestAmbience);
            Tilt = 0;
            Velocity = 0;
            WheelPosition.X = 0;
            Camera.Position = WheelPosition;
            isCrashed = false;
        }

        void SoftReset()
        {
            Tilt = 0;
            Velocity = 0;
            WheelPosition.X = 0;
            isCrashed = false;
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            switch (State)
            {
                case GameState.Menu:
                    MenuDraw();
                    break;
                case GameState.Playing:
                    GameDraw();
                    break;
                default:
                    throw new Exception("what the fuck");
            }

            base.Draw(gameTime);
        }

        void MenuDraw()
        {
            sb.Begin(samplerState: SamplerState.PointClamp);

            sb.Draw(MenuTexture, 
                Vector2.Zero,
                null,
                Color.White,
                0, 
                Vector2.Zero,
                new Vector2(ZOOM), 
                SpriteEffects.None,
                0);

            sb.End();
        }

        void GameDraw()
        {
            #region Game

            sb.Begin(samplerState: SamplerState.PointClamp, transformMatrix: Camera.GetViewMatrix());

            // negate camera vertical offset
            Background.Draw(sb, Camera.Position + new Vector2(0, -24), 0, Vector2.One);

            // clouds
            foreach (var cloud in Clouds)
            {
                cloud.Sprite.Draw(sb,
                    new Vector2(
                        cloud.Position.X + (Camera.Position.X / CLOUD_PARALLAX_FACTOR),
                        cloud.Position.Y),
                    0, 
                    Vector2.One * (cloud.Position.Z / 240));
                cloud.Count();
            }

            // negate camera vertical offset
            Middleground.Draw(sb, new Vector2(Camera.Position.X/12, -24), 0, Vector2.One);



            if (IKLegs)
            {
                var Joint0position = WheelPosition + new Vector2(0, -48);
                var Joint1position = WheelPosition + new Vector2(
                        32 * (float)Math.Cos(ToRad(Joint0angleA + 90)),
                        32 * (float)Math.Sin(ToRad(Joint0angleA + 90))); //down

                var Targetposition = WheelPosition + new Vector2(
                        8 * (float)Math.Cos(ToRad(PedalAngle - 90)),
                        8 * (float)Math.Sin(ToRad(PedalAngle - 90)));
                var length0 = Vector2.Distance(Joint0position, Joint1position);
                var length1 = Vector2.Distance(Joint1position, Targetposition);

                // Distance from Joint0 to Target
                var length2 = Vector2.Distance(Joint0position, Targetposition);

                // Inner angle alpha
                var cosAngle0 = ((length2 * length2) + (length0 * length0) - (length1 * length1)) / (2 * length2 * length0);
                var angle0 = ToDeg((float)Math.Acos(cosAngle0));

                // Inner angle beta
                var cosAngle1 = ((length1 * length1) + (length0 * length0) - (length2 * length2)) / (2 * length1 * length0);
                var angle1 = ToDeg((float)Math.Acos(cosAngle1));

                // Angle from Joint0 and Target
                var diff = Targetposition - Joint0position;
                var atan = ToDeg((float)Math.Atan2(diff.Y, diff.X));

                // So they work in Unity reference frame
                var jointAngle0 = atan - angle0; // Angle A
                var jointAngle1 = 180f - angle1; // Angle B

                Joint0angleA = jointAngle0;

                Joint1position = Joint0position + (new Vector2(
                        (float)Math.Cos(ToRad(jointAngle0)),
                        (float)Math.Sin(ToRad(jointAngle0)))
                    * 32);

                PedalStick.Draw(sb, Joint0position, ToRad(jointAngle0 - 90), Vector2.One * 4);
                PedalStick.Draw(sb, Joint1position, ToRad(jointAngle1 - 90), Vector2.One * 4);
            }





            // unicycle
            Pedal.Draw(sb,
                WheelPosition + new Vector2(
                    8 * (float)Math.Cos(ToRad(PedalAngle - 90)),
                    8 * (float)Math.Sin(ToRad(PedalAngle - 90))),
                0, Vector2.One);
            PedalStick.Draw(sb, WheelPosition, ToRad(PedalAngle + 180), Vector2.One);
            Wheel.Draw(sb, WheelPosition, ToRad(WheelAngle), Vector2.One);
            Stem.Draw(sb, WheelPosition, ToRad(Tilt), Vector2.One);
            PedalStick.Draw(sb, WheelPosition, ToRad(PedalAngle), Vector2.One);
            Pedal.Draw(sb,
                WheelPosition + new Vector2(
                    8 * (float)Math.Cos(ToRad(PedalAngle + 90)),
                    8 * (float)Math.Sin(ToRad(PedalAngle + 90))),
                0, Vector2.One);

            // negate camera vertical offset
            Foreground.Draw(sb, new Vector2(0, -24), 0, Vector2.One);




            if (IKLegs)
            {
                var Joint0position = WheelPosition + new Vector2(0, -48);
                var Joint1position = WheelPosition + new Vector2(
                        32 * (float)Math.Cos(ToRad(Joint0angleB + 90)),
                        32 * (float)Math.Sin(ToRad(Joint0angleB + 90))); //down

                var Targetposition = WheelPosition + new Vector2(
                        8 * (float)Math.Cos(ToRad(PedalAngle + 90)),
                        8 * (float)Math.Sin(ToRad(PedalAngle + 90)));
                var length0 = Vector2.Distance(Joint0position, Joint1position);
                var length1 = Vector2.Distance(Joint1position, Targetposition);

                // Distance from Joint0 to Target
                var length2 = Vector2.Distance(Joint0position, Targetposition);

                // Inner angle alpha
                var cosAngle0 = ((length2 * length2) + (length0 * length0) - (length1 * length1)) / (2 * length2 * length0);
                var angle0 = ToDeg((float)Math.Acos(cosAngle0));

                // Inner angle beta
                var cosAngle1 = ((length1 * length1) + (length0 * length0) - (length2 * length2)) / (2 * length1 * length0);
                var angle1 = ToDeg((float)Math.Acos(cosAngle1));

                // Angle from Joint0 and Target
                var diff = Targetposition - Joint0position;
                var atan = ToDeg((float)Math.Atan2(diff.Y, diff.X));

                // So they work in Unity reference frame
                var jointAngle0 = atan - angle0; // Angle A
                var jointAngle1 = 180f - angle1; // Angle B

                Joint0angleB = jointAngle0;

                Joint1position = Joint0position + (new Vector2(
                        (float)Math.Cos(ToRad(jointAngle0)),
                        (float)Math.Sin(ToRad(jointAngle0)))
                    * 32);

                PedalStick.Draw(sb, Joint0position, ToRad(jointAngle0 - 90), Vector2.One * 4);
                PedalStick.Draw(sb, Joint1position, ToRad(jointAngle1 - 90), Vector2.One * 4);
            }




            sb.End();

            #endregion

            #region UI

            sb.Begin(samplerState: SamplerState.PointClamp, blendState: BlendState.AlphaBlend);

            var scoreText = WheelPosition.X / 64 < 100 ? $"{Math.Floor(WheelPosition.X / 64)}m" : "You Win!";
            sb.DrawString(Font,
                scoreText,
                new Vector2((InternalResolution.X) - Font.MeasureString(scoreText).X - 2, 2) * ZOOM,
                Color.White,
                0,
                Vector2.Zero,
                new Vector2(ZOOM),
                SpriteEffects.None,
                0);

            if (Math.Floor(WheelPosition.X / 64) == 69)
            {
                sb.DrawString(Font,
                    "nice",
                    new Vector2(
                            (InternalResolution.X) - Font.MeasureString("nice").X - 2,
                            Font.MeasureString(scoreText).Y + 4)
                        * ZOOM,
                    Color.White,
                    0,
                    Vector2.Zero,
                    new Vector2(ZOOM),
                    SpriteEffects.None,
                    0);
            }

            try
            {
                var spookText = timesGoneLeft >= spookLines.Length ? spookLines[0] : spookLines[timesGoneLeft];
                if (spookAlpha > 0)
                {                    
                    sb.DrawString(Font,
                        spookText,
                        new Vector2(
                                (InternalResolution.X / 2) - (Font.MeasureString(spookText).X / 2),
                                (InternalResolution.Y / 2) - (Font.MeasureString(spookText).Y / 2))
                            * ZOOM,
                        Color.White * spookAlpha,
                        0,
                        Vector2.Zero,
                        new Vector2(ZOOM),
                        SpriteEffects.None,
                        0);

                    if (spookFrameTimer > 0)
                    {
                        spookFrameTimer--;
                    }
                    else
                    {
                        spookAlpha -= 0.05f;
                    }
                }
            }
            catch { }

            sb.End();

            #endregion
        }


        // IKLegs
        float Joint0angleA;
        float Joint0angleB;



        public static float ToRad(float deg) => deg * (float)(Math.PI / 180f);
        public static float ToDeg(float rad) => rad / (float)(Math.PI / 180f);
    }
}
