/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.collision;

import com.jme3.math.*;

/**
 * No longer public ..
 *
 * @author Kirill Vainer
 */
@Deprecated
class SweepSphere implements Collidable {

    private Vector3f velocity = new Vector3f();
    private Vector3f center = new Vector3f();
    private Vector3f dimension = new Vector3f();
    private Vector3f invDim = new Vector3f();

    private final Triangle scaledTri = new Triangle();
    private final Plane triPlane = new Plane();
    private final Vector3f temp1 = new Vector3f(),
                           temp2 = new Vector3f(),
                           temp3 = new Vector3f();
    private final Vector3f sVelocity = new Vector3f(),
                           sCenter = new Vector3f();

    public Vector3f getCenter() {
        return center;
    }

    public void setCenter(Vector3f center) {
        this.center.set(center);
    }

    public Vector3f getDimension() {
        return dimension;
    }

    public void setDimension(Vector3f dimension) {
        this.dimension.set(dimension);
        this.invDim.set(1,1,1).divideLocal(dimension);
    }

    public void setDimension(float x, float y, float z){
        this.dimension.set(x,y,z);
        this.invDim.set(1,1,1).divideLocal(dimension);
    }

    public void setDimension(float dim){
        this.dimension.set(dim, dim, dim);
        this.invDim.set(1,1,1).divideLocal(dimension);
    }

    public Vector3f getVelocity() {
        return velocity;
    }

    public void setVelocity(Vector3f velocity) {
        this.velocity.set(velocity);
    }

    private boolean pointsOnSameSide(Vector3f p1, Vector3f p2, Vector3f line1, Vector3f line2) {
        // V1 = (line2 - line1) x (p1    - line1)
        // V2 = (p2    - line1) x (line2 - line1)

        temp1.set(line2).subtractLocal(line1);
        temp3.set(temp1);
        temp2.set(p1).subtractLocal(line1);
        temp1.crossLocal(temp2);

        temp2.set(p2).subtractLocal(line1);
        temp3.crossLocal(temp2);

        // V1 . V2 >= 0
        return temp1.dot(temp3) >= 0;
    }

    private boolean isPointInTriangle(Vector3f point, AbstractTriangle tri) {
            if (pointsOnSameSide(point, tri.get1(), tri.get2(), tri.get3())
             && pointsOnSameSide(point, tri.get2(), tri.get1(), tri.get3())
             && pointsOnSameSide(point, tri.get3(), tri.get1(), tri.get2()))
                    return true;
            return false;
    }

    private static float getLowestRoot(float a, float b, float c, float maxR) {
        float determinant = b * b - 4f * a * c;
        if (determinant < 0){
            return Float.NaN;
        }

        float sqrtd = FastMath.sqrt(determinant);
        float r1 = (-b - sqrtd) / (2f * a);
        float r2 = (-b + sqrtd) / (2f * a);

        if (r1 > r2){
            float temp = r2;
            r2 = r1;
            r1 = temp;
        }

        if (r1 > 0 && r1 < maxR){
            return r1;
        }

        if (r2 > 0 && r2 < maxR){
            return r2;
        }

        return Float.NaN;
    }

    private float collideWithVertex(Vector3f sCenter, Vector3f sVelocity,
                                    float velocitySquared, Vector3f vertex, float t) {
        // A = velocity * velocity
        // B = 2 * (velocity . (center - vertex));
        // C = ||vertex - center||^2 - 1f;

        temp1.set(sCenter).subtractLocal(vertex);
        float a = velocitySquared;
        float b = 2f * sVelocity.dot(temp1);
        float c = temp1.negateLocal().lengthSquared() - 1f;
        float newT = getLowestRoot(a, b, c, t);

//        float A = velocitySquared;
//        float B = sCenter.subtract(vertex).dot(sVelocity) * 2f;
//        float C = vertex.subtract(sCenter).lengthSquared() - 1f;
//
//        float newT = getLowestRoot(A, B, C, Float.MAX_VALUE);
//        if (newT > 1.0f)
//            newT = Float.NaN;
        
        return newT;
    }

    private float collideWithSegment(Vector3f sCenter,
                                     Vector3f sVelocity,
                                     float velocitySquared,
                                     Vector3f l1,
                                     Vector3f l2,
                                     float t,
                                     Vector3f store) {
        Vector3f edge = temp1.set(l2).subtractLocal(l1);
        Vector3f base = temp2.set(l1).subtractLocal(sCenter);

        float edgeSquared = edge.lengthSquared();
        float baseSquared = base.lengthSquared();

        float EdotV = edge.dot(sVelocity);
        float EdotB = edge.dot(base);

        float a = (edgeSquared * -velocitySquared) + EdotV * EdotV;
        float b = (edgeSquared * 2f * sVelocity.dot(base))
                - (2f * EdotV * EdotB);
        float c = (edgeSquared * (1f - baseSquared)) + EdotB * EdotB;
        float newT = getLowestRoot(a, b, c, t);
        if (!Float.isNaN(newT)){
            float f = (EdotV * newT - EdotB) / edgeSquared;
            if (f >= 0f && f < 1f){
                store.scaleAdd(f, edge, l1);
                return newT;
            }
        }
        return Float.NaN;
    }

    private CollisionResult collideWithTriangle(AbstractTriangle tri){
        // scale scaledTriangle based on dimension
        scaledTri.get1().set(tri.get1()).multLocal(invDim);
        scaledTri.get2().set(tri.get2()).multLocal(invDim);
        scaledTri.get3().set(tri.get3()).multLocal(invDim);
//        Vector3f sVelocity = velocity.mult(invDim);
//        Vector3f sCenter = center.mult(invDim);
        velocity.mult(invDim, sVelocity);
        center.mult(invDim, sCenter);

        triPlane.setPlanePoints(scaledTri);

        float normalDotVelocity = triPlane.getNormal().dot(sVelocity);
        // XXX: sVelocity.normalize() needed?
        // back facing scaledTriangles not considered
        if (normalDotVelocity > 0f)
            return null;

        float t0, t1;
        boolean embedded = false;

        float signedDistanceToPlane = triPlane.pseudoDistance(sCenter);
        if (normalDotVelocity == 0.0f){
            // we are travelling exactly parrallel to the plane
            if (FastMath.abs(signedDistanceToPlane) >= 1.0f){
                // no collision possible
                return null;
            }else{
                // we are embedded
                t0 = 0;
                t1 = 1;
                embedded = true;
                System.out.println("EMBEDDED");
                return null;
            }
        }else{
            t0 = (-1f - signedDistanceToPlane) / normalDotVelocity;
            t1 = ( 1f - signedDistanceToPlane) / normalDotVelocity;

            if (t0 > t1){
                float tf = t1;
                t1 = t0;
                t0 = tf;
            }

            if (t0 > 1.0f || t1 < 0.0f){
                // collision is out of this sVelocity range
                return null;
            }

            // clamp the interval to [0, 1]
            t0 = Math.max(t0, 0.0f);
            t1 = Math.min(t1, 1.0f);
        }

        boolean foundCollision = false;
        float minT = 1f;

        Vector3f contactPoint = new Vector3f();
        Vector3f contactNormal = new Vector3f();
  
//        if (!embedded){
            // check against the inside of the scaledTriangle
            // contactPoint = sCenter - p.normal + t0 * sVelocity
            contactPoint.set(sVelocity);
            contactPoint.multLocal(t0);
            contactPoint.addLocal(sCenter);
            contactPoint.subtractLocal(triPlane.getNormal());

            // test to see if the collision is on a scaledTriangle interior
            if (isPointInTriangle(contactPoint, scaledTri) && !embedded){
                foundCollision = true;

                minT = t0;

                // scale collision point back into R3
                contactPoint.multLocal(dimension);
                contactNormal.set(velocity).multLocal(t0);
                contactNormal.addLocal(center);
                contactNormal.subtractLocal(contactPoint).normalizeLocal();

//                contactNormal.set(triPlane.getNormal());
                
                CollisionResult result = new CollisionResult();
                result.setContactPoint(contactPoint);
                result.setContactNormal(contactNormal);
                result.setDistance(minT * velocity.length());
                return result;
            }
//        }

        float velocitySquared = sVelocity.lengthSquared();

        Vector3f v1 = scaledTri.get1();
        Vector3f v2 = scaledTri.get2();
        Vector3f v3 = scaledTri.get3();

        // vertex 1
        float newT;
        newT = collideWithVertex(sCenter, sVelocity, velocitySquared, v1, minT);
        if (!Float.isNaN(newT)){
            minT = newT;
            contactPoint.set(v1);
            foundCollision = true;
        }

        // vertex 2
        newT = collideWithVertex(sCenter, sVelocity, velocitySquared, v2, minT);
        if (!Float.isNaN(newT)){
            minT = newT;
            contactPoint.set(v2);
            foundCollision = true;
        }

        // vertex 3
        newT = collideWithVertex(sCenter, sVelocity, velocitySquared, v3, minT);
        if (!Float.isNaN(newT)){
            minT = newT;
            contactPoint.set(v3);
            foundCollision = true;
        }

        // edge 1-2
        newT = collideWithSegment(sCenter, sVelocity, velocitySquared, v1, v2, minT, contactPoint);
        if (!Float.isNaN(newT)){
            minT = newT;
            foundCollision = true;
        }

        // edge 2-3
        newT = collideWithSegment(sCenter, sVelocity, velocitySquared, v2, v3, minT, contactPoint);
        if (!Float.isNaN(newT)){
            minT = newT;
            foundCollision = true;
        }

        // edge 3-1
        newT = collideWithSegment(sCenter, sVelocity, velocitySquared, v3, v1, minT, contactPoint);
        if (!Float.isNaN(newT)){
            minT = newT;
            foundCollision = true;
        }

        if (foundCollision){
            // compute contact normal based on minimum t
            contactPoint.multLocal(dimension);
            contactNormal.set(velocity).multLocal(t0);
            contactNormal.addLocal(center);
            contactNormal.subtractLocal(contactPoint).normalizeLocal();

            CollisionResult result = new CollisionResult();
            result.setContactPoint(contactPoint);
            result.setContactNormal(contactNormal);
            result.setDistance(minT * velocity.length());

            return result;
        }else{
            return null;
        }
    }

    public CollisionResult collideWithSweepSphere(SweepSphere other){
        temp1.set(velocity).subtractLocal(other.velocity);
        temp2.set(center).subtractLocal(other.center);
        temp3.set(dimension).addLocal(other.dimension);
        // delta V
        // delta C
        // Rsum

        float a = temp1.lengthSquared();
        float b = 2f * temp1.dot(temp2);
        float c = temp2.lengthSquared() - temp3.getX() * temp3.getX();
        float t = getLowestRoot(a, b, c, 1);

        // no collision
        if (Float.isNaN(t))
            return null;

        CollisionResult result = new CollisionResult();
        result.setDistance(velocity.length() * t);

        temp1.set(velocity).multLocal(t).addLocal(center);
        temp2.set(other.velocity).multLocal(t).addLocal(other.center);
        temp3.set(temp2).subtractLocal(temp1);
        // temp3 contains center to other.center vector

        // normalize it to get normal
        temp2.set(temp3).normalizeLocal();
        result.setContactNormal(new Vector3f(temp2));

        // temp3 is contact point
        temp3.set(temp2).multLocal(dimension).addLocal(temp1);
        result.setContactPoint(new Vector3f(temp3));
        
        return result;
    }

    public static void main(String[] args){
        SweepSphere ss = new SweepSphere();
        ss.setCenter(Vector3f.ZERO);
        ss.setDimension(1);
        ss.setVelocity(new Vector3f(10, 10, 10));

        SweepSphere ss2 = new SweepSphere();
        ss2.setCenter(new Vector3f(5, 5, 5));
        ss2.setDimension(1);
        ss2.setVelocity(new Vector3f(-10, -10, -10));

        CollisionResults cr = new CollisionResults();
        ss.collideWith(ss2, cr);
        if (cr.size() > 0){
            CollisionResult c = cr.getClosestCollision();
            System.out.println("D = "+c.getDistance());
            System.out.println("P = "+c.getContactPoint());
            System.out.println("N = "+c.getContactNormal());
        }
    }

    public int collideWith(Collidable other, CollisionResults results)
            throws UnsupportedCollisionException {
        if (other instanceof AbstractTriangle){
            AbstractTriangle tri = (AbstractTriangle) other;
            CollisionResult result = collideWithTriangle(tri);
            if (result != null){
                results.addCollision(result);
                return 1;
            }
            return 0;
        }else if (other instanceof SweepSphere){
            SweepSphere sph = (SweepSphere) other;

            CollisionResult result = collideWithSweepSphere(sph);
            if (result != null){
                results.addCollision(result);
                return 1;
            }
            return 0;
        }else{
            throw new UnsupportedCollisionException();
        }
    }

}
