import { Vector3 } from "three"
import { Ball } from "../ball"
import { Collision } from "./collision"
import { I, m, R } from "./constants"
import { exp } from "../../utils/utils"

const ab_v = new Vector3()
const abTangent_v = new Vector3()
const vPoint_v = new Vector3()
const vRelTangentialVec_v = new Vector3()
const impulseTangential_v = new Vector3()
const impulseNormal_v = new Vector3()
const angularImpulse_v = new Vector3()
const temp_v = new Vector3()

/**
 * Based on
 * https://billiards.colostate.edu/technical_proofs/new/TP_A-14.pdf
 *
 */
export class CollisionThrow {
  normalImpulse: number
  tangentialImpulse: number

  private dynamicFriction(vRel: number): number {
    return 0.01 + 0.108 * exp(-1.088 * vRel)
  }

  public updateVelocities(a: Ball, b: Ball) {
    const contact = Collision.positionsAtContact(a, b)
    a.ballmesh?.trace.forceTrace(contact.a)
    b.ballmesh?.trace.forceTrace(contact.b)
    const ab = ab_v.subVectors(contact.b, contact.a).normalize()
    const abTangent = abTangent_v.set(-ab.y, ab.x, 0)

    const e = 0.925
    const vPoint = vPoint_v
      .copy(a.vel)
      .sub(b.vel)
      .add(temp_v.copy(ab).multiplyScalar(-R).cross(a.rvel))
      .sub(temp_v.copy(ab).multiplyScalar(R).cross(b.rvel))

    const vRelNormalMag = ab.dot(vPoint)
    const vRelTangentialVec = vRelTangentialVec_v
      .copy(vPoint)
      .addScaledVector(ab, -vRelNormalMag)
    const vRelMag = vRelTangentialVec.length()

    const μ = this.dynamicFriction(vRelMag)

    // Normal impulse (inelastic collision)
    this.normalImpulse = (-(1 + e) * vRelNormalMag) / (2 / m)

    // Tangential impulse (frictional constraint)
    const impulseTangential = impulseTangential_v.set(0, 0, 0)
    if (vRelMag > 1e-8) {
      const maxJt_friction = μ * Math.abs(this.normalImpulse)
      const maxJt_stick = (m / 7) * vRelMag
      const jtMag = Math.min(maxJt_friction, maxJt_stick)
      impulseTangential.copy(vRelTangentialVec).multiplyScalar(-jtMag / vRelMag)
      this.tangentialImpulse = impulseTangential.dot(abTangent)
    } else {
      this.tangentialImpulse = 0
    }

    // Impulse vectors
    const impulseNormal = impulseNormal_v
      .copy(ab)
      .multiplyScalar(this.normalImpulse)

    // Apply impulses to linear velocities (constrained to XY plane)
    a.vel
      .addScaledVector(impulseNormal, 1 / m)
      .addScaledVector(impulseTangential, 1 / m)
    a.vel.z = 0
    b.vel
      .addScaledVector(impulseNormal, -1 / m)
      .addScaledVector(impulseTangential, -1 / m)
    b.vel.z = 0

    // Angular velocity updates
    // Jt is the tangential impulse applied TO ball A.
    // The force acts at the contact point relative to ball A: rA = R * ab
    // Torque for A: tauA = rA x Jt = (R * ab) x Jt
    // For ball B, the impulse is -Jt and it acts at rB = -R * ab
    // Torque for B: tauB = rB x (-Jt) = (-R * ab) x (-Jt) = (R * ab) x Jt
    // Thus both balls receive the SAME angular impulse.
    const angularImpulse = angularImpulse_v
      .copy(ab)
      .multiplyScalar(R)
      .cross(impulseTangential)

    a.rvel.addScaledVector(angularImpulse, 1 / I)
    b.rvel.addScaledVector(angularImpulse, 1 / I)

    return vRelNormalMag
  }
}
