using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.DirectX;

namespace Canon
{
    abstract class RectangularObject : PlayingObject
    {
        private float _x, _y;
        private int width, height;
        private float _angle;
        // Matrice de transformation (tenant compte de l'angle)
        protected Matrix mat;
        protected Terrain t;
        protected float Vx, Vy, Vangular, Weight = 1;

        public float X
        {
            get { return _x; }
            protected set { _x = value; calcMatrix(); }
        }
        public float Y
        {
            get { return _y; }
            protected set { _y = value; calcMatrix(); }
        }
        public int Width
        {
            get { return width; }
            protected set { width = value; }
        }
        public int Height
        {
            get { return height; }
            protected set { height = value; }
        }
        public float Angle
        {
            get { return _angle; }
            private set { _angle = value; calcMatrix(); }
        }

        private RectangularObject() { }
        public RectangularObject(Terrain terrain)
        {
            Angle = (float)(0 * Math.PI / 180);
            t = terrain;
            Vx = 0;
            Vy = 0;
            Vangular = 0;
        }

        // Gestion de l'objet
        public virtual void handle(bool usePhysics)
        {
            if (usePhysics)
            {
                move(Vx, Vy, Vangular);
                // Gravité
                Vy += 0.05f * Weight;
            }
        }

        public abstract void draw();

        // Retourne vrai si le point en question touche le char
        public bool inMe(CanonGfx.Rectangle point)
        {
            Matrix transform = Matrix.Invert(
                Matrix.Translation(-Width / 2, -Height / 2, 0) *
                Matrix.RotationZ(-Angle) *
                Matrix.Translation(Width / 2, Height / 2, 0) *
                Matrix.Translation(X, Y, 0));
            // Regarde si un des 4 points du rectangle est à l'intérieur de notre tank (en utilisant la transformation inverse à l'affichage)
            // Formellement c'est faux, mais comme ici le rectangle est toujours plus petit que le tank c'est bon
            Vector2[] points = new Vector2[4] {
                new Vector2(point.X, point.Y),
                new Vector2(point.X + point.Width, point.Y),
                new Vector2(point.X, point.Y + point.Height),
                new Vector2(point.X + point.Width, point.Y + point.Height)
            };
            foreach (Vector2 p in points)
            {
                p.TransformCoordinate(transform);
                if (p.X >= 0 && p.Y >= 0 && p.X <= Width && p.Y <= Height)
                    return true;
            }
            return false;
        }

        protected void move(float moveX, float moveY, float moveAngle)
        {
            const float delta = 0.5f, deltaAngle = 0.005f;
            Collision contact;
            Vector2 moveage = new Vector2(moveX, moveY);

            // TODO: optimize
            Vector2 dest = new Vector2(X + moveX, Y + moveY);
            X += moveX;
            Y += moveY;
            Angle += moveAngle;
            checkCollision(out contact);
            X -= moveX;
            Y -= moveY;
            Angle -= moveAngle;

            while (moveX != 0)
            {
                float xBak = X;
                X += moveX > 0 ? Math.Min(delta, moveX) : Math.Max(-delta, moveX);
                if (checkCollision())
                {
                    X = xBak;
                    Vx = -Vx * 0.5f;
                    break;
                }
                moveX = attenue(moveX, delta);
            }

            while (moveY != 0)
            {
                float yBak = Y;
                Y += moveY > 0 ? Math.Min(delta, moveY) : Math.Max(-delta, moveY);
                if (checkCollision())
                {
                    Y = yBak;
                    Vy = -Vy * 0.5f / Weight;
                    break;
                }
                moveY = attenue(moveY, delta);
            }

            while (moveAngle != 0)
            {
                float aBak = Angle;
                Matrix oldMat = mat;
                Collision c;
                Angle += moveAngle > 0 ? Math.Min(moveAngle, deltaAngle) : Math.Max(moveAngle, -deltaAngle);
                if (checkCollision(out c))
                {
                    // Tout ça nous permet de savoir de combien a bougé le point d'impact depuis la dernière fois
                    // Ainsi on peut prendre des mesures pour replacer le char correctement afin qu'il puisse
                    // tourner sans accroc
                    Vector2 colPoint = c.point();
                    Vector2 oldColPoint = relativePos(colPoint);
                    oldColPoint.TransformCoordinate(oldMat);
                    Vector2 moveDelta = oldColPoint - colPoint;
                    Angle = aBak;
                    move(moveDelta.X, moveDelta.Y, 0);
                    break;
                }
                moveAngle = attenue(moveAngle, deltaAngle);
            }

            if (contact.active())
                handleCollision(contact.point() + new Vector2(0.5f, 0), new Vector2(moveX, moveY));
        }

        // destPos: position où on se trouverait s'il n'y avait pas de collision
        private void handleCollision(Vector2 contactPoint, Vector2 impulse)
        {
            // Le coefficient Brönnimann (magique)
            float coeff = 0.0005f;
            //            float coeff = 0.001f;
            impulse.X = -impulse.X;
            // Lors d'un choc, diminue aussi l'énergie par frottement
            Vx = attenue(Vx, 0.1f);
            Vy = attenue(Vy, 0.1f);
            // Transforme l'impulse en un système cartésien normal (y vers le haut)
            Vector2 tmp = contactPoint - absolutePos(width / 2, height / 2);
            tmp.Y = -tmp.Y;

            float delta = cross(tmp, impulse) * coeff;
            // Affecte le choc
            Vangular *= 0.5f;
            if (Math.Abs(delta) >= 0.001f)
                Vangular += delta;
        }

        // Calcule la matrice de transformation points objet <-> univers
        private void calcMatrix()
        {
            // Après changement d'angle, recalcule la matrice
            mat = Matrix.Translation(-width / 2, -height / 2, 0)
                * Matrix.RotationZ(-Angle)
                * Matrix.Translation(width / 2, height / 2, 0)
                * Matrix.Translation(_x, _y, 0);
        }

        // Retourne la position absolue dans la map d'un point relatif par rapport au tank
        private Vector2 absolutePos(Vector2 v)
        {
            v.TransformCoordinate(mat);
            return v;
        }

        // L'inverse de absolutePos
        private Vector2 relativePos(Vector2 v)
        {
            v.TransformCoordinate(Matrix.Invert(mat));
            return v;
        }

        private Vector2 absolutePos(float x, float y) { return absolutePos(new Vector2(x, y)); }
        private Vector2 relativePos(float x, float y) { return relativePos(new Vector2(x, y)); }

        // Composante z du produit vectoriel
        private float cross(Vector2 v1, Vector2 v2)
        {
            return v1.X * v2.Y - v1.Y * v2.X;
        }

        // Donne la normale au sol. Elle est locale au point spécifié.
/*        private Vector2 getGroundNormal(Vector2 point, int diametre)
        {
            int rayonCercle = diametre / 2 - 1;
            int x = (int)point.X, y = (int)point.Y;
            int compteVecteurs = 0;
            Vector2 sommeVecteurs = new Vector2(0, 0);

            for (int j = -rayonCercle; j < rayonCercle; j++)
                for (int i = -rayonCercle; i < rayonCercle; i++)
                {
                    // Est à l'intérieur du cercle, pythagore
                    if (!t.getPixel(x + i, y + j) && i * i + j * j < rayonCercle * rayonCercle)
                    {
                        if (i != 0 || j != 0)
                        {
                            sommeVecteurs.X += i;
                            sommeVecteurs.Y -= j;
                            compteVecteurs++;
                        }
                    }
                }
            sommeVecteurs = new Vector2(sommeVecteurs.X / compteVecteurs, sommeVecteurs.Y / compteVecteurs);
            sommeVecteurs.Normalize();
            return sommeVecteurs;
        }*/

        // Atténue v de qte, et ne passe pas le zéro
        private float attenue(float v, float qte)
        {
            int sign = Math.Sign(v);
            if (v > 0)
                v -= qte;
            else
                v += qte;
            if (Math.Sign(v) != sign)
                return 0;
            else
                return v;
        }

        // Retourne l'emplacement où a eu lieu la collision
        private bool checkCollision(out Collision point)
        {
            point = new Collision();
            point.init();
            for (int i = 0; i < width; i++)
            {
                if (t.getPixel(absolutePos(i, 0)))
                {
                    point.count++;
                    point.totalVector += absolutePos(i, 0);
                }
                if (t.getPixel(absolutePos(i, height)))
                {
                    point.count++;
                    point.totalVector += absolutePos(i, height);
                }
            }
            for (int j = 0; j < height; j++)
            {
                if (t.getPixel(absolutePos(0, j)))
                {
                    point.count++;
                    point.totalVector += absolutePos(0, j);
                }
                if (t.getPixel(absolutePos(width, j)))
                {
                    point.count++;
                    point.totalVector += absolutePos(width, j);
                }
            }
            return point.count > 0;
        }

        // Retourne vrai si le tank touche le terrain dans sa position actuelle
        private bool checkCollision()
        {
            for (int i = 0; i < width; i++)
                if (t.getPixel(absolutePos(i, 0)) ||
                    t.getPixel(absolutePos(i, height)))
                    return true;
            for (int i = 0; i < height; i++)
                if (t.getPixel(absolutePos(0, i)) ||
                    t.getPixel(absolutePos(width, i)))
                    return true;
            return false;
        }

        // Utilitaire, pas besoin de protéger la visibilité
        private struct Collision
        {
            public Vector2 totalVector;
            public int count;
            public bool active() { return count > 0; }
            public void init()
            {
                totalVector = Vector2.Empty;
                count = 0;
            }
            public Vector2 point()
            {
                return totalVector * (1.0f / count);
            }
        }
    }
}
