// SPDX-License-Identifier: Apache-2.0
// 
// Copyright 2018 Andreas Girgensohn
// Copyright 2018 Conrad Sanderson (http://conradsanderson.id.au)
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------

#include <armadillo>
#include "catch.hpp"

using namespace arma;

namespace
  {
  void
  initMatrix(mat& m)
    {
    for(uword ii = 0; ii < m.n_rows; ++ii)
    for(uword jj = 0; jj < m.n_cols; ++jj)
      {
      const int i = int(ii);
      const int j = int(jj);
      
      m(ii, jj) = 5 * (i % 17) + (i + j) % 13 - 7 * ((j + 2) % 5) + double(i)/double(m.n_rows);
      }
    }

  void checkEigenvectors(const mat& coeff)
    {
    // sign of the eigenvectors can be flipped
    REQUIRE(std::abs(coeff(0,0)) == Approx(2.2366412109e-01));
    REQUIRE(std::abs(coeff(0,1)) == Approx(3.1197826828e-01));
    REQUIRE(std::abs(coeff(0,2)) == Approx(5.1847537613e-02));
    REQUIRE(std::abs(coeff(1,0)) == Approx(2.2419616512e-01));
    REQUIRE(std::abs(coeff(1,1)) == Approx(2.7564301912e-01));
    REQUIRE(std::abs(coeff(1,2)) == Approx(1.0953921221e-01));
    REQUIRE(std::abs(coeff(2,0)) == Approx(2.2427613980e-01));
    REQUIRE(std::abs(coeff(2,1)) == Approx(1.6088934501e-01));
    REQUIRE(std::abs(coeff(2,2)) == Approx(2.3660988967e-01));
    }

  void checkScore(const mat& score)
    {
    REQUIRE(score(0,0) == Approx(-1.8538115696e+02));
    REQUIRE(score(0,1) == Approx(4.6671842099e+00));
    REQUIRE(score(0,2) == Approx(1.1026881736e+01));
    REQUIRE(score(1,0) == Approx(-1.6144314244e+02));
    REQUIRE(score(1,1) == Approx(8.0636602200e+00));
    REQUIRE(score(1,2) == Approx(8.5129014856e+00));
    REQUIRE(score(2,0) == Approx(-1.3750123749e+02));
    REQUIRE(score(2,1) == Approx(1.0312494525e+01));
    REQUIRE(score(2,2) == Approx(4.5214633042e+00));
    }

  void checkEigenvalues(const vec& latent)
    {
    REQUIRE(latent(0) == Approx(1.1989436021e+04));
    REQUIRE(latent(1) == Approx(9.2136913098e+01));
    REQUIRE(latent(2) == Approx(7.8335565832e+01));
    REQUIRE(latent(3) == Approx(2.4204644513e+01));
    REQUIRE(latent(4) == Approx(2.1302619671e+01));
    REQUIRE(latent(5) == Approx(1.1615198930e+01));
    REQUIRE(latent(6) == Approx(1.1040034957e+01));
    REQUIRE(latent(7) == Approx(7.7918177707e+00));
    REQUIRE(latent(8) == Approx(7.2862524567e+00));
    REQUIRE(latent(9) == Approx(6.5039856845e+00));
    }

  void checkHotteling(const vec& tsquared)
    {
    REQUIRE(tsquared(0) == Approx(7.1983631370e+02));
    REQUIRE(tsquared(1) == Approx(6.5616053343e+02));
    REQUIRE(tsquared(2) == Approx(5.6308987454e+02));
    REQUIRE(tsquared(3) == Approx(3.6908398978e+02));
    REQUIRE(tsquared(4) == Approx(2.4632493795e+02));
    REQUIRE(tsquared(5) == Approx(1.3213013367e+02));
    REQUIRE(tsquared(6) == Approx(5.7414718234e+01));
    REQUIRE(tsquared(7) == Approx(1.5157746233e+01));
    REQUIRE(tsquared(8) == Approx(1.7316032365e+01));
    REQUIRE(tsquared(9) == Approx(2.9290529527e+01));
    REQUIRE(tsquared(20) == Approx(2.6159738840e+02));
    }
  }

TEST_CASE("fn_princomp_1", "[princomp]")
  {
  mat m(1000, 20);
  initMatrix(m);
  mat coeff = princomp(m);
  checkEigenvectors(coeff);
  }

TEST_CASE("fn_princomp_2", "[princomp]")
  {
  mat m(1000, 20);
  initMatrix(m);
  mat coeff;
  princomp(coeff, m);
  checkEigenvectors(coeff);
  }

TEST_CASE("fn_princomp_3", "[princomp]")
  {
  mat m(1000, 20);
  initMatrix(m);
  mat coeff;
  mat score;
  princomp(coeff, score, m);
  checkScore(score);
  checkEigenvectors(coeff);
  }

TEST_CASE("fn_princomp_4", "[princomp]")
  {
  mat m(1000, 20);
  initMatrix(m);
  mat coeff;
  mat score;
  vec latent;
  princomp(coeff, score, latent, m);
  checkEigenvectors(coeff);
  checkScore(score);
  checkEigenvalues(latent);
  }

TEST_CASE("fn_princomp_5", "[princomp]")
  {
  mat m(1000, 20);
  initMatrix(m);
  mat coeff;
  mat score;
  vec latent;
  vec tsquared;
  princomp(coeff, score, latent, tsquared, m);
  checkEigenvectors(coeff);
  checkScore(score);
  checkEigenvalues(latent);
  // checkHotteling(tsquared);  // TODO
  }

TEST_CASE("fn_princomp_6", "[princomp]")
  {
  mat m(5, 20);
  initMatrix(m);
  mat coeff = princomp(m);
  REQUIRE(std::abs(coeff(0,0)) == Approx(2.4288979933e-01).margin(0.001));
  REQUIRE(std::abs(coeff(0,1)) == Approx(3.9409505019e-16).margin(0.001));
  REQUIRE(std::abs(coeff(0,2)) == Approx(1.2516285718e-02).margin(0.001));
  REQUIRE(std::abs(coeff(1,0)) == Approx(2.4288979933e-01).margin(0.001));
  REQUIRE(std::abs(coeff(1,1)) == Approx(2.9190770799e-16).margin(0.001));
  REQUIRE(std::abs(coeff(1,2)) == Approx(1.2516285718e-02).margin(0.001));
  REQUIRE(std::abs(coeff(2,0)) == Approx(2.4288979933e-01).margin(0.001));
  REQUIRE(std::abs(coeff(2,1)) == Approx(3.4719806003e-17).margin(0.001));
  REQUIRE(std::abs(coeff(2,2)) == Approx(1.2516285718e-02).margin(0.001));
  REQUIRE(std::abs(coeff(19,19)) == Approx(9.5528446175e-01).margin(0.01));
  }

