//----------------------------------------------------------------------------//
//                                                                            //
// ozz-animation is hosted at http://github.com/guillaumeblanc/ozz-animation  //
// and distributed under the MIT License (MIT).                               //
//                                                                            //
// Copyright (c) Guillaume Blanc                                              //
//                                                                            //
// Permission is hereby granted, free of charge, to any person obtaining a    //
// copy of this software and associated documentation files (the "Software"), //
// to deal in the Software without restriction, including without limitation  //
// the rights to use, copy, modify, merge, publish, distribute, sublicense,   //
// and/or sell copies of the Software, and to permit persons to whom the      //
// Software is furnished to do so, subject to the following conditions:       //
//                                                                            //
// The above copyright notice and this permission notice shall be included in //
// all copies or substantial portions of the Software.                        //
//                                                                            //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR //
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,   //
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL    //
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER //
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING    //
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER        //
// DEALINGS IN THE SOFTWARE.                                                  //
//                                                                            //
//----------------------------------------------------------------------------//

#ifndef OZZ_OZZ_BASE_MATHS_SOA_FLOAT4X4_H_
#define OZZ_OZZ_BASE_MATHS_SOA_FLOAT4X4_H_

#include <cassert>

#include "ozz/base/maths/soa_float.h"
#include "ozz/base/maths/soa_quaternion.h"
#include "ozz/base/platform.h"

namespace ozz {
namespace math {

// Declare the 4x4 soa matrix type. Uses the column major convention where the
// matrix-times-vector is written v'=Mv:
// [ m.cols[0].x m.cols[1].x m.cols[2].x m.cols[3].x ]   {v.x}
// | m.cols[0].y m.cols[1].y m.cols[2].y m.cols[3].y | * {v.y}
// | m.cols[0].z m.cols[1].y m.cols[2].y m.cols[3].y |   {v.z}
// [ m.cols[0].w m.cols[1].w m.cols[2].w m.cols[3].w ]   {v.1}
struct SoaFloat4x4 {
  // Soa matrix columns.
  SoaFloat4 cols[4];

  // Returns the identity matrix.
  static OZZ_INLINE SoaFloat4x4 identity() {
    const SimdFloat4 zero = simd_float4::zero();
    const SimdFloat4 one = simd_float4::one();
    SoaFloat4x4 ret = {{{one, zero, zero, zero},
                        {zero, one, zero, zero},
                        {zero, zero, one, zero},
                        {zero, zero, zero, one}}};
    return ret;
  }

  // Returns a scaling matrix that scales along _v.
  // _v.w is ignored.
  static OZZ_INLINE SoaFloat4x4 Scaling(const SoaFloat4& _v) {
    const SimdFloat4 zero = simd_float4::zero();
    const SimdFloat4 one = simd_float4::one();
    const SoaFloat4x4 ret = {{{_v.x, zero, zero, zero},
                              {zero, _v.y, zero, zero},
                              {zero, zero, _v.z, zero},
                              {zero, zero, zero, one}}};
    return ret;
  }

  // Returns the rotation matrix built from quaternion defined by x, y, z and w
  // components of _v.
  static OZZ_INLINE SoaFloat4x4 FromQuaternion(const SoaQuaternion& _q) {
    assert(AreAllTrue(IsNormalizedEst(_q)));

    const SimdFloat4 zero = simd_float4::zero();
    const SimdFloat4 one = simd_float4::one();
    const SimdFloat4 two = one + one;

    const SimdFloat4 xx = _q.x * _q.x;
    const SimdFloat4 xy = _q.x * _q.y;
    const SimdFloat4 xz = _q.x * _q.z;
    const SimdFloat4 xw = _q.x * _q.w;
    const SimdFloat4 yy = _q.y * _q.y;
    const SimdFloat4 yz = _q.y * _q.z;
    const SimdFloat4 yw = _q.y * _q.w;
    const SimdFloat4 zz = _q.z * _q.z;
    const SimdFloat4 zw = _q.z * _q.w;

    const SoaFloat4x4 ret = {
        {{one - two * (yy + zz), two * (xy + zw), two * (xz - yw), zero},
         {two * (xy - zw), one - two * (xx + zz), two * (yz + xw), zero},
         {two * (xz + yw), two * (yz - xw), one - two * (xx + yy), zero},
         {zero, zero, zero, one}}};
    return ret;
  }

  // Returns the affine transformation matrix built from split translation,
  // rotation (quaternion) and scale.
  static OZZ_INLINE SoaFloat4x4 FromAffine(const SoaFloat3& _translation,
                                           const SoaQuaternion& _quaternion,
                                           const SoaFloat3& _scale) {
    assert(AreAllTrue(IsNormalizedEst(_quaternion)));

    const SimdFloat4 zero = simd_float4::zero();
    const SimdFloat4 one = simd_float4::one();
    const SimdFloat4 two = one + one;

    const SimdFloat4 xx = _quaternion.x * _quaternion.x;
    const SimdFloat4 xy = _quaternion.x * _quaternion.y;
    const SimdFloat4 xz = _quaternion.x * _quaternion.z;
    const SimdFloat4 xw = _quaternion.x * _quaternion.w;
    const SimdFloat4 yy = _quaternion.y * _quaternion.y;
    const SimdFloat4 yz = _quaternion.y * _quaternion.z;
    const SimdFloat4 yw = _quaternion.y * _quaternion.w;
    const SimdFloat4 zz = _quaternion.z * _quaternion.z;
    const SimdFloat4 zw = _quaternion.z * _quaternion.w;

    const SoaFloat4x4 ret = {
        {{_scale.x * (one - two * (yy + zz)), _scale.x * two * (xy + zw),
          _scale.x * two * (xz - yw), zero},
         {_scale.y * two * (xy - zw), _scale.y * (one - two * (xx + zz)),
          _scale.y * two * (yz + xw), zero},
         {_scale.z * two * (xz + yw), _scale.z * two * (yz - xw),
          _scale.z * (one - two * (xx + yy)), zero},
         {_translation.x, _translation.y, _translation.z, one}}};
    return ret;
  }
};

// Returns the transpose of matrix _m.
OZZ_INLINE SoaFloat4x4 Transpose(const SoaFloat4x4& _m) {
  const SoaFloat4x4 ret = {
      {{_m.cols[0].x, _m.cols[1].x, _m.cols[2].x, _m.cols[3].x},
       {_m.cols[0].y, _m.cols[1].y, _m.cols[2].y, _m.cols[3].y},
       {_m.cols[0].z, _m.cols[1].z, _m.cols[2].z, _m.cols[3].z},
       {_m.cols[0].w, _m.cols[1].w, _m.cols[2].w, _m.cols[3].w}}};
  return ret;
}

// Returns the inverse of matrix _m.
// If _invertible is not nullptr, each component will be set to true if its
// respective matrix is invertible. If _invertible is nullptr, then an assert is
// triggered in case any of the 4 matrices isn't invertible.
OZZ_INLINE SoaFloat4x4 Invert(const SoaFloat4x4& _m,
                              SimdInt4* _invertible = nullptr) {
  const SoaFloat4* cols = _m.cols;
  const SimdFloat4 a00 = cols[2].z * cols[3].w - cols[3].z * cols[2].w;
  const SimdFloat4 a01 = cols[2].y * cols[3].w - cols[3].y * cols[2].w;
  const SimdFloat4 a02 = cols[2].y * cols[3].z - cols[3].y * cols[2].z;
  const SimdFloat4 a03 = cols[2].x * cols[3].w - cols[3].x * cols[2].w;
  const SimdFloat4 a04 = cols[2].x * cols[3].z - cols[3].x * cols[2].z;
  const SimdFloat4 a05 = cols[2].x * cols[3].y - cols[3].x * cols[2].y;
  const SimdFloat4 a06 = cols[1].z * cols[3].w - cols[3].z * cols[1].w;
  const SimdFloat4 a07 = cols[1].y * cols[3].w - cols[3].y * cols[1].w;
  const SimdFloat4 a08 = cols[1].y * cols[3].z - cols[3].y * cols[1].z;
  const SimdFloat4 a09 = cols[1].x * cols[3].w - cols[3].x * cols[1].w;
  const SimdFloat4 a10 = cols[1].x * cols[3].z - cols[3].x * cols[1].z;
  const SimdFloat4 a11 = cols[1].y * cols[3].w - cols[3].y * cols[1].w;
  const SimdFloat4 a12 = cols[1].x * cols[3].y - cols[3].x * cols[1].y;
  const SimdFloat4 a13 = cols[1].z * cols[2].w - cols[2].z * cols[1].w;
  const SimdFloat4 a14 = cols[1].y * cols[2].w - cols[2].y * cols[1].w;
  const SimdFloat4 a15 = cols[1].y * cols[2].z - cols[2].y * cols[1].z;
  const SimdFloat4 a16 = cols[1].x * cols[2].w - cols[2].x * cols[1].w;
  const SimdFloat4 a17 = cols[1].x * cols[2].z - cols[2].x * cols[1].z;
  const SimdFloat4 a18 = cols[1].x * cols[2].y - cols[2].x * cols[1].y;

  const SimdFloat4 b0x = cols[1].y * a00 - cols[1].z * a01 + cols[1].w * a02;
  const SimdFloat4 b1x = -cols[1].x * a00 + cols[1].z * a03 - cols[1].w * a04;
  const SimdFloat4 b2x = cols[1].x * a01 - cols[1].y * a03 + cols[1].w * a05;
  const SimdFloat4 b3x = -cols[1].x * a02 + cols[1].y * a04 - cols[1].z * a05;

  const SimdFloat4 b0y = -cols[0].y * a00 + cols[0].z * a01 - cols[0].w * a02;
  const SimdFloat4 b1y = cols[0].x * a00 - cols[0].z * a03 + cols[0].w * a04;
  const SimdFloat4 b2y = -cols[0].x * a01 + cols[0].y * a03 - cols[0].w * a05;
  const SimdFloat4 b3y = cols[0].x * a02 - cols[0].y * a04 + cols[0].z * a05;

  const SimdFloat4 b0z = cols[0].y * a06 - cols[0].z * a07 + cols[0].w * a08;
  const SimdFloat4 b1z = -cols[0].x * a06 + cols[0].z * a09 - cols[0].w * a10;
  const SimdFloat4 b2z = cols[0].x * a11 - cols[0].y * a09 + cols[0].w * a12;
  const SimdFloat4 b3z = -cols[0].x * a08 + cols[0].y * a10 - cols[0].z * a12;

  const SimdFloat4 b0w = -cols[0].y * a13 + cols[0].z * a14 - cols[0].w * a15;
  const SimdFloat4 b1w = cols[0].x * a13 - cols[0].z * a16 + cols[0].w * a17;
  const SimdFloat4 b2w = -cols[0].x * a14 + cols[0].y * a16 - cols[0].w * a18;
  const SimdFloat4 b3w = cols[0].x * a15 - cols[0].y * a17 + cols[0].z * a18;

  const SimdFloat4 det =
      cols[0].x * b0x + cols[0].y * b1x + cols[0].z * b2x + cols[0].w * b3x;
  const SimdInt4 invertible = CmpNe(det, simd_float4::zero());
  assert((_invertible || AreAllTrue(invertible)) && "Matrix is not invertible");
  if (_invertible != nullptr) {
    *_invertible = invertible;
  }
  const SimdFloat4 inv_det =
      Select(invertible, RcpEstNR(det), simd_float4::zero());

  const SoaFloat4x4 ret = {
      {{b0x * inv_det, b0y * inv_det, b0z * inv_det, b0w * inv_det},
       {b1x * inv_det, b1y * inv_det, b1z * inv_det, b1w * inv_det},
       {b2x * inv_det, b2y * inv_det, b2z * inv_det, b2w * inv_det},
       {b3x * inv_det, b3y * inv_det, b3z * inv_det, b3w * inv_det}}};

  return ret;
}

// Scales matrix _m along the axis defined by _v components.
// _v.w is ignored.
OZZ_INLINE SoaFloat4x4 Scale(const SoaFloat4x4& _m, const SoaFloat4& _v) {
  const SoaFloat4x4 ret = {{{_m.cols[0].x * _v.x, _m.cols[0].y * _v.x,
                             _m.cols[0].z * _v.x, _m.cols[0].w * _v.x},
                            {_m.cols[1].x * _v.y, _m.cols[1].y * _v.y,
                             _m.cols[1].z * _v.y, _m.cols[1].w * _v.y},
                            {_m.cols[2].x * _v.z, _m.cols[2].y * _v.z,
                             _m.cols[2].z * _v.z, _m.cols[2].w * _v.z},
                            _m.cols[3]}};
  return ret;
}
}  // namespace math
}  // namespace ozz

// Computes the multiplication of matrix Float4x4 and vector  _v.
OZZ_INLINE ozz::math::SoaFloat4 operator*(const ozz::math::SoaFloat4x4& _m,
                                          const ozz::math::SoaFloat4& _v) {
  const ozz::math::SoaFloat4 ret = {
      _m.cols[0].x * _v.x + _m.cols[1].x * _v.y + _m.cols[2].x * _v.z +
          _m.cols[3].x * _v.w,
      _m.cols[0].y * _v.x + _m.cols[1].y * _v.y + _m.cols[2].y * _v.z +
          _m.cols[3].y * _v.w,
      _m.cols[0].z * _v.x + _m.cols[1].z * _v.y + _m.cols[2].z * _v.z +
          _m.cols[3].z * _v.w,
      _m.cols[0].w * _v.x + _m.cols[1].w * _v.y + _m.cols[2].w * _v.z +
          _m.cols[3].w * _v.w};
  return ret;
}

// Computes the multiplication of two matrices _a and _b.
OZZ_INLINE ozz::math::SoaFloat4x4 operator*(const ozz::math::SoaFloat4x4& _a,
                                            const ozz::math::SoaFloat4x4& _b) {
  const ozz::math::SoaFloat4x4 ret = {
      {_a * _b.cols[0], _a * _b.cols[1], _a * _b.cols[2], _a * _b.cols[3]}};
  return ret;
}

// Computes the per element addition of two matrices _a and _b.
OZZ_INLINE ozz::math::SoaFloat4x4 operator+(const ozz::math::SoaFloat4x4& _a,
                                            const ozz::math::SoaFloat4x4& _b) {
  const ozz::math::SoaFloat4x4 ret = {
      {{_a.cols[0].x + _b.cols[0].x, _a.cols[0].y + _b.cols[0].y,
        _a.cols[0].z + _b.cols[0].z, _a.cols[0].w + _b.cols[0].w},
       {_a.cols[1].x + _b.cols[1].x, _a.cols[1].y + _b.cols[1].y,
        _a.cols[1].z + _b.cols[1].z, _a.cols[1].w + _b.cols[1].w},
       {_a.cols[2].x + _b.cols[2].x, _a.cols[2].y + _b.cols[2].y,
        _a.cols[2].z + _b.cols[2].z, _a.cols[2].w + _b.cols[2].w},
       {_a.cols[3].x + _b.cols[3].x, _a.cols[3].y + _b.cols[3].y,
        _a.cols[3].z + _b.cols[3].z, _a.cols[3].w + _b.cols[3].w}}};
  return ret;
}

// Computes the per element subtraction of two matrices _a and _b.
OZZ_INLINE ozz::math::SoaFloat4x4 operator-(const ozz::math::SoaFloat4x4& _a,
                                            const ozz::math::SoaFloat4x4& _b) {
  const ozz::math::SoaFloat4x4 ret = {
      {{_a.cols[0].x - _b.cols[0].x, _a.cols[0].y - _b.cols[0].y,
        _a.cols[0].z - _b.cols[0].z, _a.cols[0].w - _b.cols[0].w},
       {_a.cols[1].x - _b.cols[1].x, _a.cols[1].y - _b.cols[1].y,
        _a.cols[1].z - _b.cols[1].z, _a.cols[1].w - _b.cols[1].w},
       {_a.cols[2].x - _b.cols[2].x, _a.cols[2].y - _b.cols[2].y,
        _a.cols[2].z - _b.cols[2].z, _a.cols[2].w - _b.cols[2].w},
       {_a.cols[3].x - _b.cols[3].x, _a.cols[3].y - _b.cols[3].y,
        _a.cols[3].z - _b.cols[3].z, _a.cols[3].w - _b.cols[3].w}}};
  return ret;
}
#endif  // OZZ_OZZ_BASE_MATHS_SOA_FLOAT4X4_H_
