// Copyright (c) 2015, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
part of '../../vector_math.dart';

/// Defines a 3-dimensional oriented bounding box defined with a [center],
/// [halfExtents] and axes.
class Obb3 {
  final Vector3 _center;
  final Vector3 _halfExtents;
  final Vector3 _axis0;
  final Vector3 _axis1;
  final Vector3 _axis2;

  /// The center of the OBB.
  Vector3 get center => _center;

  /// The half extends of the OBB.
  Vector3 get halfExtents => _halfExtents;

  /// The first axis of the OBB.
  Vector3 get axis0 => _axis0;

  /// The second axis of the OBB.
  Vector3 get axis1 => _axis1;

  /// The third axis of the OBB.
  Vector3 get axis2 => _axis2;

  /// Create a new OBB with erverything set to zero.
  Obb3()
      : _center = Vector3.zero(),
        _halfExtents = Vector3.zero(),
        _axis0 = Vector3(1.0, 0.0, 0.0),
        _axis1 = Vector3(0.0, 1.0, 0.0),
        _axis2 = Vector3(0.0, 0.0, 1.0);

  /// Create a new OBB as a copy of [other].
  Obb3.copy(Obb3 other)
      : _center = Vector3.copy(other._center),
        _halfExtents = Vector3.copy(other._halfExtents),
        _axis0 = Vector3.copy(other._axis0),
        _axis1 = Vector3.copy(other._axis1),
        _axis2 = Vector3.copy(other._axis2);

  /// Create a new OBB using [center], [halfExtents] and axis.
  Obb3.centerExtentsAxes(Vector3 center, Vector3 halfExtents, Vector3 axis0,
      Vector3 axis1, Vector3 axis2)
      : _center = Vector3.copy(center),
        _halfExtents = Vector3.copy(halfExtents),
        _axis0 = Vector3.copy(axis0),
        _axis1 = Vector3.copy(axis1),
        _axis2 = Vector3.copy(axis2);

  /// Copy from [other] into this.
  void copyFrom(Obb3 other) {
    _center.setFrom(other._center);
    _halfExtents.setFrom(other._halfExtents);
    _axis0.setFrom(other._axis0);
    _axis1.setFrom(other._axis1);
    _axis2.setFrom(other._axis2);
  }

  /// Copy from this into [other].
  void copyInto(Obb3 other) {
    other._center.setFrom(_center);
    other._halfExtents.setFrom(_halfExtents);
    other._axis0.setFrom(_axis0);
    other._axis1.setFrom(_axis1);
    other._axis2.setFrom(_axis2);
  }

  /// Reset the rotation of this.
  void resetRotation() {
    _axis0.setValues(1.0, 0.0, 0.0);
    _axis1.setValues(0.0, 1.0, 0.0);
    _axis2.setValues(0.0, 0.0, 1.0);
  }

  /// Translate this by [offset].
  void translate(Vector3 offset) {
    _center.add(offset);
  }

  /// Rotate this by the rotation matrix [t].
  void rotate(Matrix3 t) {
    t
      ..transform(_axis0..scale(_halfExtents.x))
      ..transform(_axis1..scale(_halfExtents.y))
      ..transform(_axis2..scale(_halfExtents.z));

    _halfExtents
      ..x = _axis0.normalize()
      ..y = _axis1.normalize()
      ..z = _axis2.normalize();
  }

  /// Transform this by the transform [t].
  void transform(Matrix4 t) {
    t
      ..transform3(_center)
      ..rotate3(_axis0..scale(_halfExtents.x))
      ..rotate3(_axis1..scale(_halfExtents.y))
      ..rotate3(_axis2..scale(_halfExtents.z));

    _halfExtents
      ..x = _axis0.normalize()
      ..y = _axis1.normalize()
      ..z = _axis2.normalize();
  }

  /// Store the corner with [cornerIndex] in [corner].
  void copyCorner(int cornerIndex, Vector3 corner) {
    assert(cornerIndex >= 0 || cornerIndex < 8);

    corner.setFrom(_center);

    switch (cornerIndex) {
      case 0:
        corner
          ..addScaled(_axis0, -_halfExtents.x)
          ..addScaled(_axis1, -_halfExtents.y)
          ..addScaled(_axis2, -_halfExtents.z);
        break;
      case 1:
        corner
          ..addScaled(_axis0, -_halfExtents.x)
          ..addScaled(_axis1, -_halfExtents.y)
          ..addScaled(_axis2, _halfExtents.z);
        break;
      case 2:
        corner
          ..addScaled(_axis0, -_halfExtents.x)
          ..addScaled(_axis1, _halfExtents.y)
          ..addScaled(_axis2, -_halfExtents.z);
        break;
      case 3:
        corner
          ..addScaled(_axis0, -_halfExtents.x)
          ..addScaled(_axis1, _halfExtents.y)
          ..addScaled(_axis2, _halfExtents.z);
        break;
      case 4:
        corner
          ..addScaled(_axis0, _halfExtents.x)
          ..addScaled(_axis1, -_halfExtents.y)
          ..addScaled(_axis2, -_halfExtents.z);
        break;
      case 5:
        corner
          ..addScaled(_axis0, _halfExtents.x)
          ..addScaled(_axis1, -_halfExtents.y)
          ..addScaled(_axis2, _halfExtents.z);
        break;
      case 6:
        corner
          ..addScaled(_axis0, _halfExtents.x)
          ..addScaled(_axis1, _halfExtents.y)
          ..addScaled(_axis2, -_halfExtents.z);
        break;
      case 7:
        corner
          ..addScaled(_axis0, _halfExtents.x)
          ..addScaled(_axis1, _halfExtents.y)
          ..addScaled(_axis2, _halfExtents.z);
        break;
    }
  }

  /// Find the closest point [q] on the OBB to the point [p] and store it in
  /// [q].
  void closestPointTo(Vector3 p, Vector3 q) {
    final d = p - _center;

    q.setFrom(_center);

    var dist = d.dot(_axis0);
    dist = dist.clamp(-_halfExtents.x, _halfExtents.x).toDouble();
    q.addScaled(_axis0, dist);

    dist = d.dot(_axis1);
    dist = dist.clamp(-_halfExtents.y, _halfExtents.y).toDouble();
    q.addScaled(_axis1, dist);

    dist = d.dot(_axis2);
    dist = dist.clamp(-_halfExtents.z, _halfExtents.z).toDouble();
    q.addScaled(_axis2, dist);
  }

  // Avoid allocating these instance on every call to intersectsWithObb3
  static final _r = Matrix3.zero();
  static final _absR = Matrix3.zero();
  static final _t = Vector3.zero();

  /// Check for intersection between this and [other].
  bool intersectsWithObb3(Obb3 other, [double epsilon = 1e-3]) {
    // Compute rotation matrix expressing other in this's coordinate frame
    _r
      ..setEntry(0, 0, _axis0.dot(other._axis0))
      ..setEntry(1, 0, _axis1.dot(other._axis0))
      ..setEntry(2, 0, _axis2.dot(other._axis0))
      ..setEntry(0, 1, _axis0.dot(other._axis1))
      ..setEntry(1, 1, _axis1.dot(other._axis1))
      ..setEntry(2, 1, _axis2.dot(other._axis1))
      ..setEntry(0, 2, _axis0.dot(other._axis2))
      ..setEntry(1, 2, _axis1.dot(other._axis2))
      ..setEntry(2, 2, _axis2.dot(other._axis2));

    // Compute translation vector t
    _t
      ..setFrom(other._center)
      ..sub(_center);

    // Bring translation into this's coordinate frame
    _t.setValues(_t.dot(_axis0), _t.dot(_axis1), _t.dot(_axis2));

    // Compute common subexpressions. Add in an epsilon term to
    // counteract arithmetic errors when two edges are parallel and
    // their cross product is (near) null.
    for (var i = 0; i < 3; i++) {
      for (var j = 0; j < 3; j++) {
        _absR.setEntry(i, j, _r.entry(i, j).abs() + epsilon);
      }
    }

    double ra;
    double rb;

    // Test axes L = A0, L = A1, L = A2
    for (var i = 0; i < 3; i++) {
      ra = _halfExtents[i];
      rb = other._halfExtents[0] * _absR.entry(i, 0) +
          other._halfExtents[1] * _absR.entry(i, 1) +
          other._halfExtents[2] * _absR.entry(i, 2);

      if (_t[i].abs() > ra + rb) {
        return false;
      }
    }

    // Test axes L = B0, L = B1, L = B2
    for (var i = 0; i < 3; i++) {
      ra = _halfExtents[0] * _absR.entry(0, i) +
          _halfExtents[1] * _absR.entry(1, i) +
          _halfExtents[2] * _absR.entry(2, i);
      rb = other._halfExtents[i];

      if ((_t[0] * _r.entry(0, i) +
                  _t[1] * _r.entry(1, i) +
                  _t[2] * _r.entry(2, i))
              .abs() >
          ra + rb) {
        return false;
      }
    }

    // Test axis L = A0 x B0
    ra = _halfExtents[1] * _absR.entry(2, 0) +
        _halfExtents[2] * _absR.entry(1, 0);
    rb = other._halfExtents[1] * _absR.entry(0, 2) +
        other._halfExtents[2] * _absR.entry(0, 1);
    if ((_t[2] * _r.entry(1, 0) - _t[1] * _r.entry(2, 0)).abs() > ra + rb) {
      return false;
    }

    // Test axis L = A0 x B1
    ra = _halfExtents[1] * _absR.entry(2, 1) +
        _halfExtents[2] * _absR.entry(1, 1);
    rb = other._halfExtents[0] * _absR.entry(0, 2) +
        other._halfExtents[2] * _absR.entry(0, 0);
    if ((_t[2] * _r.entry(1, 1) - _t[1] * _r.entry(2, 1)).abs() > ra + rb) {
      return false;
    }

    // Test axis L = A0 x B2
    ra = _halfExtents[1] * _absR.entry(2, 2) +
        _halfExtents[2] * _absR.entry(1, 2);
    rb = other._halfExtents[0] * _absR.entry(0, 1) +
        other._halfExtents[1] * _absR.entry(0, 0);
    if ((_t[2] * _r.entry(1, 2) - _t[1] * _r.entry(2, 2)).abs() > ra + rb) {
      return false;
    }

    // Test axis L = A1 x B0
    ra = _halfExtents[0] * _absR.entry(2, 0) +
        _halfExtents[2] * _absR.entry(0, 0);
    rb = other._halfExtents[1] * _absR.entry(1, 2) +
        other._halfExtents[2] * _absR.entry(1, 1);
    if ((_t[0] * _r.entry(2, 0) - _t[2] * _r.entry(0, 0)).abs() > ra + rb) {
      return false;
    }

    // Test axis L = A1 x B1
    ra = _halfExtents[0] * _absR.entry(2, 1) +
        _halfExtents[2] * _absR.entry(0, 1);
    rb = other._halfExtents[0] * _absR.entry(1, 2) +
        other._halfExtents[2] * _absR.entry(1, 0);
    if ((_t[0] * _r.entry(2, 1) - _t[2] * _r.entry(0, 1)).abs() > ra + rb) {
      return false;
    }

    // Test axis L = A1 x B2
    ra = _halfExtents[0] * _absR.entry(2, 2) +
        _halfExtents[2] * _absR.entry(0, 2);
    rb = other._halfExtents[0] * _absR.entry(1, 1) +
        other._halfExtents[1] * _absR.entry(1, 0);
    if ((_t[0] * _r.entry(2, 2) - _t[2] * _r.entry(0, 2)).abs() > ra + rb) {
      return false;
    }

    // Test axis L = A2 x B0
    ra = _halfExtents[0] * _absR.entry(1, 0) +
        _halfExtents[1] * _absR.entry(0, 0);
    rb = other._halfExtents[1] * _absR.entry(2, 2) +
        other._halfExtents[2] * _absR.entry(2, 1);
    if ((_t[1] * _r.entry(0, 0) - _t[0] * _r.entry(1, 0)).abs() > ra + rb) {
      return false;
    }

    // Test axis L = A2 x B1
    ra = _halfExtents[0] * _absR.entry(1, 1) +
        _halfExtents[1] * _absR.entry(0, 1);
    rb = other._halfExtents[0] * _absR.entry(2, 2) +
        other._halfExtents[2] * _absR.entry(2, 0);
    if ((_t[1] * _r.entry(0, 1) - _t[0] * _r.entry(1, 1)).abs() > ra + rb) {
      return false;
    }

    // Test axis L = A2 x B2
    ra = _halfExtents[0] * _absR.entry(1, 2) +
        _halfExtents[1] * _absR.entry(0, 2);
    rb = other._halfExtents[0] * _absR.entry(2, 1) +
        other._halfExtents[1] * _absR.entry(2, 0);
    if ((_t[1] * _r.entry(0, 2) - _t[0] * _r.entry(1, 2)).abs() > ra + rb) {
      return false;
    }

    // Since no separating axis is found, the OBBs must be intersecting
    return true;
  }

  // Avoid allocating these instance on every call to intersectsWithTriangle
  static final _triangle = Triangle();
  static final _aabb3 = Aabb3();
  static final _zeroVector = Vector3.zero();

  /// Return if this intersects with [other]
  bool intersectsWithTriangle(Triangle other, {IntersectionResult? result}) {
    _triangle.copyFrom(other);

    _triangle.point0
      ..sub(_center)
      ..setValues(_triangle.point0.dot(axis0), _triangle.point0.dot(axis1),
          _triangle.point0.dot(axis2));
    _triangle.point1
      ..sub(_center)
      ..setValues(_triangle.point1.dot(axis0), _triangle.point1.dot(axis1),
          _triangle.point1.dot(axis2));
    _triangle.point2
      ..sub(_center)
      ..setValues(_triangle.point2.dot(axis0), _triangle.point2.dot(axis1),
          _triangle.point2.dot(axis2));

    _aabb3.setCenterAndHalfExtents(_zeroVector, _halfExtents);

    return _aabb3.intersectsWithTriangle(_triangle, result: result);
  }

  // Avoid allocating these instance on every call to intersectsWithVector3
  static final _vector = Vector3.zero();

  /// Return if this intersects with [other]
  bool intersectsWithVector3(Vector3 other) {
    _vector
      ..setFrom(other)
      ..sub(_center)
      ..setValues(_vector.dot(axis0), _vector.dot(axis1), _vector.dot(axis2));

    _aabb3.setCenterAndHalfExtents(_zeroVector, _halfExtents);

    return _aabb3.intersectsWithVector3(_vector);
  }

  // Avoid allocating these instance on every call to intersectsWithTriangle
  static final _quadTriangle0 = Triangle();
  static final _quadTriangle1 = Triangle();

  /// Return if this intersects with [other]
  bool intersectsWithQuad(Quad other, {IntersectionResult? result}) {
    other.copyTriangles(_quadTriangle0, _quadTriangle1);

    return intersectsWithTriangle(_quadTriangle0, result: result) ||
        intersectsWithTriangle(_quadTriangle1, result: result);
  }
}
