using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace BreadAndCheesePong {

    public interface ISpriteBasedGame
    {
        void SetMessage(string message);
        GamePadState GetGamePad(PlayerIndex player);
        void StartGame();
        void EndGame();
    }

#region Sprajty

    public class BaseSprite {
        protected Texture2D spriteTexture;
        protected Rectangle spriteRectangle;

        /// <summary>
        /// Ładuje teksturę używaną w ramach danego sprajtu.
        /// </summary>
        /// <param name="inSpriteTexture">Używana tekstura.</param>
        public void LoadTexture(Texture2D inSpriteTexture) {
            spriteTexture = inSpriteTexture;
        }

        /// <summary>
        /// Ładuje prostokąt wyznaczający wymiary na potrzeby 
        /// operacji rysowania tego sprajtu tła.
        /// </summary>
        /// <param name="inSpriteRectangle">Prostokąt, który ma zostać użyty.</param>
        public void SetRectangle(Rectangle inSpriteRectangle) {
            spriteRectangle = inSpriteRectangle;
        }

        /// <summary>
        /// Metoda wywoływana w momencie uruchamiania gry
        /// </summary>
        public virtual void StartGame() {
        }

        /// <summary>
        /// Metoda wywoływana w momencie zatrzymywania gry
        /// </summary>
        public virtual void EndGame() {
        }

        /// <summary>
        /// Rysuje teksturę na podstawie prostokąta określającego jej wymiary.
        /// </summary>
        /// <param name="spriteBatch">Obiekt klasy SpriteBatch, który ma zostać użyty
        /// przez operację rysowania.</param>
        public virtual void Draw(SpriteBatch spriteBatch) {
            spriteBatch.Draw(spriteTexture, spriteRectangle, Color.White);
        }

        /// <summary>
        /// Aktualizuje zachowanie dla tytułu. Sprajt bazowy nie zawiera żadnego
        /// zachowania aktualizacji, ale pozostałe sprajty mogą zawierać takie zachowania.
        /// </summary>
        /// <param name="game">Gra, której przebieg ma być kontrolowany.</param>
        public virtual void Update(ISpriteBasedGame game) {
        }

        public BaseSprite(Texture2D inSpriteTexture, Rectangle inRectangle) {
            LoadTexture(inSpriteTexture);
            SetRectangle(inRectangle);
        }

    }

    public class TitleSprite : BaseSprite {
        /// <summary>
        /// Aktualizuje zachowanie dla tytułu. Metoda testuje stan przycisku A i uruchamia grę,
        /// jeśli ten przycisk jest wciśnięty.
        /// </summary>
        /// <param name="game">Gra, której przebieg ma być kontrolowany.</param>
        public override void Update(ISpriteBasedGame game) {
            if (game.GetGamePad(PlayerIndex.One).Buttons.A == ButtonState.Pressed) {
                game.StartGame();
            }
        }

        public TitleSprite(Texture2D inSpriteTexture, Rectangle inRectangle)
        : base(inSpriteTexture, inRectangle) {
        }
    }

    public class MovingSprite : BaseSprite {
        protected float x;
        protected float y;
        protected float xSpeed;
        protected float ySpeed;

        protected float initialX;
        protected float initialY;

        protected float minDisplayX;
        protected float maxDisplayX;

        protected float minDisplayY;
        protected float maxDisplayY;

        float displayWidth;
        float displayHeight;

        public float XPos
        {
            get
            {
                return x;
            }
        }

        public float YPos
        {
            get
            {
                return y;
            }
        }


        /// <summary>
        /// Sprawdza ewentualne kolizje pomiędzy danym sprajtem a pozostałymi obiektami.
        /// </summary>
        /// <param name="target">Prostokąt reprezentujący położenie innego obiektu.</param>
        /// <returns>true, jeśli chleb koliduje z danym obiektem</returns>
        public virtual bool CheckCollision(Rectangle target) {
            return spriteRectangle.Intersects(target);
        }

        /// <summary>
        /// Tworzy poruszający się sprajt
        /// </summary>
        /// <param name="inSpriteTexture">Tekstura używana dla tego sprajtu</param>
        /// <param name="widthFactor">Rozmiar obiektu w grze wyrażony jako ułamek szerokości
        /// ekranu.</param>
        /// <param name="ticksToCrossScreen">Szybkość poruszania się obiektu wyrażona w liczbie taktów (1/60 sekundy) 
        /// potrzebnych do pokonania całego ekranu.</param>
        /// <param name="inMinDisplayX">minimalna wartość XPos</param>
        /// <param name="inMaxDisplayX">maksymalna wartość XPos</param>
        /// <param name="inMinDisplayY">minimalna wartość YPos</param>
        /// <param name="inMaxDisplayY">maksymalna wartość YPos</param>
        /// <param name="initialX">początkowa pozycja XPos pałki</param>
        /// <param name="initialY">początkowa pozycja YPos pałki</param>
        public MovingSprite(
                Texture2D inSpriteTexture,
                float widthFactor,
                float ticksToCrossScreen,
                float inMinDisplayX,
                float inMaxDisplayX,
                float inMinDisplayY,
                float inMaxDisplayY,
                float inInitialX,
                float inInitialY)
        : base(inSpriteTexture, Rectangle.Empty) {
            minDisplayX = inMinDisplayX;
            minDisplayY = inMinDisplayY;
            maxDisplayX = inMaxDisplayX;
            maxDisplayY = inMaxDisplayY;
            initialX = inInitialX;
            initialY = inInitialY;

            displayWidth = maxDisplayX - minDisplayX;
            displayHeight = maxDisplayY - minDisplayY;

            spriteRectangle.Width = (int) ((displayWidth * widthFactor) + 0.5f);
            float aspectRatio =
                    (float) spriteTexture.Width / spriteTexture.Height;
            spriteRectangle.Height =
                    (int) ((spriteRectangle.Width / aspectRatio) + 0.5f);
            xSpeed = displayWidth / ticksToCrossScreen;
            ySpeed = xSpeed;
        }

        public override void StartGame() {
            x = minDisplayX + (initialX * displayWidth);
            y = minDisplayY + (initialY * displayHeight);
            spriteRectangle.X = (int) x;
            spriteRectangle.Y = (int) y;
            base.StartGame();
        }
    }

    public class BatSprite : MovingSprite {
        private PlayerIndex player;

        /// <summary>
        /// Aktualizuje położenie pałki zależnie od stanu pada.
        /// </summary>
        public override void Update(ISpriteBasedGame game) {
            x = x + (xSpeed * game.GetGamePad(player).ThumbSticks.Left.X);
            y = y - (ySpeed * game.GetGamePad(player).ThumbSticks.Left.Y);
            spriteRectangle.X = (int) x;
            spriteRectangle.Y = (int) y;
        }

        public BatSprite(Texture2D inSpriteTexture,
                float widthFactor, float ticksToCrossScreen,
                float inMinDisplayX, float inMaxDisplayX,
                float inMinDisplayY, float inMaxDisplayY,
                float inInitialX, float inInitialY,
                PlayerIndex inPlayer)
        : base(inSpriteTexture, widthFactor, ticksToCrossScreen,
        inMinDisplayX, inMaxDisplayX,
        inMinDisplayY, inMaxDisplayY,
        inInitialX, inInitialY) {
            player = inPlayer;
        }

    }

    public class BallSprite : MovingSprite {
        private SoundEffect BatHitSound;

        private SoundEffect RepeatHitSound;

        private SoundEffect EdgeHitSound;

        private SoundEffect LoseLifeSound;

        private BatSprite player1;
        private BatSprite player2;

        private int player1Score;
        private int player2Score;

        /// <summary>
        /// Aktualizuje położenie piłki. Obsługuje kolizje z pałką
        /// </summary>
        /// <param name="game">Gra, w której występuje sprajt sera</param>
        public override void Update(ISpriteBasedGame game) {
            x = x + xSpeed;
            y = y + ySpeed;

            spriteRectangle.X = (int) (x + 0.5f);
            spriteRectangle.Y = (int) (y + 0.5f);

            if (player1.CheckCollision(spriteRectangle)) {
                // Pierwszy gracz trafił w piłkę.
                BatHitSound.Play();
                xSpeed = xSpeed * -1;
            }

            if (player2.CheckCollision(spriteRectangle)) {
                // Drugi gracz trafił w piłkę.
                BatHitSound.Play();
                xSpeed = xSpeed * -1;
            }

            if (x + spriteRectangle.Width >= maxDisplayX) {
                // Piłka została trafiona z prawej strony.
                // Pierwszy gracz zdobył punkty.
                LoseLifeSound.Play();
                player1Score++;
                xSpeed = Math.Abs(xSpeed) * -1;
            }

            if (x <= minDisplayX) {
                // Piłka została trafiona z lewej strony.
                // Drugi gracz zdobył punkty.
                LoseLifeSound.Play();
                player2Score++;
                xSpeed = Math.Abs(xSpeed);
            }

            if (y + spriteRectangle.Height >= maxDisplayY) {
                // Piłka zderzyła się z dolną krawędzią.
                EdgeHitSound.Play();
                ySpeed = Math.Abs(ySpeed) * -1;
            }

            if (y <= minDisplayY) {
                // Piłka uderzyła w górną część ekranu.
                EdgeHitSound.Play();
                ySpeed = Math.Abs(ySpeed);
            }
            game.SetMessage(player1Score.ToString() + " : " + player2Score.ToString());
        }

        public override void StartGame() {
            player1Score = 0;
            player2Score = 0;
            base.StartGame();
        }

        public BallSprite(Texture2D inSpriteTexture,
                float widthFactor, float ticksToCrossScreen,
                float inMinDisplayX, float inMaxDisplayX,
                float inMinDisplayY, float inMaxDisplayY,
                float inInitialX, float inInitialY,
                BatSprite inPlayer1,
                BatSprite inPlayer2,
                SoundEffect inBatHitSound,
                SoundEffect inRepeatHitSound,
                SoundEffect inEdgeHitSound,
                SoundEffect inLoseLifeSound)
        : base(inSpriteTexture, widthFactor, ticksToCrossScreen,
        inMinDisplayX, inMaxDisplayX,
        inMinDisplayY, inMaxDisplayY,
        inInitialX, inInitialY) {
            player1 = inPlayer1;
            player2 = inPlayer2;
            BatHitSound = inBatHitSound;
            RepeatHitSound = inRepeatHitSound;
            EdgeHitSound = inEdgeHitSound;
            LoseLifeSound = inLoseLifeSound;
        }
    }

#endregion


    /// <summary>
    /// To jest główny typ Twojej gry.
    /// </summary>
    public class PongGame : Microsoft.Xna.Framework.Game, ISpriteBasedGame {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // Świat gry

        public BatSprite Player1;
        public BatSprite Player2;
        public BallSprite Cheese;
        public TitleSprite Title;
        public BaseSprite Background;

        public List<BaseSprite> GameSprites = new List<BaseSprite>();

#region Wartości reprezentujące wymiary ekranu

        int displayWidth;
        int displayHeight;
        float overScanPercentage = 10.0f;
        float minDisplayX;
        float maxDisplayX;
        float minDisplayY;
        float maxDisplayY;

        private void setScreenSizes() {
            displayWidth = graphics.GraphicsDevice.Viewport.Width;
            displayHeight = graphics.GraphicsDevice.Viewport.Height;
            float xOverscanMargin = getPercentage(overScanPercentage, displayWidth) / 2.0f;
            float yOverscanMargin = getPercentage(overScanPercentage, displayHeight) / 2.0f;

            minDisplayX = xOverscanMargin;
            minDisplayY = yOverscanMargin;

            maxDisplayX = displayWidth - xOverscanMargin;
            maxDisplayY = displayHeight - yOverscanMargin;
        }


#endregion

#region Metody pomocnicze

        /// <summary>
        /// Oblicza wartości procentowe
        /// </summary>
        /// <param name="percentage">procent, który ma zostać wyznaczony</param>
        /// <param name="inputValue">wartość do przeliczenia</param>
        /// <returns>wartość wynikowa</returns>

        float getPercentage(float percentage, float inputValue) {
            return (inputValue * percentage) / 100;
        }

#endregion

#region Rysowanie tekstu

        SpriteFont font;

        string displayMessage = "";

        private void loadFont() {
            font = Content.Load<SpriteFont > ("SpriteFont1");
        }

        /// <summary>
        /// Rysuje tekst na ekranie
        /// </summary>
        /// <param name="text">tekst do wyświetlenia</param>
        /// <param name="textColor">kolor tekstu</param>
        /// <param name="x">lewa krawędź tekstu</param>
        /// <param name="y">górna krawędź tekstu</param>

        void drawText(string text, Color textColor, float x, float y) {
            int layer;
            Vector2 textVector = new Vector2(x - 16, y - 16);

            // Rysuje cień
            Color backColor = new Color(0, 0, 0, 10);
            for (layer = 0; layer < 10; layer++) {
                spriteBatch.DrawString(font, text, textVector, backColor);
                textVector.X--;
                textVector.Y++;
            }

            // Rysuje podstawę znaków
            backColor = new Color(190, 190, 190);
            for (layer = 0; layer < 5; layer++) {
                spriteBatch.DrawString(font, text, textVector, backColor);
                textVector.X--;
                textVector.Y++;
            }

            // Rysuje przednią część znaków
            spriteBatch.DrawString(font, text, textVector, textColor);
        }

        public void CenterBottomMessage(string displayMessage, Color color) {
            Vector2 messageSize = font.MeasureString(displayMessage);
            float y = displayHeight - messageSize.Y;
            float x = (displayWidth - messageSize.X) / 2;
            drawText(displayMessage, color, x, y);

        }

        public void SetMessage(string message) {
            displayMessage = message;
        }

#endregion


#region Zarządzanie grą i punktacją

        public enum GameState {
            titleScreen,
            playingGame
        }

        GameState state = GameState.titleScreen;

        public void StartGame() {

            foreach(BaseSprite sprite in GameSprites) {
                sprite.StartGame();
            }

            state = GameState.playingGame;
        }

        public GamePadState GetGamePad(PlayerIndex player) {
            return GamePad.GetState(player);
        }

        public void EndGame() {

            foreach(BaseSprite sprite in GameSprites) {
                sprite.EndGame();
            }

            state = GameState.titleScreen;
        }

        public GameState GetState() {
            return state;
        }

#endregion

        public PongGame() {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Umożliwia ewentualną inicjalizację przed uruchomieniem właściwej gry.
        /// W tym miejscu można odnaleźć wszystkie wymagane zasoby i załadować treści
        /// related content. Wywołanie metody base.Initialize spowoduje przeszukanie wszystkich komponentów
        /// i ich inicjalizację.
        /// </summary>
        protected override void Initialize() {
            setScreenSizes();
            base.Initialize();
        }

        /// <summary>
        /// Metoda LoadContent jest wywoływana tylko raz dla całej gry i jako taka jest właściwym miejscem
        /// dla kodu ładującego treść.
        /// </summary>
        protected override void LoadContent() {
            // Tworzy nowy obiekt klasy SpriteBatch, którego można używać do rysowania tekstur.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            Background = new BaseSprite(
                    Content.Load<Texture2D > ("Images/Background"),
                    new Rectangle(0, 0, displayWidth, displayHeight));

            GameSprites.Add(Background);

            Title = new TitleSprite(
                    Content.Load<Texture2D > ("Images/Title"),
                    new Rectangle(0, 0, displayWidth, displayHeight));

            Player1 = new BatSprite(
                    Content.Load<Texture2D > ("Images/WhiteBread"),
                    0.166f, // szerokość chleba wynosi 0,166 szerokości ekranu
                    150, // pokonanie ekranu zajmuje 150 taktów
                    minDisplayX, maxDisplayX, minDisplayY, maxDisplayY,
                    0.25f, // jedna czwarta szerokości ekranu od lewej krawędzi
                    0.5f, // środek ekranu (w pionie)
                    PlayerIndex.One); // sterowany przez pierwszego gracza

            GameSprites.Add(Player1);

            Player2 = new BatSprite(
                    Content.Load<Texture2D > ("Images/BrownBread"),
                    0.166f, // szerokość chleba wynosi 0,166 szerokości ekranu
                    150, // pokonanie ekranu zajmuje 150 taktów
                    minDisplayX, maxDisplayX, minDisplayY, maxDisplayY,
                    0.75f, // jedna czwarta szerokości ekranu od prawej krawędzi
                    0.5f, // środek ekranu (w pionie)
                    PlayerIndex.Two); // sterowany przez drugiego gracza

            GameSprites.Add(Player2);

            Cheese = new BallSprite(
                    Content.Load<Texture2D > ("Images/Cheese"),
                    0.07f, // szerokość sera wynosi 0,07 szerokości ekranu
                    200, // ser potrzebuje 200 taktów na pokonanie całego ekranu
                    minDisplayX, maxDisplayX, minDisplayY, maxDisplayY,
                    0.25f, // jedna czwarta szerokości ekranu od lewej krawędzi
                    0.25f, // jedna czwarta wysokości ekranu od górnej krawędzi
                    Player1, // pierwszy gracz
                    Player2, // drugi gracz
                    Content.Load<SoundEffect > ("Sounds/BreadHit"),
                    Content.Load<SoundEffect > ("Sounds/RepeatHit"),
                    Content.Load<SoundEffect > ("Sounds/EdgeHit"),
                    Content.Load<SoundEffect > ("Sounds/LoseLife"));

            GameSprites.Add(Cheese);

            loadFont();
        }

        /// <summary>
        /// Metoda UnloadContent jest wywoływana tylko raz dla całej gry i jako taka jest właściwym miejscem
        /// dla kodu usuwającego całą treść z pamięci.
        /// </summary>
        protected override void UnloadContent() {
            // TODO: Należy usunąć całą treść, którą nie zarządza ContentManager.
        }

        /// <summary>
        /// Umożliwia grze wykonywanie logiki związanej z aktualizacją świata gry,
        /// sprawdzaniem kolizji, pobieraniem danych wejściowych czy odtwarzaniem dźwięków.
        /// </summary>
        /// <param name="gameTime">Udostępnia wartości reprezentujące bieżący czas.</param>
        protected override void Update(GameTime gameTime) {
            // Pozwala opuścić grę
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            switch (state) {
                case GameState.titleScreen:
                    Title.Update(this);
                    break;
                case GameState.playingGame:
                    foreach(BaseSprite sprite in GameSprites)
                {
                    sprite.Update(this);
                }
                    break;
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// Ta metoda jest wywoływana w momencie, w którym gra musi narysować swój świat.
        /// </summary>
        /// <param name="gameTime">Udostępnia wartości reprezentujące bieżący czas.</param>
        protected override void Draw(GameTime gameTime) {
            spriteBatch.Begin();

            switch (state) {
                case GameState.titleScreen:
                    Title.Draw(spriteBatch);
                    break;
                case GameState.playingGame:
                    foreach(BaseSprite sprite in GameSprites)
                {
                    sprite.Draw(spriteBatch);
                }
                    break;
            }

            CenterBottomMessage(displayMessage, Color.Red);

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}
