// 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;

/// Defines a frustum constructed out of six [Plane]s.
class Frustum {
  final Plane _plane0;
  final Plane _plane1;
  final Plane _plane2;
  final Plane _plane3;
  final Plane _plane4;
  final Plane _plane5;

  /// The first plane that defines the bounds of this frustum.
  Plane get plane0 => _plane0;

  /// The second plane that defines the bounds of this frustum.
  Plane get plane1 => _plane1;

  /// The third plane that defines the bounds of this frustum.
  Plane get plane2 => _plane2;

  /// The fourth plane that defines the bounds of this frustum.
  Plane get plane3 => _plane3;

  /// The fifth plane that defines the bounds of this frustum.
  Plane get plane4 => _plane4;

  /// The sixed plane that defines the bounds of this frustum.
  Plane get plane5 => _plane5;

  /// Create a new frustum without initializing its bounds.
  Frustum()
      : _plane0 = Plane(),
        _plane1 = Plane(),
        _plane2 = Plane(),
        _plane3 = Plane(),
        _plane4 = Plane(),
        _plane5 = Plane();

  /// Create a new frustum as a copy of [other].
  factory Frustum.copy(Frustum other) => Frustum()..copyFrom(other);

  /// Create a new furstum from a [matrix].
  factory Frustum.matrix(Matrix4 matrix) => Frustum()..setFromMatrix(matrix);

  /// Copy the [other] frustum into this.
  void copyFrom(Frustum other) {
    _plane0.copyFrom(other._plane0);
    _plane1.copyFrom(other._plane1);
    _plane2.copyFrom(other._plane2);
    _plane3.copyFrom(other._plane3);
    _plane4.copyFrom(other._plane4);
    _plane5.copyFrom(other._plane5);
  }

  /// Set this from [matrix].
  void setFromMatrix(Matrix4 matrix) {
    final me = matrix.storage;
    final me0 = me[0], me1 = me[1], me2 = me[2], me3 = me[3];
    final me4 = me[4], me5 = me[5], me6 = me[6], me7 = me[7];
    final me8 = me[8], me9 = me[9], me10 = me[10], me11 = me[11];
    final me12 = me[12], me13 = me[13], me14 = me[14], me15 = me[15];

    _plane0
      ..setFromComponents(me3 - me0, me7 - me4, me11 - me8, me15 - me12)
      ..normalize();
    _plane1
      ..setFromComponents(me3 + me0, me7 + me4, me11 + me8, me15 + me12)
      ..normalize();
    _plane2
      ..setFromComponents(me3 + me1, me7 + me5, me11 + me9, me15 + me13)
      ..normalize();
    _plane3
      ..setFromComponents(me3 - me1, me7 - me5, me11 - me9, me15 - me13)
      ..normalize();
    _plane4
      ..setFromComponents(me3 - me2, me7 - me6, me11 - me10, me15 - me14)
      ..normalize();
    _plane5
      ..setFromComponents(me3 + me2, me7 + me6, me11 + me10, me15 + me14)
      ..normalize();
  }

  /// Check if this contains a [point].
  bool containsVector3(Vector3 point) {
    if (_plane0.distanceToVector3(point) < 0.0) {
      return false;
    }

    if (_plane1.distanceToVector3(point) < 0.0) {
      return false;
    }

    if (_plane2.distanceToVector3(point) < 0.0) {
      return false;
    }

    if (_plane3.distanceToVector3(point) < 0.0) {
      return false;
    }

    if (_plane4.distanceToVector3(point) < 0.0) {
      return false;
    }

    if (_plane5.distanceToVector3(point) < 0.0) {
      return false;
    }

    return true;
  }

  /// Check if this intersects with [aabb].
  bool intersectsWithAabb3(Aabb3 aabb) {
    if (_intersectsWithAabb3CheckPlane(aabb, _plane0)) {
      return false;
    }

    if (_intersectsWithAabb3CheckPlane(aabb, _plane1)) {
      return false;
    }

    if (_intersectsWithAabb3CheckPlane(aabb, _plane2)) {
      return false;
    }

    if (_intersectsWithAabb3CheckPlane(aabb, _plane3)) {
      return false;
    }

    if (_intersectsWithAabb3CheckPlane(aabb, _plane4)) {
      return false;
    }

    if (_intersectsWithAabb3CheckPlane(aabb, _plane5)) {
      return false;
    }

    return true;
  }

  /// Check if this intersects with [sphere].
  bool intersectsWithSphere(Sphere sphere) {
    final negativeRadius = -sphere.radius;
    final center = sphere.center;

    if (_plane0.distanceToVector3(center) < negativeRadius) {
      return false;
    }

    if (_plane1.distanceToVector3(center) < negativeRadius) {
      return false;
    }

    if (_plane2.distanceToVector3(center) < negativeRadius) {
      return false;
    }

    if (_plane3.distanceToVector3(center) < negativeRadius) {
      return false;
    }

    if (_plane4.distanceToVector3(center) < negativeRadius) {
      return false;
    }

    if (_plane5.distanceToVector3(center) < negativeRadius) {
      return false;
    }

    return true;
  }

  /// Calculate the corners of this and write them into [corner0] to
  // [corner7].
  void calculateCorners(
      Vector3 corner0,
      Vector3 corner1,
      Vector3 corner2,
      Vector3 corner3,
      Vector3 corner4,
      Vector3 corner5,
      Vector3 corner6,
      Vector3 corner7) {
    Plane.intersection(_plane0, _plane2, _plane4, corner0);
    Plane.intersection(_plane0, _plane3, _plane4, corner1);
    Plane.intersection(_plane0, _plane3, _plane5, corner2);
    Plane.intersection(_plane0, _plane2, _plane5, corner3);
    Plane.intersection(_plane1, _plane2, _plane4, corner4);
    Plane.intersection(_plane1, _plane3, _plane4, corner5);
    Plane.intersection(_plane1, _plane3, _plane5, corner6);
    Plane.intersection(_plane1, _plane2, _plane5, corner7);
  }

  bool _intersectsWithAabb3CheckPlane(Aabb3 aabb, Plane plane) {
    double outPx, outPy, outPz, outNx, outNy, outNz;

    if (plane._normal.x < 0.0) {
      outPx = aabb.min.x;
      outNx = aabb.max.x;
    } else {
      outPx = aabb.max.x;
      outNx = aabb.min.x;
    }

    if (plane._normal.y < 0.0) {
      outPy = aabb.min.y;
      outNy = aabb.max.y;
    } else {
      outPy = aabb.max.y;
      outNy = aabb.min.y;
    }

    if (plane._normal.z < 0.0) {
      outPz = aabb.min.z;
      outNz = aabb.max.z;
    } else {
      outPz = aabb.max.z;
      outNz = aabb.min.z;
    }

    final d1 = plane._normal.x * outPx +
        plane._normal.y * outPy +
        plane._normal.z * outPz +
        plane.constant;
    final d2 = plane._normal.x * outNx +
        plane._normal.y * outNy +
        plane._normal.z * outNz +
        plane.constant;

    return d1 < 0 && d2 < 0;
  }
}
