using System;
using System.Text;
using Microsoft.DirectX.Direct3D;
using Microsoft.DirectX;
using System.Drawing;
using CanonGfx;
using System.Diagnostics;
using System.Windows.Forms;
using ChtiCanon;
using System.Xml;
using System.Xml.XPath;
using System.Collections.Generic;
using Shared;

namespace Canon
{
    public delegate void SimpleEvent(object sender);

    class Game
    {
        // Paramètres généraux
        // Dessin
        public const int screenWidth = 640, screenHeight = 360;
        // Objet permettant le dessin
        // On viole un peu l'encapsulation en l'exposant, cela permet de
        // simplifier énormément la structure du code plutôt que passer l'objet
        // à toutes les méthodes.
        private static Gfx drawer = null;
        public static Gfx Drawer { get { return Game.drawer; } }
        // Etat de la souris
        // Cet objet est piloté par le contrôle encapsulant une instance de
        // Game, qui doit lui fournir les événements (souris a bougé, etc.)
        public CanonInterface.MouseState mouse = new CanonInterface.MouseState();
        // Parents, pour une communication plus facile (par exemple savoir la
        // taille courante de l'affichage pour s'adapter).
        private GameControl gc;
        // Permet d'obtenir des informations (paramètres fournis au thread
        // gérant le lancement du jeu).
        public Program.GameThread GameThread { get { return Net ? gc.GameThread : null; } }
        // Réseau actif?
        public bool Net { get { return gc.GameThread.Connection != null; } }
        // Terrain de jeu (aussi appelé map)
        private BackgroundImage background = null;
        private Terrain terrain;
        public Terrain Terrain { get { return terrain; } }
        // Paramètres de la caméra
        private bool dezoom;
        private VectorPositionInterpolator scrollPosition, zoomValue;
        public PlayingObject CameraFollows;
        // Panneau d'armes
        private CanonInterface.GunPanel gunPanel;
        // Infos sur les joueurs
        private Player[] players;
        private int myPlayerNumber, currentPlayerTurn = 0;
        // Notre joueur
        private Player MyPlayer { get { return players[myPlayerNumber]; } }
        // Joueur qui a le tour
        private Player TurnPlayer { get { return players[currentPlayerTurn]; } }
        // Est-ce mon tour?
        private bool MyTurn { get { return TurnPlayer == MyPlayer; } }
        // Couleurs prédéfinies des tanks
        private static Color[] tankColors = new Color[]
        {
            Color.FromArgb(255, 64, 64),
            Color.FromArgb(255, 255, 64),
            Color.FromArgb(64, 255, 64),
            Color.FromArgb(64, 255, 255),
            Color.FromArgb(64, 64, 255),
            Color.FromArgb(255, 64, 255),
        };
        // Timer permettant d'attendre un peu avant de donner le prochain tour
        private Timer nextTurnTimer = new Timer(120);
        // Infos sur le vent
        private float windDirection = 0, windForce = 0;
        public float WindDirection { get { return windDirection; } }
        public float WindForce { get { return windForce; } }
        // La physique des caisses ne peut être active que pendant les tirs et l'attente entre tours
        // C'est à dire un nombre de frames bien précis, afin d'avoir un comportement déterministe
        // et synchronisé sur tous les clients
        private bool PhysicsActive { get { return physicsTime != 0; } set { physicsTime = value ? 150 : 0; } }
        private int physicsTime;
        // Objet connexion à utiliser pour envoyer des données
        public ConnectionManager Connection { get { return Net? GameThread.Connection: null; } }
        private Round receivedRound = null;
        private Shoot receivedShoot = null;
        // La caisse est suivie pendant 180 frames, dont 150 où la physique est active
        private Timer endFollowingCrateTimer = new Timer(180);
        // Liste des caisses
        private List<Caisse> crateList = new List<Caisse>();
        // La GE List contient des composants généraux (en l'occurence juste
        // des explosions, temporaires). Ces éléments sont gérés en arrière-
        // plan et dessinés à chaque rendu.
        private List<GraphicalElement> geList = new List<GraphicalElement>();
        // Composants de l'interface
        private CanonInterface.ComponentManager components = new CanonInterface.ComponentManager();
        private CanonInterface.WindIndicator windIndicator;
        private CanonInterface.PlayerStatIndicator indicator;
        // Ressources (images, sons, polices) év. partagées
        private CanonAudio.Sound bgMusic;
        public static CanonGfx.Font font, fontSmall, fontVerySmall;
        public static CanonAudio.Sound sfxTir;

        // Constructeur
        public Game(Device dev, GameControl gc)
        {
            // Map à jouer en local
            int map = 4;
            // Parent
            this.gc = gc;
            // En local, seulement 2 joueurs
            if (Net)
            {
                players = new Player[GameThread.Game.pos.Length];
                myPlayerNumber = GameThread.GamerId;
                map = GameThread.Game.map + 1;
            }
            else
                players = new Player[2];

            // Chargement des réglages
            Settings set = new Settings(getResourceFile("map-script" + map + ".xml"));
            // Chargement du terrain
            drawer = new Gfx(dev);
            terrain = new Terrain(Game.drawer.createImage(set.bgFile, set.width, set.height, Format.A8B8G8R8), set.width, set.height);
            background = new BackgroundImage(drawer.createImage(set.skyFile, set.width, set.height, Format.A8B8G8R8));

            // Crée les joueurs
            for (int i = 0; i < players.Length; i++)
            {
                int noPos = Net? GameThread.Game.pos[i] : i;
                players[i] = new Player(this, new Vector2(set.playerPos[noPos].X, set.playerPos[noPos].Y), terrain, tankColors[i]);
                // FIXME
                if (Net)
                    players[i].Name = GameThread.PlayerName(i);
                else
                    players[i].Name = "Player " + (i + 1);
            }

            // Eléments graphiques par défaut
            scrollPosition = new VectorPositionInterpolator(0, 0, 0.1f);
            zoomValue = new VectorPositionInterpolator(1, 1, 0.15f);
            components.add(new CanonInterface.ZoomButton(this, new Vector2(screenWidth - 46, 0)));
            components.add(new CanonInterface.GunListButton(this, new Vector2(screenWidth - 46, 30)));
            windIndicator = new CanonInterface.WindIndicator(new Vector2(0, 0));
            components.add(windIndicator);
            gunPanel = new CanonInterface.GunPanel(MyPlayer);
            components.add(gunPanel);
            indicator = new CanonInterface.PlayerStatIndicator(this, players);
            components.add(new CanonInterface.PlayerPanelButton(this, new Vector2(0, 80)));
            components.add(indicator);
            mouse.setLeft();

            components.add(nextTurnTimer);
            components.add(endFollowingCrateTimer);
            TurnPlayer.TurnFinished += turnFinished;
            CameraFollows = TurnPlayer;
            PhysicsActive = false;

            // Création des ressources statiques
            initStaticResources();
            try
            {
                bgMusic = new CanonAudio.Sound(getResourceFile(set.bgMusic), true);
                bgMusic.Play();
            }
            // On ne fait rien si le son n'est pas trouvé
            catch (Exception) { }

            endFollowingCrateTimer.Tick += endFollowingCrate;

            if (Net)
            {
                connectSrvEvents(Connection);
                // Premier tour "bidon" pour indiquer au serveur qu'on est prêt
                turnFinished(this);
            }
            else
                TurnPlayer.CanFire = true;
        }

        // Création des fichiers (images, sons, etc.) partagés
        private void initStaticResources()
        {
            if (sfxTir == null)
            {
                sfxTir = new CanonAudio.Sound(getResourceFile("tir.wav"), false);
                font = Game.drawer.createFont("segoe.png", "segoe.txt");
                fontSmall = Game.drawer.createFont("segoesmall.png", "segoesmall.txt");
                fontVerySmall = Game.drawer.createFont("segoevsmall.png", "segoevsmall.txt");
            }
        }

        // Calcule la matrice de transformation pour la scène
        private Matrix getZoomScrollMatrix()
        {
            Vector2 scroll = clampScrolling(scrollPosition.Current);
            return Matrix.Translation((int)scroll.X, (int)scroll.Y, 0) *
                   Matrix.Scaling(zoomValue.X, zoomValue.Y, 1);
        }

        // Appelé tous les "tick" (60 fois par seconde)
        public void handle()
        {
            if (physicsTime > 0)
                physicsTime--;
            // On a reçu l'ordre qu'un client a tiré -> on tirera dès que tout sera figé
            if (!PhysicsActive && receivedShoot != null)
            {
                TurnPlayer.fire(receivedShoot);
                receivedShoot = null;
            }
            // Gestion de la souris
            mouse.handle();
            bool mouseInInterface = components.handle(mouse);
            // Si le joueur joue et la souris n'est pas capturée par l'interface
            // On peut définir l'angle de la tourelle
            if (!gunPanel.Open && !mouseInInterface && MyTurn)
            {
                // Transforme les coordonnées pour les passer au tank
                Vector2 oldPos = mouse.Position, newPos = mouse.AbsolutePosition;
                float oldShotAngle = TurnPlayer.ShotAngle;
                // Les coordonnées du tank sont transformées par la matrice de
                // la scène, donc on transforme les coordonnées de la souris
                // dans les coordonnées locales du tank par l'opération inverse
                newPos.TransformCoordinate(Matrix.Invert(getZoomScrollMatrix()));
                mouse.Position = new Vector2(newPos.X / screenWidth, newPos.Y / screenHeight);
                MyPlayer.handle(mouse, PhysicsActive);
                mouse.Position = oldPos;
                // Si l'angle a été modifié, on l'envoie au serveur
                if (oldShotAngle != TurnPlayer.ShotAngle && Net)
                    Connection.send(new Angle(TurnPlayer.ShotAngle));
            }
            else
                // Ce joueur n'a pas la souris (null)
                TurnPlayer.handle(null, PhysicsActive);

            if (!Net)       // GunPanel gère joueur courant
                gunPanel.Player = TurnPlayer;

            CanonGfx.Rectangle missileRectangle = null;
            if (TurnPlayer.Missile != null)
            {
                missileRectangle = new CanonGfx.Rectangle(TurnPlayer.Missile.X, TurnPlayer.Missile.Y, TurnPlayer.Missile.Width, TurnPlayer.Missile.Height);
                // Les éléments vont pouvoir bouger un moment
                PhysicsActive = true;
            }
            foreach (Player p in players)
            {
                // On traite le joueur en cours à part
                if (p == TurnPlayer)
                    continue;
                p.handle(null, PhysicsActive);
                // Essaie de voir si le missile du joueur courant les touche
                if (missileRectangle != null)
                    if (p.inMe(missileRectangle))
                    {
                        // On fait déjà perdre un peu de vie au joueur directement touché
                        // Il en perdra encore plus avec le souffle après
                        p.Life -= TurnPlayer.Missile.LifeLose;
                        // FIXME: "pousse" le tank un peu si on lui tire directement dessus (on ne gère pas le souffle de l'explosion)
                        p.push(TurnPlayer.Missile.Speed * 0.3f);
                        TurnPlayer.Missile.explode();
                    }
            }
            // Gestion des caisses
            for (int i = 0; i < crateList.Count; i++)
            {
                Caisse c = crateList[i];
                c.handle(PhysicsActive);
                // Le missile touche-t-il la caisse?
                if (TurnPlayer.Missile != null)
                    if (c.inMe(missileRectangle))
                    {
                        // Ajoute l'arme reçue
                        TurnPlayer.GunCount[c.ContainedGun]++;
                        TurnPlayer.Missile.explode();
                        crateList.Remove(c);
                        // L'élément courant est devenu la prochaine caisse
                        i--;
                    }
            }
            // Eléments divers
            for (int i = 0; i < geList.Count; i++)
            {
                geList[i].handle();
                // Anim terminée
                if (geList[i].Finished)
                    geList.Remove(geList[i]);
            }

            // Ferme le menu de sélection des armes si on clique ailleurs
            if (mouse.isClicked(MouseButtons.Left) && !mouseInInterface)
                gunPanel.Open = false;
            // Animation du scrolling et zoom
            scrollPosition.handle();
            zoomValue.handle();
            windIndicator.SetUp(windDirection, windForce);
            handleCamera();
        }

        // Réalise le dessin de la scène
        public void draw()
        {
            Vector2 scroll = clampScrolling(scrollPosition.Current);

            // Dessin de l'arrière plan
            Matrix initial = drawer.CurrentMatrix;
            drawer.clearScreen(Color.Black);
            // Tient compte du zoom et du scrolling
            drawer.scaleScreen(zoomValue.X, zoomValue.Y);
            background.draw(
                new Vector2(scroll.X * 0.5f, scroll.Y * 0.9f),
                new Vector2((float)screenWidth / zoomValue.X, (float)screenHeight / zoomValue.Y));

            // Dessin de l'avant plan
            drawer.moveScreen(scroll.X, scroll.Y);
            terrain.draw();
            // Dessin des objets par-dessus
            foreach (Player p in players)
                p.draw();
            foreach (Caisse c in crateList)
                c.draw();
            foreach (GraphicalElement e in geList)
                e.draw();

            // Restaure l'état du rendu
            drawer.CurrentMatrix = initial;
            // Dessine l'interface
            components.draw();
            // Puis un petit texte
            drawer.drawText(font, 1, 341, "Presented by Brunni, Artémis, Fun_Key, Romain and Evilsky", Color.FromArgb(128, 0, 0, 0));
            drawer.drawText(font, 0, 340, "Presented by Brunni, Artémis, Fun_Key, Romain and Evilsky", Color.White);
        }

        private void handleCamera()
        {
            if (dezoom)
                return;
            // Fait en sorte que l'objet suivi se retrouve à peu près au milieu de l'écran
            float centerX = CameraFollows.X + CameraFollows.Width / 2,
                  centerY = CameraFollows.Y + CameraFollows.Height / 2,
                  posX = centerX - (float)screenWidth / 2,
                  posY = centerY - (float)screenHeight / 2;
/*            Vector2 oldZoomValue = zoomValue.Current;
            zoomValue.Current = new Vector2(1, 1);
            scrollPosit ion.Dest = clampScrolling(new Vector2(-posX, -posY));
            zoomValue.Current = oldZoomValue;*/
            scrollPosition.Dest = new Vector2(-posX, -posY);

            zoomValue.Dest.X = 1;
            zoomValue.Dest.Y = 1;
            // Scrolling lent pour un léger effet de décalage plus réaliste
            if (Math.Abs(zoomValue.X - 1) < 0.001f && Math.Abs(zoomValue.Y - 1) < 0.001f)
                scrollPosition.Speed = new Vector2(0.05f, 0.05f);
        }

        private Vector2 clampScrolling(Vector2 scroll)
        {
            scroll.X = Math.Max(-terrain.Width + screenWidth / zoomValue.X, Math.Min(0, scroll.X));
            scroll.Y = Math.Max(-terrain.Height + screenHeight / zoomValue.Y, Math.Min(0, scroll.Y));
            return scroll;
        }

        public void addExplosion(Missile m, float x, float y, float size)
        {
            geList.Add(new Explosion(x, y, size * 2.5f));
            handleExplosion(m, x, y, size);
        }

        // Passe au prochain joueur
        private void giveTurnToNextPlayer(object sender)
        {
            Caisse newCrate = null;
            // Pas encore reçu?
            if (Net && receivedRound == null)
            {
                // Attend un peu et reteste
                nextTurnTimer.start();
                return;
            }
            // Annule le timer
            nextTurnTimer.Tick -= giveTurnToNextPlayer;
            TurnPlayer.TurnFinished -= turnFinished;
            newCrate = null;
            if (Net)
            {
                windDirection = receivedRound.angle * 2.0f * (float)Math.PI;
                windForce = receivedRound.windForce;
                currentPlayerTurn = receivedRound.player;
                // Le joueur n'avait pas le droit de jouer, il est mort...
                if (TurnPlayer.Life == 0)
                    signalCheat("Player " + TurnPlayer.Name + " could not play");
                // FIXME: manque quantité & arme!
                if (receivedRound.cratePos >= 0)
                    crateList.Add(newCrate = new Caisse(this, receivedRound.cratePos * (terrain.Width - 64), -60, receivedRound.crateCont));
                receivedRound = null;
            }
            else
            {
                windDirection = Rand.Float * 2.0f * (float)Math.PI;
                windForce = Rand.Float;
                currentPlayerTurn = (currentPlayerTurn + 1) % players.Length;
                // FIXME
                if (Rand.Float < 0.1f)
                    crateList.Add(newCrate = new Caisse(this, Rand.Float * (terrain.Width - 64), -60, (int)(Rand.Float * 3) + 1));
            }

            if (newCrate != null)
            {
                endFollowingCrateTimer.start();
                CameraFollows = newCrate;
                // A l'occasion d'une nouvelle caisse, on réactive la physique un petit moment
                PhysicsActive = true;
            }
            else
                // Pas de caisse à suivre => on passe directement au prochain round
                nextRound();
        }

        // On a fini de suivre la caisse
        public void endFollowingCrate(object o)
        {
            nextRound();
        }

        // Donne le tour au prochain joueur
        public void nextRound()
        {
            // En local on joue tout le monde
            if (!Net)
                myPlayerNumber = currentPlayerTurn;
            TurnPlayer.TurnFinished += turnFinished;
            TurnPlayer.CanFire = true;
            CameraFollows = TurnPlayer;
        }

        private void turnFinished(object sender)
        {
            // Attend deux secondes pour la destruction
            nextTurnTimer.start();
            nextTurnTimer.Tick += giveTurnToNextPlayer;
            // Dit au serveur si on est vivant ou pas
            if (Net)
                Connection.send(MyPlayer.Life > 0);
        }

        // Le bouton d'ouverture de la liste des armes a été pressé
        public void gunListButtonPressed()
        {
            gunPanel.Open = !gunPanel.Open;
        }
        // Le bouton de zoom a été pressé
        public void zoomButtonPressed()
        {
            if (dezoom = !dezoom)
            {
                // Calcule le format optimal pour qu'on voit tout
                float aspectX = (float)screenWidth / terrain.Width,
                      aspectY = (float)screenHeight / terrain.Height;
                zoomValue.Dest.X = aspectX;
                zoomValue.Dest.Y = aspectY;
                scrollPosition.Dest.X = 0;
                scrollPosition.Dest.Y = 0;
                scrollPosition.Speed = new Vector2(0.1f, 0.1f);
            }
        }

        // Retourne un chemin pour les fichiers de ressource
        public static string getResourceFile(string file)
        {
            return "res/" + file;
        }

        // Définit la partie visible de l'écran (permet d'éviter qu'un objet ne dépasse)
        public void setClipping(int x, int y, int width, int height)
        {
            Vector2 vs = new Vector2(x, y), ve = new Vector2(width, height);
            vs.TransformCoordinate(drawer.dev.Transform.Projection);
            ve.TransformCoordinate(drawer.dev.Transform.Projection);
            vs.X = vs.X * gc.Width / 2;
            vs.Y = -vs.Y * gc.Height / 2;
            ve.X = ve.X * gc.Width / 2;
            ve.Y = -ve.Y * gc.Height / 2;
            drawer.dev.ScissorRectangle = new System.Drawing.Rectangle((int)vs.X, (int)vs.Y, (int)ve.X, (int)ve.Y);
        }

        // Affiche ou masque la liste des utilisateurs
        public void playerPanelButtonPressed()
        {
            indicator.EnhancedView = !indicator.EnhancedView;
        }

        // Evénements du serveur
        public void connectSrvEvents(ConnectionManager cm)
        {
            cm.angle += CM_angle;
            cm.shoot += CM_shoot;
            cm.winner += CM_winner;
            cm.round += CM_round;
            cm.users += CM_users;
        }
        public void CM_users(object sender, Users e)
        {
            // Gère la déconnexion des utilisateurs
            bool[] usersActive = new bool[players.Length];
            for (int i = 0; i < usersActive.Length; i++)
                usersActive[i] = false;
            foreach (User u in e.list)
                usersActive[u.id] = true;
            for (int i = 0; i < usersActive.Length; i++)
                if (!usersActive[i])
                {
                    // Tue et colore en gris les joueurs déconnectés
                    players[i].Life = 0;
                    players[i].Color = Color.Gray;
                }
        }
        public void CM_winner(object sender, User e) {
            // Affiche l'écran du gagnant
            components.add(new CanonInterface.WinnerScreen(this, e.name));
            // Désactive la connexion réseau
            gc.GameThread.Connection = null;
        }
        // Stocke les données reçues pour plus tard (on ne processe rien
        // immédiatement car il faut être synchro avec l'animation)
        public void CM_shoot(object sender, Shoot e) { receivedShoot = e; }
        public void CM_angle(object sender, Angle e) { TurnPlayer.ShotAngle = e.deg; }
        public void CM_round(object sender, Round e) { receivedRound = e; }

        public void openGunPanel()
        {
            gunPanel.Open = true;
        }
        // Signale qu'un problème est survenu dans la partie (triche)
        public void signalCheat(string reason) {
            components.add(new CanonInterface.CheatScreen(this, reason));
        }
        // Gère une explosion d'un missile de rayon 'impact' au point X, Y
        public void handleExplosion(Missile m, float x, float y, float size)
        {
            // Regarde s'il y a qqn dans les parages
            foreach (Player p in players)
            {
                // Calcule la distance entre le centre du missile et le joueur
                float distance =
                    new Vector2(
                        (p.X + p.Width / 2) - x,
                        (p.Y + p.Height / 2) - y).Length(),
                    distanceMax = size + p.Height;      // Passé cette distance ils ne se touchent plus
                // Ils se touchent
                if (distance < distanceMax)
                    p.Life -= (int)(m.LifeLose * (distanceMax - distance) / distanceMax);
            }
        }

        // Classe gérant les paramètres d'une map chargés depuis un fichier XML
        private class Settings
        {
            public int width, height;
            public string bgFile, skyFile, bgMusic;
            public Vector2[] playerPos = new Vector2[6];
            public int players = 0;

            public Settings(string file)
            {
                XmlDocument xd = new XmlDocument();
                xd.Load(file);
                XmlNode map = xd.ChildNodes[0];
                if (map.Name != "map")
                    throw new ApplicationException("Invalid root");
                // Attributs de la map
                foreach (XmlAttribute a in map.Attributes)
                {
                    if (a.Name == "width")
                        width = int.Parse(a.Value);
                    else if (a.Name == "height")
                        height = int.Parse(a.Value);
                    else if (a.Name == "bgfile")
                        bgFile = a.Value;
                    else if (a.Name == "skyfile")
                        skyFile = a.Value;
                    else if (a.Name == "bgmusic")
                        bgMusic = a.Value;
                }

                // Joueurs
                foreach (XmlNode node in xd.ChildNodes[0])
                {
                    if (node.Name == "player")
                    {
                        foreach (XmlAttribute a in node.Attributes)
                        {
                            if (a.Name == "x")
                                playerPos[players].X = int.Parse(a.Value);
                            else if (a.Name == "y")
                                playerPos[players].Y = int.Parse(a.Value);
                        }
                        players++;
                    }
                }
            }
        }
    }

    abstract class ScalarPositionInterpolator<Type>
    {
        protected Type current;
        public Type Current {
            get { return current; }
            set { Dest = current = value; }
        }
        public Type Dest, Speed;
        public ScalarPositionInterpolator(Type initial, Type speed) {
            current = Dest = initial;
            Speed = speed;
        }
        public abstract void handle();
    }

    class FloatPositionInterpolator : ScalarPositionInterpolator<float>
    {
        public FloatPositionInterpolator(float initial, float speed) : base(initial, speed) { }
        public override void handle()
        {
            current += (Dest - current) * Speed;
        }
    }

    class VectorPositionInterpolator : ScalarPositionInterpolator<Vector2>
    {
        public VectorPositionInterpolator(float x, float y, Vector2 speed) : base(new Vector2(x, y), speed) { }
        public VectorPositionInterpolator(float x, float y, float speed) : base(new Vector2(x, y), new Vector2(speed, speed)) { }
        public override void handle()
        {
            current.X += (Dest.X - current.X) * Speed.X;
            current.Y += (Dest.Y - current.Y) * Speed.Y;
        }
        public float X { get { return current.X; } }
        public float Y { get { return current.Y; } }
    }

    interface PlayingObject
    {
        float X { get; }
        float Y { get; }
        int Width { get; }
        int Height { get; }
    }

    class Caisse : RectangularObject
    {
        private CanonGfx.Image img = null;
        public int ContainedGun;

        public Caisse(Game g, float x, float y, int armeContenue) : base(g.Terrain)
        {
            X = x;
            Y = y;
            Weight = 2;
            Width = Height = 64;
            ContainedGun = armeContenue;
            if (img == null)
                img = Game.Drawer.createImage("caisse.png", 64, 64);
        }

        public override void handle(bool usePhysics)
        {
            if (Y < 0)
                Y += 4;
            base.handle(usePhysics);
        }

        public override void draw()
        {
            img.setPosition(X, Y);
            img.Angle = Angle;
            Game.Drawer.drawImage(img);
        }
    }

    class Missile : PlayingObject
    {
        private static CanonGfx.Image img;
        private float _x, _y, vx, vy;
        private int impact;
        private int _width, _height;
        private Game game;
        public float X { get { return _x; } }
        public float Y { get { return _y; } }
        public int Width { get { return _width; } }
        public int Height { get { return _height; } }
        public Vector2 Speed { get { return new Vector2(vx, vy); } }
        private bool alive;
        // Indique que le missile a atteint sa cible
        public event SimpleEvent Exploded;
        private static int[] gunPower = new int[] { 10, 30, 80, 250 };
        // Points de vie que fait perdre cette arme
        private static int[] gunLifeLose = new int[] { 5, 15, 30, 50 };
        private int gunType;
        public int LifeLose { get { return gunLifeLose[gunType]; } }

        public Missile(Game g, float x, float y, float speed, float angle, int no_arme)
        {
            game = g;
            _x = x; _y = y;
            impact = gunPower[no_arme];
            vx = (float)Math.Cos(angle) * speed;
            vy = (float)Math.Sin(angle) * speed;
            _width = _height = 10;
            gunType = no_arme;
            alive = true;
            if (img == null) {
                // FIXME: peut dépendre du type de bullet
                img = Game.Drawer.createImage("bomb.png", 128, 80);
                img.BilinearFiltering = true;
            }
        }

        public void handle()
        {
            if (!alive)
                return;
            // Gravité
            vy += 0.1f;
            // Vent
            vx += game.WindForce * (float)Math.Cos(game.WindDirection) * 0.1f;
            vy -= game.WindForce * (float)Math.Sin(game.WindDirection) * 0.1f;
            // Avance pixel par pixel
            float moveX = Math.Abs(vx), moveY = Math.Abs(vy);
            int signX = Math.Sign(vx), signY = Math.Sign(vy);
            while (moveX != 0 || moveY != 0)
            {
                _x += Math.Min(1, moveX) * signX;
                _y += Math.Min(1, moveY) * signY;
                moveX -= Math.Min(1, moveX);
                moveY -= Math.Min(1, moveY);
                // Détection de collisions
                int ptImpactX = 0, ptImpactY = 0;
                for (ptImpactX = (int)_x; ptImpactX < (int)_x + (int)_width; ptImpactX++)
                    for (ptImpactY = (int)_y; ptImpactY < (int)_y + (int)_height; ptImpactY++)
                        if (game.Terrain.getPixel(ptImpactX, ptImpactY))
                        {
                            explode(ptImpactX, ptImpactY);
                            return;
                        }
                continue;
            }
        }

        // Quand le missile touche un mur
        public void explode(int ptImpactX, int ptImpactY)
        {
            // Gestion de l'impact
            alive = false;
            for (int j = -impact; j < impact; j++)
                for (int i = -impact; i < impact; i++)
                {
                    // Est à l'intérieur du cercle, pythagore
                    if (i * i + j * j < impact * impact)
                        game.Terrain.clearPixel(i + ptImpactX, j + ptImpactY);
                }
            game.Terrain.updateTexture();
            game.addExplosion(this, ptImpactX, ptImpactY, impact);
            Exploded(this);
        }
        // Explosion sans info supplémentaire, utilise le centre du missile
        public void explode()
        {
            explode((int)X + Width / 2, (int)Y + Height / 2);
        }

        public void draw()
        {
            img.Angle = (float)Math.Atan2(-vy, vx);
            img.setScale(impact, impact * img.Height / img.Width);
            img.setPosition(_x - img.ScaleWidth / 2, _y - img.ScaleHeight / 2);
            Game.Drawer.drawImage(img);
        }
    }

    interface GraphicalElement
    {
        bool Finished { get; }
        void handle();
        void draw();
    }

    // Indicateur de dommages
    class DamageIndicator : GraphicalElement
    {
        private int time = 0, value, alpha;
        private float X, Y;
        public bool Finished { get { return time >= 120; } }
        public DamageIndicator(float x, float y, int value)
        {
            X = x;
            Y = y + 10;
            alpha = 0;
            this.value = value;
        }
        public void handle()
        {
            if (time < 20)
            {
                Y -= 1.5f;
                alpha = Math.Min(255, alpha + 13);
            }
            if (time > 100)
            {
                Y -= 1.5f;
                alpha = Math.Max(0, alpha - 13);
            }
            time++;
        }
        public void draw()
        {
            Game.Drawer.drawText(Game.fontSmall, (int)X, (int)Y, value.ToString(), Color.FromArgb(alpha, 255, 0, 0));
        }
    }

    class Explosion : GraphicalElement
    {
        private static CanonGfx.Image img = null;
        private int time = 0;
        private float X, Y, size;
        public bool Finished { get { return time >= 4 * 16; } }

        public Explosion(float x, float y, float size)
        {
            X = x - size / 2;
            Y = y - size / 2;
            this.size = size;
            if (img == null)
                img = Game.Drawer.createImage("explosion.png", 256, 256);
        }

        public void handle()
        {
            time++;
        }

        public void draw()
        {
            if (Finished)       // Pas besoin de dessiner
                return;
            int noFrame = time / 4;
            float rx = noFrame % 4, ry = noFrame / 4;
            img.setPosition(X, Y);
            img.setSourceRect(new CanonGfx.Rectangle(rx / 4, ry / 4, 0.25f, 0.25f));
            img.setScale(size, size);
            Game.Drawer.drawImage(img);
        }
    }

    class Player : RectangularObject
    {
        protected static CanonGfx.Image img, imgCanon, imgPowerBar, imgWhiteFlag;
        private float _shotAngle = 0;
        public float ShotAngle
        {
            get { return _shotAngle + Angle; }
            set { _shotAngle = value; }
        }
        public Color Color { get { return color; } set { color = value; } }
        private Game game;
        private Missile missile;
        public Missile Missile { get { return missile; } }
        public event SimpleEvent TurnFinished;
        private Color color;
        public bool CanFire = false;
        // Force de tir courante
        private int firing = 0, firingFade = 0;
        public string Name;
        private int life = 100;
        public int Life
        {
            get { return life; }
            set { life = Math.Max(0, value); }
        }
        private DamageIndicator damage = null;
        private int lastLife;
        public int SelectedGun = 0;
        public int[] GunCount = new int[] { 1, 10, 4, 1 };
        public bool CanCloseGunPanel { get { return !(GunCount[SelectedGun] == 0 && CanFire); } }
        private int whiteFlagCounter = 0;
        private FloatPositionInterpolator whiteFlagPos = new FloatPositionInterpolator(0, 0.1f);

        public Player(Game g, Vector2 position, Terrain terrain, Color c) : base(terrain)
        {
            X = position.X - 30;
            Y = position.Y - 44;
            loadResources();
            Width = 60; Height = 48;
            game = g;
            color = c;
            lastLife = life;
        }

        // Si mouse != null, le tank a le focus (est le joueur)
        public void handle(CanonInterface.MouseState mouse, bool usePhysics)
        {
            base.handle(usePhysics);
            if (damage != null)
            {
                damage.handle();
                if (damage.Finished)
                    damage = null;
            }
            // On est mort?
            if (life == 0)
                if (++whiteFlagCounter == 100)
                    whiteFlagPos.Dest = -30;
                whiteFlagPos.handle();
            // Gestion du fade out de l'indicateur de tir une fois le tir effectué
            if (firingFade > 0)
                if (++firingFade == 100)
                    firing = firingFade = 0;
            // Pas assez de munitions -> ouvre le panneau
            if (!CanCloseGunPanel)
                game.openGunPanel();
            if (mouse != null && mouse.isInScreen() && firingFade == 0)
            {
                if ((mouse.isReleased(MouseButtons.Left) && firing > 0) || firing == 100)
                {
                    Vector2 pos = new Vector2(0, 0);
                    Matrix transform =
                        Matrix.Translation(30 - 3, 9, 0) *
                        Matrix.Translation(-Width / 2, -Height / 2, 0) *
                        Matrix.RotationZ(-Angle) *
                        Matrix.Translation(Width / 2, Height / 2, 0) *
                        Matrix.Translation(X, Y, 0);
                    pos.TransformCoordinate(transform);
                    // Temps de lancer le missile
                    Shoot sh = new Shoot(pos.X, pos.Y, SelectedGun, _shotAngle, 2.0f + firing * 8.0f / 100);
                    fire(sh);
                    // Commence le fade et interdit tout tir supplémentaire
                    firingFade = 1;
                    CanFire = false;
                    Game.sfxTir.Play();
                    // Envoie au serveur
                    if (game.Net)
                        game.Connection.send(sh);
                }
                // On interdit de tirer si la physique est active. Une fois stoppée on sait que c'est pareil sur tous les clients
                else if (mouse.isClicked(MouseButtons.Left) && CanFire && firing == 0 && !usePhysics && GunCount[SelectedGun] > 0)
                    firing = 1;
                else if (mouse.isDown(MouseButtons.Left) && CanFire && firing > 0)
                    firing++;
                else if (CanFire)
                {
                    Vector2 center = new Vector2(X + Width / 2, Y + Height / 2);
                    float angle = (float)Math.Atan2(mouse.AbsolutePosition.Y - center.Y, mouse.AbsolutePosition.X - center.X);
                    ShotAngle = angle;
                }
            }
            if (missile != null)
                missile.handle();
            // Si on a été touché, affiche le dommage
            if (life < lastLife)
                damage = new DamageIndicator(Width / 2 - 8, 0, lastLife - life);
            lastLife = life;
        }

        public void fire(Shoot sh)
        {
            // Arme à vide
            if (sh.w < 0)
            {
                // Ne fait rien, explose immédiatement
                missileExploded(this);
                return;
            }
            missile = new Missile(game, sh.x, sh.y, sh.strength, sh.angle, sh.w);
            // Comptabilise quand même le nombre de munitions des joueurs pour éviter la triche
            if (sh.w > 0)
                if (--GunCount[sh.w] < 0)
                    game.signalCheat("Player " + Name + " had none of this weapon " + sh.w);
            missile.Exploded += missileExploded;
            game.CameraFollows = missile;
        }

        // Ajoute une vitesse au tank, par exemple le souffle d'une explosion
        public void push(Vector2 speed)
        {
            Vx += speed.X;
            Vy += speed.Y;
        }

        // Indique que le missile a atteint sa cible -> fin du tour du joueur
        public void missileExploded(object sender)
        {
            missile = null;
            TurnFinished(this);
        }

        // Dessine l'objet
        public override void draw()
        {
            // Sauvegarde l'état pour annuler la rotation
            Gfx drawer = Game.Drawer;
            Matrix state = drawer.CurrentMatrix;
            img.setSourceRect(new CanonGfx.Rectangle(0, 0, Width / 64.0f, Height / 64.0f));
            drawer.moveScreen(X, Y);
            drawer.rotateScreen(-Angle, Width / 2, Height / 2);
            Matrix stateBeforeCanon = drawer.CurrentMatrix;
            // Dessine éventuellement le drapeau blanc (perdu)
            if (whiteFlagCounter > 100)
            {
                imgWhiteFlag.setPosition(0, whiteFlagPos.Current);
                Game.Drawer.drawImage(imgWhiteFlag);
            }
            // Dessine l'image du fort par-dessus
            img.setScale(Width, Height);
            img.Color = color;
            drawer.drawImage(img);
            drawer.moveScreen(30-3, 12-3);
            drawer.rotateScreen(ShotAngle, 3, 3);
            drawer.drawImage(imgCanon);
            // Barre de puissance
            drawer.CurrentMatrix = stateBeforeCanon;
            if (damage != null)
                damage.draw();
            if (firing > 0)
            {
                // Définit la transparence de la barre: pendant les 60 premières frames (1 seconde)
                // ne fait rien, pendant les 40 suivantes passe de 255 à 0.
                imgPowerBar.ColorARGB = (255 - Math.Max(0, firingFade - 60) * 255 / 40) << 24 | 0xffffff;
                drawer.moveScreen(0, -20);
                imgPowerBar.setSourceRect(new CanonGfx.Rectangle(0, 0, 1, 0.5f));
                imgPowerBar.setScale(60, 15);
                drawer.drawImage(imgPowerBar);
                imgPowerBar.setSourceRect(new CanonGfx.Rectangle(0, 0.475f, firing / 100.0f, 0.5f));
                imgPowerBar.setScale(60 * firing / 100.0f, 15);
                drawer.drawImage(imgPowerBar);
                drawer.moveScreen(0, +20);
            }
            drawer.CurrentMatrix = state;
            if (missile != null)
                missile.draw();
        }

        // Charge les ressources partagées
        private static void loadResources()
        {
            if (img == null)
            {
                img = Game.Drawer.createImage("fort.png");
                imgCanon = Game.Drawer.createImage("canon.png");
                imgPowerBar = Game.Drawer.createImage("powerbar.png");
                imgWhiteFlag = Game.Drawer.createImage("whiteflag.png");
            }
        }
    }

    class Terrain
    {
        private byte[,] pixels;
        private int width, height;
        private CanonGfx.Image img;
        public int Width { get { return width; } }
        public int Height { get { return height; } }

        // Taille en pixels de la map
        public Terrain(CanonGfx.Image img, int width, int height)
        {
            this.width = width;
            this.height = height;
            this.img = img;
            pixels = new byte[height, width];
            generate();
            updateTexture();
        }

        public bool getPixel(int x, int y) {
            if (x >= 0 && y >= 0 && x < width && y < height)
                return pixels[y, x] >= 64;
            else
                return true;
        }
        public bool getPixel(Vector2 p) { return getPixel((int)p.X, (int)p.Y); }
        public void clearPixel(int x, int y)
        {
            if (x >= 0 && y >= 0 && x < width && y < height)
                pixels[y, x] = 0;
        }

        public void generate()
        {
            int pitch;
            int width = img.Width, height = img.Height;
            GraphicsStream gs = img.Tex.LockRectangle(0, LockFlags.None, out pitch);
            unsafe
            {
                byte *pt = (byte*)gs.InternalData;
                for (int j = 0; j < height; j++)
                {
                    for (int i = 0; i < width; i++)
                    {
                        pt += 3;
                        pixels[j, i] = *(pt++);
                    }
                    // Prochaine ligne
                    pt += pitch - width * 4;
                }
            }
            img.Tex.UnlockRectangle(0);
        }

        public void updateTexture()
        {
            int pitch;
            int width = img.Width, height = img.Height;
            GraphicsStream gs = img.Tex.LockRectangle(0, LockFlags.None, out pitch);
            unsafe
            {
                byte* pt = (byte*)gs.InternalData;
                for (int j = 0; j < height; j++)
                {
                    for (int i = 0; i < width; i++)
                    {
                        pt += 3;
                        *(pt++) = pixels[j, i];
                    }
                    // Prochaine ligne
                    pt += pitch - width * 4;
                }
            }
            img.Tex.UnlockRectangle(0);
        }

        public void draw()
        {
            Game.Drawer.drawImage(img);
        }
    }

    class BackgroundImage
    {
        private CanonGfx.Image img;
        public BackgroundImage(CanonGfx.Image image)
        {
            img = image;
        }
        public void draw(Vector2 scroll, Vector2 size)
        {
            scroll.X %= img.Width;
            scroll.Y %= img.Height;
            img.setSourceRect(new CanonGfx.Rectangle(
                -scroll.X / img.Width,
                -scroll.Y / img.Height,
                size.X / img.Width,
                size.Y / img.Height));
            img.setPosition(0, 0);
            img.setScale(size.X, size.Y);
            img.Wrapping = true;
            Game.Drawer.drawImage(img);
        }
    }

    // Permet d'exécuter une tâche dans un nombre n de frames
    class Timer : CanonInterface.Component
    {
        public event SimpleEvent Tick;
        private int delay, counter = 0;
        public int Delay { get { return delay; } set { delay = value; } }
        public Timer(int delay) { this.delay = delay; }
        public bool running() { return counter != 0; }
        public void start() { counter = delay; }
        public override bool handle(CanonInterface.MouseState mouse)
        {
            if (--counter == 0)
                if (Tick != null)
                    Tick(this);
            return false;
        }
        // Un timer ça ne se dessine pas... si?
        public override void draw() { }
    }

}
