#include "fesa/math/matrix.hpp"

#include <gtest/gtest.h>

#include <limits>
#include <stdexcept>
#include <utility>

namespace fesa {
namespace {

TEST(DenseMath, RowMajorMatrixMatchesKnownGemvGemm) {
    const std::size_t wraparoundRows =
        (std::numeric_limits<std::size_t>::max)() / 2U + 1U;
    EXPECT_THROW(static_cast<void>(Matrix{wraparoundRows, 2}), std::length_error);

    Matrix zeroRows{0, 3};
    Vector threeValues{3, 2.0};
    const Vector zeroRowProduct = zeroRows.multiply(threeValues);
    EXPECT_EQ(zeroRowProduct.size(), 0U);

    Matrix zeroColumns{2, 0};
    const Vector zeroColumnProduct = zeroColumns.multiply(Vector{0});
    ASSERT_EQ(zeroColumnProduct.size(), 2U);
    EXPECT_DOUBLE_EQ(zeroColumnProduct[0], 0.0);
    EXPECT_DOUBLE_EQ(zeroColumnProduct[1], 0.0);

    Matrix zeroInnerRight{0, 3};
    const Matrix zeroInnerProduct = zeroColumns.multiply(zeroInnerRight);
    EXPECT_EQ(zeroInnerProduct.rows(), 2U);
    EXPECT_EQ(zeroInnerProduct.columns(), 3U);
    for (std::size_t row = 0; row < zeroInnerProduct.rows(); ++row) {
        for (std::size_t column = 0; column < zeroInnerProduct.columns(); ++column) {
            EXPECT_DOUBLE_EQ(zeroInnerProduct(row, column), 0.0);
        }
    }

    Matrix left{2, 3};
    left(0, 0) = 1.0;
    left(0, 1) = 2.0;
    left(0, 2) = 3.0;
    left(1, 0) = 4.0;
    left(1, 1) = 5.0;
    left(1, 2) = 6.0;

    EXPECT_EQ(&left(0, 0) + 1, &left(0, 1));
    EXPECT_EQ(&left(0, 0) + 2, &left(0, 2));
    EXPECT_EQ(&left(0, 0) + 3, &left(1, 0));
    const Matrix& constLeft = left;
    EXPECT_DOUBLE_EQ(constLeft(1, 2), 6.0);

    Matrix copied{left};
    copied(0, 0) = 42.0;
    EXPECT_DOUBLE_EQ(left(0, 0), 1.0);

    Matrix copyAssigned{0, 0};
    copyAssigned = left;
    copyAssigned(1, 2) = -7.0;
    EXPECT_DOUBLE_EQ(left(1, 2), 6.0);

    Matrix moved{std::move(copied)};
    EXPECT_EQ(copied.rows(), 0U);
    EXPECT_EQ(copied.columns(), 0U);
    EXPECT_EQ(moved.rows(), 2U);
    EXPECT_EQ(moved.columns(), 3U);
    EXPECT_DOUBLE_EQ(moved(0, 0), 42.0);
    EXPECT_NO_THROW(static_cast<void>(copied.multiply(Vector{0})));

    Matrix moveAssigned{1, 1, -1.0};
    moveAssigned = std::move(copyAssigned);
    EXPECT_EQ(copyAssigned.rows(), 0U);
    EXPECT_EQ(copyAssigned.columns(), 0U);
    EXPECT_EQ(moveAssigned.rows(), 2U);
    EXPECT_EQ(moveAssigned.columns(), 3U);
    EXPECT_DOUBLE_EQ(moveAssigned(1, 2), -7.0);

    Vector vector{3};
    vector[0] = 7.0;
    vector[1] = 8.0;
    vector[2] = 9.0;
    const Vector matrixVectorProduct = left.multiply(vector);
    ASSERT_EQ(matrixVectorProduct.size(), 2U);
    EXPECT_DOUBLE_EQ(matrixVectorProduct[0], 50.0);
    EXPECT_DOUBLE_EQ(matrixVectorProduct[1], 122.0);

    Matrix right{3, 2};
    right(0, 0) = 7.0;
    right(0, 1) = 8.0;
    right(1, 0) = 9.0;
    right(1, 1) = 10.0;
    right(2, 0) = 11.0;
    right(2, 1) = 12.0;
    const Matrix matrixProduct = left.multiply(right);
    ASSERT_EQ(matrixProduct.rows(), 2U);
    ASSERT_EQ(matrixProduct.columns(), 2U);
    EXPECT_DOUBLE_EQ(matrixProduct(0, 0), 58.0);
    EXPECT_DOUBLE_EQ(matrixProduct(0, 1), 64.0);
    EXPECT_DOUBLE_EQ(matrixProduct(1, 0), 139.0);
    EXPECT_DOUBLE_EQ(matrixProduct(1, 1), 154.0);

    EXPECT_THROW(static_cast<void>(left(2, 0)), std::out_of_range);
    EXPECT_THROW(static_cast<void>(left(0, 3)), std::out_of_range);
    EXPECT_THROW(static_cast<void>(constLeft(2, 0)), std::out_of_range);
    EXPECT_THROW(static_cast<void>(left.multiply(Vector{2})), std::invalid_argument);
    EXPECT_THROW(static_cast<void>(left.multiply(Matrix{4, 1})), std::invalid_argument);
}

} // namespace
} // namespace fesa
