﻿using UnityEngine;

[AddComponentMenu("Dynamic Bone/Dynamic Bone Collider")]
public class DynamicBoneCollider : DynamicBoneColliderBase
{
#if UNITY_5_3_OR_NEWER
    [Tooltip("The radius of the sphere or capsule.")]
#endif	
    public float m_Radius = 0.5f;

#if UNITY_5_3_OR_NEWER
    [Tooltip("The height of the capsule.")]
#endif		
    public float m_Height = 0;

#if UNITY_5_3_OR_NEWER
    [Tooltip("The other radius of the capsule.")]
#endif	
    public float m_Radius2 = 0;

    // prepare data
    float m_ScaledRadius;
    float m_ScaledRadius2;
    Vector3 m_C0;
    Vector3 m_C1;
    float m_C01Distance;
    int m_CollideType;

    void OnValidate()
    {
        m_Radius = Mathf.Max(m_Radius, 0);
        m_Height = Mathf.Max(m_Height, 0);
        m_Radius2 = Mathf.Max(m_Radius2, 0);
    }

    public override void Prepare()
    {
        float scale = Mathf.Abs(transform.lossyScale.x);
        float halfHeight = m_Height * 0.5f;

        if (m_Radius2 <= 0 || Mathf.Abs(m_Radius - m_Radius2) < 0.01f)
        {
            m_ScaledRadius = m_Radius * scale;

            float h = halfHeight - m_Radius;
            if (h <= 0)
            {
                m_C0 = transform.TransformPoint(m_Center);

                if (m_Bound == Bound.Outside)
                {
                    m_CollideType = 0;
                }
                else
                {
                    m_CollideType = 1;
                }
            }
            else
            {
                Vector3 c0 = m_Center;
                Vector3 c1 = m_Center;

                switch (m_Direction)
                {
                    case Direction.X:
                        c0.x += h;
                        c1.x -= h;
                        break;
                    case Direction.Y:
                        c0.y += h;
                        c1.y -= h;
                        break;
                    case Direction.Z:
                        c0.z += h;
                        c1.z -= h;
                        break;
                }

                m_C0 = transform.TransformPoint(c0);
                m_C1 = transform.TransformPoint(c1);
                m_C01Distance = (m_C1 - m_C0).magnitude;

                if (m_Bound == Bound.Outside)
                {
                    m_CollideType = 2;
                }
                else
                {
                    m_CollideType = 3;
                }
            }
        }
        else
        {
            float r = Mathf.Max(m_Radius, m_Radius2);
            if (halfHeight - r <= 0)
            {
                m_ScaledRadius = r * scale;
                m_C0 = transform.TransformPoint(m_Center);

                if (m_Bound == Bound.Outside)
                {
                    m_CollideType = 0;
                }
                else
                {
                    m_CollideType = 1;
                }
            }
            else
            {
                m_ScaledRadius = m_Radius * scale;
                m_ScaledRadius2 = m_Radius2 * scale;

                float h0 = halfHeight - m_Radius;
                float h1 = halfHeight - m_Radius2;
                Vector3 c0 = m_Center;
                Vector3 c1 = m_Center;

                switch (m_Direction)
                {
                    case Direction.X:
                        c0.x += h0;
                        c1.x -= h1;
                        break;
                    case Direction.Y:
                        c0.y += h0;
                        c1.y -= h1;
                        break;
                    case Direction.Z:
                        c0.z += h0;
                        c1.z -= h1;
                        break;
                }

                m_C0 = transform.TransformPoint(c0);
                m_C1 = transform.TransformPoint(c1);
                m_C01Distance = (m_C1 - m_C0).magnitude;

                if (m_Bound == Bound.Outside)
                {
                    m_CollideType = 4;
                }
                else
                {
                    m_CollideType = 5;
                }
            }
        }
    }

    public override bool Collide(ref Vector3 particlePosition, float particleRadius)
    {
        switch (m_CollideType)
        {
            case 0:
                return OutsideSphere(ref particlePosition, particleRadius, m_C0, m_ScaledRadius);
            case 1:
                return InsideSphere(ref particlePosition, particleRadius, m_C0, m_ScaledRadius);
            case 2:
                return OutsideCapsule(ref particlePosition, particleRadius, m_C0, m_C1, m_ScaledRadius, m_C01Distance);
            case 3:
                return InsideCapsule(ref particlePosition, particleRadius, m_C0, m_C1, m_ScaledRadius, m_C01Distance);
            case 4:
                return OutsideCapsule2(ref particlePosition, particleRadius, m_C0, m_C1, m_ScaledRadius, m_ScaledRadius2, m_C01Distance);
            case 5:
                return InsideCapsule2(ref particlePosition, particleRadius, m_C0, m_C1, m_ScaledRadius, m_ScaledRadius2, m_C01Distance);
        }
        return false;
    }

    static bool OutsideSphere(ref Vector3 particlePosition, float particleRadius, Vector3 sphereCenter, float sphereRadius)
    {
        float r = sphereRadius + particleRadius;
        float r2 = r * r;
        Vector3 d = particlePosition - sphereCenter;
        float dlen2 = d.sqrMagnitude;

        // if is inside sphere, project onto sphere surface
        if (dlen2 > 0 && dlen2 < r2)
        {
            float dlen = Mathf.Sqrt(dlen2);
            particlePosition = sphereCenter + d * (r / dlen);
            return true;
        }
        return false;
    }

    static bool InsideSphere(ref Vector3 particlePosition, float particleRadius, Vector3 sphereCenter, float sphereRadius)
    {
        float r = sphereRadius - particleRadius;
        float r2 = r * r;
        Vector3 d = particlePosition - sphereCenter;
        float dlen2 = d.sqrMagnitude;

        // if is outside sphere, project onto sphere surface
        if (dlen2 > r2)
        {
            float dlen = Mathf.Sqrt(dlen2);
            particlePosition = sphereCenter + d * (r / dlen);
            return true;
        }
        return false;
    }

    static bool OutsideCapsule(ref Vector3 particlePosition, float particleRadius, Vector3 capsuleP0, Vector3 capsuleP1, float capsuleRadius, float dirlen)
    {
        float r = capsuleRadius + particleRadius;
        float r2 = r * r;
        Vector3 dir = capsuleP1 - capsuleP0;
        Vector3 d = particlePosition - capsuleP0;
        float t = Vector3.Dot(d, dir);

        if (t <= 0)
        {
            // check sphere1
            float dlen2 = d.sqrMagnitude;
            if (dlen2 > 0 && dlen2 < r2)
            {
                float dlen = Mathf.Sqrt(dlen2);
                particlePosition = capsuleP0 + d * (r / dlen);
                return true;
            }
        }
        else
        {
            float dirlen2 = dirlen * dirlen;
            if (t >= dirlen2)
            {
                // check sphere2
                d = particlePosition - capsuleP1;
                float dlen2 = d.sqrMagnitude;
                if (dlen2 > 0 && dlen2 < r2)
                {
                    float dlen = Mathf.Sqrt(dlen2);
                    particlePosition = capsuleP1 + d * (r / dlen);
                    return true;
                }
            }
            else
            {
                // check cylinder
                Vector3 q = d - dir * (t / dirlen2);
                float qlen2 = q.sqrMagnitude;
                if (qlen2 > 0 && qlen2 < r2)
                {
                    float qlen = Mathf.Sqrt(qlen2);
                    particlePosition += q * ((r - qlen) / qlen);
                    return true;
                }
            }
        }
        return false;
    }

    static bool InsideCapsule(ref Vector3 particlePosition, float particleRadius, Vector3 capsuleP0, Vector3 capsuleP1, float capsuleRadius, float dirlen)
    {
        float r = capsuleRadius - particleRadius;
        float r2 = r * r;
        Vector3 dir = capsuleP1 - capsuleP0;
        Vector3 d = particlePosition - capsuleP0;
        float t = Vector3.Dot(d, dir);

        if (t <= 0)
        {
            // check sphere1
            float dlen2 = d.sqrMagnitude;
            if (dlen2 > r2)
            {
                float dlen = Mathf.Sqrt(dlen2);
                particlePosition = capsuleP0 + d * (r / dlen);
                return true;
            }
        }
        else
        {
            float dirlen2 = dirlen * dirlen;
            if (t >= dirlen2)
            {
                // check sphere2
                d = particlePosition - capsuleP1;
                float dlen2 = d.sqrMagnitude;
                if (dlen2 > r2)
                {
                    float dlen = Mathf.Sqrt(dlen2);
                    particlePosition = capsuleP1 + d * (r / dlen);
                    return true;
                }
            }
            else
            {
                // check cylinder
                Vector3 q = d - dir * (t / dirlen2);
                float qlen2 = q.sqrMagnitude;
                if (qlen2 > r2)
                {
                    float qlen = Mathf.Sqrt(qlen2);
                    particlePosition += q * ((r - qlen) / qlen);
                    return true;
                }
            }
        }
        return false;
    }

    static bool OutsideCapsule2(ref Vector3 particlePosition, float particleRadius, Vector3 capsuleP0, Vector3 capsuleP1, float capsuleRadius0, float capsuleRadius1, float dirlen)
    {
        Vector3 dir = capsuleP1 - capsuleP0;
        Vector3 d = particlePosition - capsuleP0;
        float t = Vector3.Dot(d, dir);

        if (t <= 0)
        {
            // check sphere1
            float r = capsuleRadius0 + particleRadius;
            float r2 = r * r;
            float dlen2 = d.sqrMagnitude;
            if (dlen2 > 0 && dlen2 < r2)
            {
                float dlen = Mathf.Sqrt(dlen2);
                particlePosition = capsuleP0 + d * (r / dlen);
                return true;
            }
        }
        else
        {
            float dirlen2 = dirlen * dirlen;
            if (t >= dirlen2)
            {
                // check sphere2
                float r = capsuleRadius1 + particleRadius;
                float r2 = r * r;
                d = particlePosition - capsuleP1;
                float dlen2 = d.sqrMagnitude;
                if (dlen2 > 0 && dlen2 < r2)
                {
                    float dlen = Mathf.Sqrt(dlen2);
                    particlePosition = capsuleP1 + d * (r / dlen);
                    return true;
                }
            }
            else
            {
                // check cylinder
                Vector3 q = d - dir * (t / dirlen2);
                float qlen2 = q.sqrMagnitude;

                float klen = Vector3.Dot(d, dir / dirlen);
                float r = Mathf.Lerp(capsuleRadius0, capsuleRadius1, klen / dirlen) + particleRadius;
                float r2 = r * r;

                if (qlen2 > 0 && qlen2 < r2)
                {
                    float qlen = Mathf.Sqrt(qlen2);
                    particlePosition += q * ((r - qlen) / qlen);
                    return true;
                }
            }
        }
        return false;
    }

    static bool InsideCapsule2(ref Vector3 particlePosition, float particleRadius, Vector3 capsuleP0, Vector3 capsuleP1, float capsuleRadius0, float capsuleRadius1, float dirlen)
    {
        Vector3 dir = capsuleP1 - capsuleP0;
        Vector3 d = particlePosition - capsuleP0;
        float t = Vector3.Dot(d, dir);

        if (t <= 0)
        {
            // check sphere1
            float r = capsuleRadius0 - particleRadius;
            float r2 = r * r;
            float dlen2 = d.sqrMagnitude;
            if (dlen2 > r2)
            {
                float dlen = Mathf.Sqrt(dlen2);
                particlePosition = capsuleP0 + d * (r / dlen);
                return true;
            }
        }
        else
        {
            float dirlen2 = dirlen * dirlen;
            if (t >= dirlen2)
            {
                // check sphere2
                float r = capsuleRadius1 - particleRadius;
                float r2 = r * r;
                d = particlePosition - capsuleP1;
                float dlen2 = d.sqrMagnitude;
                if (dlen2 > r2)
                {
                    float dlen = Mathf.Sqrt(dlen2);
                    particlePosition = capsuleP1 + d * (r / dlen);
                    return true;
                }
            }
            else
            {
                // check cylinder
                Vector3 q = d - dir * (t / dirlen2);
                float qlen2 = q.sqrMagnitude;

                float klen = Vector3.Dot(d, dir / dirlen);
                float r = Mathf.Lerp(capsuleRadius0, capsuleRadius1, klen / dirlen) - particleRadius;
                float r2 = r * r;

                if (qlen2 > r2)
                {
                    float qlen = Mathf.Sqrt(qlen2);
                    particlePosition += q * ((r - qlen) / qlen);
                    return true;
                }
            }
        }
        return false;
    }

    void OnDrawGizmosSelected()
    {
        if (!enabled)
            return;

        Prepare();

        if (m_Bound == Bound.Outside)
        {
            Gizmos.color = Color.yellow;
        }
        else
        {
            Gizmos.color = Color.magenta;
        }

        switch (m_CollideType)
        {
            case 0:
            case 1:
                Gizmos.DrawWireSphere(m_C0, m_ScaledRadius);
                break;
            case 2:
            case 3:
                DrawCapsule(m_C0, m_C1, m_ScaledRadius, m_ScaledRadius);
                break;
            case 4:
            case 5:
                DrawCapsule(m_C0, m_C1, m_ScaledRadius, m_ScaledRadius2);
                break;
        }
    }

    static void DrawCapsule(Vector3 c0, Vector3 c1, float radius0, float radius1)
    {
        Gizmos.DrawLine(c0, c1);
        Gizmos.DrawWireSphere(c0, radius0);
        Gizmos.DrawWireSphere(c1, radius1);
    }
}
