/// <reference path="../math/Vec2.ts" />
/// <reference path="../geom/Point.ts" />
/// <reference path="../math/Vec2Utils.ts" />
/// <reference path="shapes/Shape.ts" />
/// <reference path="shapes/Circle.ts" />
/// <reference path="shapes/Poly.ts" />
/// <reference path="shapes/Segment.ts" />
/// <reference path="Manager.ts" />
/// <reference path="Body.ts" />
/// <reference path="Contact.ts" />

/**
* Phaser - Advanced Physics - Collision Handlers
*
* Based on the work Ju Hyung Lee started in JS PhyRus.
*/

module Phaser.Physics {

    export class Collision {

        public collide(a, b, contacts: Contact[]) {

            //  Circle (a is the circle)
            if (a.type == Manager.SHAPE_TYPE_CIRCLE)
            {
                if (b.type == Manager.SHAPE_TYPE_CIRCLE)
                {
                    return this.circle2Circle(a, b, contacts);
                }
                else if (b.type == Manager.SHAPE_TYPE_SEGMENT)
                {
                    return this.circle2Segment(a, b, contacts);
                }
                else if (b.type == Manager.SHAPE_TYPE_POLY)
                {
                    return this.circle2Poly(a, b, contacts);
                }
            }

            //  Segment (a is the segment)
            if (a.type == Manager.SHAPE_TYPE_SEGMENT)
            {
                if (b.type == Manager.SHAPE_TYPE_CIRCLE)
                {
                    return this.circle2Segment(b, a, contacts);
                }
                else if (b.type == Manager.SHAPE_TYPE_SEGMENT)
                {
                    return this.segment2Segment(a, b, contacts);
                }
                else if (b.type == Manager.SHAPE_TYPE_POLY)
                {
                    return this.segment2Poly(a, b, contacts);
                }
            }

            //  Poly (a is the poly)
            if (a.type == Manager.SHAPE_TYPE_POLY)
            {
                if (b.type == Manager.SHAPE_TYPE_CIRCLE)
                {
                    return this.circle2Poly(b, a, contacts);
                }
                else if (b.type == Manager.SHAPE_TYPE_SEGMENT)
                {
                    return this.segment2Poly(b, a, contacts);
                }
                else if (b.type == Manager.SHAPE_TYPE_POLY)
                {
                    return this.poly2Poly(a, b, contacts);
                }
            }

        }

        private _circle2Circle(c1, r1, c2, r2, contactArr) {

            var rmax = r1 + r2;

            var t: Phaser.Vec2 = new Phaser.Vec2;
            //var t = vec2.sub(c2, c1);
            Phaser.Vec2Utils.subtract(c2, c1, t);

            var distsq = t.lengthSq();

            if (distsq > rmax * rmax)
            {
                return 0;
            }

            var dist = Math.sqrt(distsq);

            var p: Phaser.Vec2 = new Phaser.Vec2;
            Phaser.Vec2Utils.multiplyAdd(c1, t, 0.5 + (r1 - r2) * 0.5 / dist, p);
            //var p = vec2.mad(c1, t, 0.5 + (r1 - r2) * 0.5 / dist);

            var n: Phaser.Vec2 = new Phaser.Vec2;
            //var n = (dist != 0) ? vec2.scale(t, 1 / dist) : vec2.zero;

            if (dist != 0)
            {
                Phaser.Vec2Utils.scale(t, 1 / dist, n);
            }

            var d = dist - rmax;

            contactArr.push(new Contact(p, n, d, 0));

            return 1;

        }

        public circle2Circle(circ1: Phaser.Physics.Shapes.Circle, circ2: Phaser.Physics.Shapes.Circle, contactArr: Contact[]) {
            return this._circle2Circle(circ1.tc, circ1.radius, circ2.tc, circ2.radius, contactArr);
        }

        public circle2Segment(circ: Phaser.Physics.Shapes.Circle, seg: Phaser.Physics.Shapes.Segment, contactArr: Contact[]) {

            var rsum = circ.radius + seg.radius;

            // Normal distance from segment
            var dn = Phaser.Vec2Utils.dot(circ.tc, seg.tn) - Phaser.Vec2Utils.dot(seg.ta, seg.tn);
            var dist = (dn < 0 ? dn * -1 : dn) - rsum;
            if (dist > 0)
            {
                return 0;
            }

            // Tangential distance along segment
            var dt = Phaser.Vec2Utils.cross(circ.tc, seg.tn);
            var dtMin = Phaser.Vec2Utils.cross(seg.ta, seg.tn);
            var dtMax = Phaser.Vec2Utils.cross(seg.tb, seg.tn);

            if (dt < dtMin)
            {
                if (dt < dtMin - rsum)
                {
                    return 0;
                }

                return this._circle2Circle(circ.tc, circ.radius, seg.ta, seg.radius, contactArr);
            }
            else if (dt > dtMax)
            {
                if (dt > dtMax + rsum)
                {
                    return 0;
                }

                return this._circle2Circle(circ.tc, circ.radius, seg.tb, seg.radius, contactArr);
            }

            var n: Phaser.Vec2 = new Phaser.Vec2;

            if (dn > 0)
            {
                n.copyFrom(seg.tn);
            }
            else
            {
                Phaser.Vec2Utils.negative(seg.tn, n);
            }
            //var n = (dn > 0) ? seg.tn : vec2.neg(seg.tn);

            var c1: Phaser.Vec2 = new Phaser.Vec2;
            Phaser.Vec2Utils.multiplyAdd(circ.tc, n, -(circ.radius + dist * 0.5), c1);

            var c2: Phaser.Vec2 = new Phaser.Vec2;
            Phaser.Vec2Utils.negative(n, c2);

            contactArr.push(new Contact(c1, c2, dist, 0));
            //contactArr.push(new Contact(vec2.mad(circ.tc, n, -(circ.r + dist * 0.5)), vec2.neg(n), dist, 0));

            return 1;

        }

        public circle2Poly(circ: Phaser.Physics.Shapes.Circle, poly: Phaser.Physics.Shapes.Poly, contactArr: Contact[]) {

            var minDist = -999999;
            var minIdx = -1;

            for (var i = 0; i < poly.verts.length; i++)
            {
                var plane = poly.tplanes[i];
                var dist = Phaser.Vec2Utils.dot(circ.tc, plane.normal) - plane.d - circ.radius;

                if (dist > 0)
                {
                    return 0;
                }
                else if (dist > minDist)
                {
                    minDist = dist;
                    minIdx = i;
                }
            }

            var n = poly.tplanes[minIdx].normal;
            var a = poly.tverts[minIdx];
            var b = poly.tverts[(minIdx + 1) % poly.verts.length];
            var dta = Phaser.Vec2Utils.cross(a, n);
            var dtb = Phaser.Vec2Utils.cross(b, n);
            var dt = Phaser.Vec2Utils.cross(circ.tc, n);

            if (dt > dta)
            {
                return this._circle2Circle(circ.tc, circ.radius, a, 0, contactArr);
            }
            else if (dt < dtb)
            {
                return this._circle2Circle(circ.tc, circ.radius, b, 0, contactArr);
            }

            var c1: Phaser.Vec2 = new Phaser.Vec2;
            Phaser.Vec2Utils.multiplyAdd(circ.tc, n, -(circ.radius + minDist * 0.5), c1);

            var c2: Phaser.Vec2 = new Phaser.Vec2;
            Phaser.Vec2Utils.negative(n, c2);

            contactArr.push(new Contact(c1, c2, minDist, 0));

            //contactArr.push(new Contact(vec2.mad(circ.tc, n, -(circ.r + minDist * 0.5)), vec2.neg(n), minDist, 0));

            return 1;

        }

        public segmentPointDistanceSq(seg: Phaser.Physics.Shapes.Segment, p) {

            var w: Phaser.Vec2 = new Phaser.Vec2;
            var d: Phaser.Vec2 = new Phaser.Vec2;
            Phaser.Vec2Utils.subtract(p, seg.ta, w);
            Phaser.Vec2Utils.subtract(seg.tb, seg.ta, d);

            //var w = vec2.sub(p, seg.ta);
            //var d = vec2.sub(seg.tb, seg.ta);

            var proj = w.dot(d);

            if (proj <= 0)
            {
                return w.dot(w);
            }

            var vsq = d.dot(d);

            if (proj >= vsq)
            {
                return w.dot(w) - 2 * proj + vsq;
            }

            return w.dot(w) - proj * proj / vsq;

        }

        // FIXME and optimise me lots!!!
        public segment2Segment(seg1: Phaser.Physics.Shapes.Segment, seg2: Phaser.Physics.Shapes.Segment, contactArr: Contact[]) {

            var d = [];
            d[0] = this.segmentPointDistanceSq(seg1, seg2.ta);
            d[1] = this.segmentPointDistanceSq(seg1, seg2.tb);
            d[2] = this.segmentPointDistanceSq(seg2, seg1.ta);
            d[3] = this.segmentPointDistanceSq(seg2, seg1.tb);

            var idx1 = d[0] < d[1] ? 0 : 1;
            var idx2 = d[2] < d[3] ? 2 : 3;
            var idxm = d[idx1] < d[idx2] ? idx1 : idx2;
            var s, t;

            var u = Phaser.Vec2Utils.subtract(seg1.tb, seg1.ta);
            var v = Phaser.Vec2Utils.subtract(seg2.tb, seg2.ta);

            switch (idxm)
            {
                case 0:
                    s = Phaser.Vec2Utils.dot(Phaser.Vec2Utils.subtract(seg2.ta, seg1.ta), u) / Phaser.Vec2Utils.dot(u, u);
                    s = s < 0 ? 0 : (s > 1 ? 1 : s);
                    t = 0;
                    break;
                case 1:
                    s = Phaser.Vec2Utils.dot(Phaser.Vec2Utils.subtract(seg2.tb, seg1.ta), u) / Phaser.Vec2Utils.dot(u, u);
                    s = s < 0 ? 0 : (s > 1 ? 1 : s);
                    t = 1;
                    break;
                case 2:
                    s = 0;
                    t = Phaser.Vec2Utils.dot(Phaser.Vec2Utils.subtract(seg1.ta, seg2.ta), v) / Phaser.Vec2Utils.dot(v, v);
                    t = t < 0 ? 0 : (t > 1 ? 1 : t);
                    break;
                case 3:
                    s = 1;
                    t = Phaser.Vec2Utils.dot(Phaser.Vec2Utils.subtract(seg1.tb, seg2.ta), v) / Phaser.Vec2Utils.dot(v, v);
                    t = t < 0 ? 0 : (t > 1 ? 1 : t);
                    break;
            }

            var minp1 = Phaser.Vec2Utils.multiplyAdd(seg1.ta, u, s);
            var minp2 = Phaser.Vec2Utils.multiplyAdd(seg2.ta, v, t);

            return this._circle2Circle(minp1, seg1.radius, minp2, seg2.radius, contactArr);

        }

        // Identify vertexes that have penetrated the segment.
        public findPointsBehindSeg(contactArr: Contact[], seg: Phaser.Physics.Shapes.Segment, poly: Phaser.Physics.Shapes.Poly, dist: number, coef: number) {

            var dta = Phaser.Vec2Utils.cross(seg.tn, seg.ta);
            var dtb = Phaser.Vec2Utils.cross(seg.tn, seg.tb);

            var n: Phaser.Vec2 = new Phaser.Vec2;
            Phaser.Vec2Utils.scale(seg.tn, coef, n);
            //var n = vec2.scale(seg.tn, coef);

            for (var i = 0; i < poly.verts.length; i++)
            {
                var v = poly.tverts[i];

                if (Phaser.Vec2Utils.dot(v, n) < Phaser.Vec2Utils.dot(seg.tn, seg.ta) * coef + seg.radius)
                {
                    var dt = Phaser.Vec2Utils.cross(seg.tn, v);

                    if (dta >= dt && dt >= dtb)
                    {
                        contactArr.push(new Contact(v, n, dist, (poly.id << 16) | i));
                    }
                }
            }
        }

        public segment2Poly(seg: Phaser.Physics.Shapes.Segment, poly: Phaser.Physics.Shapes.Poly, contactArr: Contact[]) {

            var seg_td = Phaser.Vec2Utils.dot(seg.tn, seg.ta);
            var seg_d1 = poly.distanceOnPlane(seg.tn, seg_td) - seg.radius;

            if (seg_d1 > 0)
            {
                return 0;
            }

            var n: Phaser.Vec2 = new Phaser.Vec2;
            Phaser.Vec2Utils.negative(seg.tn, n);
            var seg_d2 = poly.distanceOnPlane(n, -seg_td) - seg.radius;
            //var seg_d2 = poly.distanceOnPlane(vec2.neg(seg.tn), -seg_td) - seg.r;

            if (seg_d2 > 0)
            {
                return 0;
            }

            var poly_d = -999999;
            var poly_i = -1;

            for (var i = 0; i < poly.verts.length; i++)
            {
                var plane = poly.tplanes[i];
                var dist = seg.distanceOnPlane(plane.normal, plane.d);

                if (dist > 0)
                {
                    return 0;
                }

                if (dist > poly_d)
                {
                    poly_d = dist;
                    poly_i = i;
                }
            }

            var poly_n: Phaser.Vec2 = new Phaser.Vec2;
            Phaser.Vec2Utils.negative(poly.tplanes[poly_i].normal, poly_n);
            //var poly_n = vec2.neg(poly.tplanes[poly_i].n);

            var va: Phaser.Vec2 = new Phaser.Vec2;
            Phaser.Vec2Utils.multiplyAdd(seg.ta, poly_n, seg.radius, va);
            //var va = vec2.mad(seg.ta, poly_n, seg.r);

            var vb: Phaser.Vec2 = new Phaser.Vec2;
            Phaser.Vec2Utils.multiplyAdd(seg.tb, poly_n, seg.radius, vb);
            //var vb = vec2.mad(seg.tb, poly_n, seg.r);

            if (poly.containPoint(va))
            {
                contactArr.push(new Contact(va, poly_n, poly_d, (seg.id << 16) | 0));
            }

            if (poly.containPoint(vb))
            {
                contactArr.push(new Contact(vb, poly_n, poly_d, (seg.id << 16) | 1));
            }

            // Floating point precision problems here.
            // This will have to do for now.
            poly_d -= 0.1

            if (seg_d1 >= poly_d || seg_d2 >= poly_d)
            {
                if (seg_d1 > seg_d2)
                {
                    this.findPointsBehindSeg(contactArr, seg, poly, seg_d1, 1);
                }
                else
                {
                    this.findPointsBehindSeg(contactArr, seg, poly, seg_d2, -1);
                }
            }

            // If no other collision points are found, try colliding endpoints.
            if (contactArr.length == 0)
            {
                var poly_a = poly.tverts[poly_i];
                var poly_b = poly.tverts[(poly_i + 1) % poly.verts.length];

                if (this._circle2Circle(seg.ta, seg.radius, poly_a, 0, contactArr))
                {
                    return 1;
                }

                if (this._circle2Circle(seg.tb, seg.radius, poly_a, 0, contactArr))
                {
                    return 1;
                }

                if (this._circle2Circle(seg.ta, seg.radius, poly_b, 0, contactArr))
                {
                    return 1;
                }

                if (this._circle2Circle(seg.tb, seg.radius, poly_b, 0, contactArr))
                {
                    return 1;
                }
            }

            return contactArr.length;

        }

        // Find the minimum separating axis for the given poly and plane list.
        public findMSA(poly: Phaser.Physics.Shapes.Poly, planes: Phaser.Physics.Plane[], num: number) {

            var min_dist: number = -999999;
            var min_index: number = -1;

            for (var i: number = 0; i < num; i++)
            {
                var dist: number = poly.distanceOnPlane(planes[i].normal, planes[i].d);

                if (dist > 0)
                {
                    // no collision
                    return { dist: 0, index: -1 };
                }
                else if (dist > min_dist)
                {
                    min_dist = dist;
                    min_index = i;
                }
            }

            //  new object - see what we can do here
            return { dist: min_dist, index: min_index };

        }

        public findVertsFallback(contactArr: Contact[], poly1: Phaser.Physics.Shapes.Poly, poly2: Phaser.Physics.Shapes.Poly, n, dist: number) {

            var num = 0;

            for (var i = 0; i < poly1.verts.length; i++)
            {
                var v = poly1.tverts[i];

                if (poly2.containPointPartial(v, n))
                {
                    contactArr.push(new Contact(v, n, dist, (poly1.id << 16) | i));
                    num++;
                }
            }

            for (var i = 0; i < poly2.verts.length; i++)
            {
                var v = poly2.tverts[i];

                if (poly1.containPointPartial(v, n))
                {
                    contactArr.push(new Contact(v, n, dist, (poly2.id << 16) | i));
                    num++;
                }
            }

            return num;

        }

        // Find the overlapped vertices.
        public findVerts(contactArr: Contact[], poly1: Phaser.Physics.Shapes.Poly, poly2: Phaser.Physics.Shapes.Poly, n, dist: number) {

            var num = 0;

            for (var i = 0; i < poly1.verts.length; i++)
            {
                var v = poly1.tverts[i];

                if (poly2.containPoint(v))
                {
                    contactArr.push(new Contact(v, n, dist, (poly1.id << 16) | i));
                    num++;
                }
            }

            for (var i = 0; i < poly2.verts.length; i++)
            {
                var v = poly2.tverts[i];

                if (poly1.containPoint(v))
                {
                    contactArr.push(new Contact(v, n, dist, (poly2.id << 16) | i));
                    num++;
                }
            }

            return num > 0 ? num : this.findVertsFallback(contactArr, poly1, poly2, n, dist);

        }

        public poly2Poly(poly1: Phaser.Physics.Shapes.Poly, poly2: Phaser.Physics.Shapes.Poly, contactArr: Contact[]) {

            var msa1 = this.findMSA(poly2, poly1.tplanes, poly1.verts.length);

            if (msa1.index == -1)
            {
                console.log('poly2poly 0', msa1);
                return 0;
            }

            var msa2 = this.findMSA(poly1, poly2.tplanes, poly2.verts.length);

            if (msa2.index == -1)
            {
                console.log('poly2poly 1', msa2);
                return 0;
            }

            // Penetration normal direction should be from poly1 to poly2
            if (msa1.dist > msa2.dist)
            {
                return this.findVerts(contactArr, poly1, poly2, poly1.tplanes[msa1.index].normal, msa1.dist);
            }

            return this.findVerts(contactArr, poly1, poly2, Phaser.Vec2Utils.negative(poly2.tplanes[msa2.index].normal), msa2.dist);

        }

    }

}