using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using MegaManRipoff.UI;

namespace MegaManRipoff.MainGameClasses
{
    /// <summary>
    /// Determines the direction that the screen could scroll in, if at all.
    /// </summary>
    public enum ScrollDirection
    {
        Up, Down, Left, Right
    }

    /// <summary>
    /// Contains the event data for scrolling events.
    /// </summary>
    class ScrollEventArgs : EventArgs
    {
        public ScrollEventArgs(Room room)
        {
            Room = room;
        }

        /// <summary>
        /// The room that the camera started/finished in.
        /// </summary>
        public Room Room { get; private set; }
    }

    /// <summary>
    /// The handler for the event called when the camera starts scrolling between rooms.
    /// </summary>
    /// <param name="sender">The sender object.</param>
    /// <param name="e">The event's arguments, if any.</param>
    delegate void ScrollStart(object sender, ScrollEventArgs e);

    /// <summary>
    /// The handler for the event called when the camera finishes scrolling between rooms.
    /// </summary>
    /// <param name="sender">The sender object.</param>
    /// <param name="e">The event's arguments, if any.</param>
    delegate void ScrollFinish(object sender, ScrollEventArgs e);

    /// <summary>
    /// A 2D camera designed for working whilst playing a level. Allows for
    /// scrolling room transitions.
    /// </summary>
    class GameCamera : Camera
    {
        #region Member Variables

        /// <summary>
        /// The current level.
        /// </summary>
        Level _level;

        /// <summary>
        /// The object to centre the camera upon.
        /// </summary>
        IFocusable _focus;

        /// <summary>
        /// Determines the direction that the screen is currently
        /// scrolling in, if at all.
        /// </summary>
        ScrollDirection _scrollDirection;

        /// <summary>
        /// Keeps track of how long the camera should scroll for.
        /// </summary>
        int _scrollTimer;

        /// <summary>
        /// The vector the screen should move upon whilst scrolling.
        /// </summary>
        Vector2 _scrollSpeed;

        /// <summary>
        /// The previous focus, used to revert to original status once
        /// the camera has finished scrolling.
        /// </summary>
        IFocusable _previousFocus;

        #region Shake Fields

        /// <summary>
        /// Keeps track of how long the camera should remaing shaking for, in ticks.
        /// </summary>
        int _shakeTimer;

        /// <summary>
        /// The length of time the camera should shake for, in ticks.
        /// </summary>
        const int ShakeDuration = 30;

        /// <summary>
        /// The variable used to make the camera oscillate when it is shaking.
        /// </summary>
        float _theta;

        /// <summary>
        /// Determines the amplitude of the shaking oscillation. This is used as a base, whilst
        /// the actual amplitude decays as the shake timer reaches zero.
        /// </summary>
        const int OscillationAmplitude = 3;

        /// <summary>
        /// Determines the frequency of the shaking oscillation, as a
        /// divisor of 1 radian (2 * pi).
        /// </summary>
        const float OscillationFrequency = 6;

        #endregion

        #endregion

        #region Event Declarations

        /// <summary>
        /// The event called when the camera starts scrolling between rooms.
        /// </summary>
        public event ScrollStart OnScrollStart;

        /// <summary>
        /// The event called when the camera finishes scrolling between rooms.
        /// </summary>
        public event ScrollFinish OnScrollFinish;

        #endregion

        #region Properties

        /// <summary>
        /// Determines if the camera is in the middle of transitioning 
        /// to the next room.
        /// </summary>
        public bool IsScrolling
        {
            get { return (_scrollTimer > 0); }
        }

        /// <summary>
        /// Gets or sets object the camera is currently following.
        /// </summary>
        public IFocusable Focus
        {
            get { return _focus; }
            set
            { 
                //Don't allow anyone to set the focus whilst scrolling.
                if (IsScrolling)
                    throw new Exception("Cannot set focus whilst the camera " + 
                        "is scrolling.");
                _focus = value;
            }
        }

        #endregion

        /// <summary>
        /// Creates a new game camera.
        /// </summary>
        /// <param name="mainGame">The main game instance.</param>
        /// <param name="level">The current level.</param>
        /// <param name="position">The position of the camera.</param>
        public GameCamera(MainGame mainGame, Level level, Vector2 position)
            : base(mainGame.GraphicsDevice, position)
        {
            _level = level;
        }

        #region Methods

        /// <summary>
        /// Determines if the given hitbox is in the camera's view.
        /// </summary>
        /// <param name="hitbox">The hitbox.</param>
        /// <returns>Returns true if the hitbox is partially in the camera view,
        /// and false otherwise.</returns>
        public bool IsInView(Rectangle hitbox)
        {
            return hitbox.Intersects(Area);
        }

        /// <summary>
        /// Determines if the given animation at its current frame and positon is
        /// in the camera's view.
        /// </summary>
        /// <param name="position">The position.</param>
        /// <param name="animation">The animation.</param>
        /// <returns>Returns true if the animation frame is partially in the camera
        /// view, and false otherwise.</returns>
        public bool IsInView(Vector2 position, Animation animation)
        {
            //Get the necessary details from the current animation frame.
            Rectangle currentFrameSource = animation.CurrentFrame.Item1;
            Vector2 currentFrameOffset = animation.CurrentFrame.Item2;

            //Construct the area the animation frame currently occupies.
            Rectangle area = new Rectangle((int)(position.X + currentFrameOffset.X),
                                           (int)(position.Y + currentFrameOffset.Y),
                                           currentFrameSource.Width,
                                           currentFrameSource.Height);
            return Area.Intersects(area);
        }

        /// <summary>
        /// Starts a room transition.
        /// </summary>
        /// <param name="scrollDirection">The direction to scroll in.</param>
        /// <param name="scrollDuration">How long the transition should last, in ticks.</param>
        public void StartScrolling(ScrollDirection scrollDirection, int scrollDuration)
        {
            //Store the scroll direction.
            _scrollDirection = scrollDirection;
   
            //Store the current focus so we can revert it later.
            _previousFocus = _focus;

            //Lose focus so that the screen can move independently.
            _focus = null;

            //Set the speed vector based on the direction to move, the distance the
            //camera has to move and how much time has been allocated.
            switch (scrollDirection) 
            {
                case ScrollDirection.Left:
                    _scrollSpeed = new Vector2(-VIEWPORT_WIDTH / (float)scrollDuration, 0);
                    break;
                case ScrollDirection.Right:
                    _scrollSpeed = new Vector2(VIEWPORT_WIDTH / (float)scrollDuration, 0);
                    break;
                case ScrollDirection.Up:
                    _scrollSpeed = new Vector2(0, -VIEWPORT_HEIGHT / (float)scrollDuration);
                    break;
                case ScrollDirection.Down:
                    _scrollSpeed = new Vector2(0, VIEWPORT_HEIGHT / (float)scrollDuration);
                    break;
            }

            //Start the timer.
            _scrollTimer = scrollDuration;

            //If there is at least one subscriber, call the start scrolling event.
            if (OnScrollStart != null)
            {
                //Pass the current room as an argument too.
                OnScrollStart(this, new ScrollEventArgs(_level.ActiveRooms[0]));
            }
        }

        /// <summary>
        /// Continues scrolling and, if necessary, stops scrolling and resumes normal camera
        /// action.
        /// </summary>
        private void Scroll()
        {
            //Forcibly move the camera and the previously focussed object.
            _position += _scrollSpeed;
            _previousFocus.Position += _scrollSpeed / 16f;

            //Decrement the timer; if it is now zero, revert status to its normal state.
            if (--_scrollTimer <= 0)
            {
                _scrollSpeed = Vector2.Zero;

                //Round the focus' position to remove any division precision errors.
                _previousFocus.Position = new Vector2((float)Math.Round(_previousFocus.Position.X),
                                                      (float)Math.Round(_previousFocus.Position.Y));
                _focus = _previousFocus;

                //If there is at least one subscriber, call the finish scrolling event.
                if (OnScrollStart != null)
                {
                    //Pass the new room as an argument too.
                    OnScrollFinish(this, new ScrollEventArgs(_level.ActiveRooms[1]));
                }
            }
        }

        /// <summary>
        /// Causes the camera to start shaking for a short time.
        /// </summary>
        public void StartShaking()
        {
            _shakeTimer = ShakeDuration;
        }

        /// <summary>
        /// Causes camera shake and returns the amount needed to shake.
        /// </summary>
        /// <returns>The vector after calculating shakiness.</returns>
        private Vector2 Shake()
        {
            //Decrement the timer; if it is now zero, return the zero vector.
            if (--_shakeTimer <= 0)
            {
                //Also set the theta variable to zero.
                _theta = 0;
                return Vector2.Zero;
            }

            Vector2 returnVector = Vector2.Zero;

            //Otherwise, increase the theta value and increase the Y value dependent on the sine
            //of theta.
            _theta += (float)(2 * Math.PI) / OscillationFrequency;
            returnVector.Y = OscillationAmplitude * (float)Math.Sin(_theta);

            //Now change the vector according to the proportion between the time left on the timer and
            //the maximum time on the timer.
            returnVector.Y *= _shakeTimer / (float)ShakeDuration;

            return returnVector;
        }

        /// <summary>
        /// Performs the game camera's update game loop.
        /// </summary>
        /// <param name="gameTime">The game's current snapshot of time.</param>
        public void Update(GameTime gameTime)
        {
            //If we're scrolling, do the scroll method.
            if (IsScrolling)
            {
                Scroll();
            }

            //Check again - if we're now NOT scrolling...
            if (!IsScrolling)
            {
                //Follow the focus object, as long as it exists.
                if (_focus != null)
                {
                    //Keep the camera within the bounds of the current room.
                    _position.X = MathHelper.Clamp(_focus.FocusPoint.X - (VIEWPORT_WIDTH / 2),
                        _level.ActiveRooms[0].Area.Left, _level.ActiveRooms[0].Area.Right - (VIEWPORT_WIDTH * 1f));
                    _position.Y = MathHelper.Clamp(_focus.FocusPoint.Y - (VIEWPORT_HEIGHT / 2),
                        _level.ActiveRooms[0].Area.Top, _level.ActiveRooms[0].Area.Bottom - (VIEWPORT_HEIGHT * 1f));
                }
            }

            //Add shake.
            _position.Y += Shake().Y;

            base.Update();
        }

        #endregion
    }
}
