/*
 * Copyright 2023 Hartmut Seichter
 *
 * 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 PW_CORE_MATRIX_HPP
#define PW_CORE_MATRIX_HPP

#include <paradiso/globals.hpp>
#include <paradiso/matrixbase.hpp>


namespace paradiso {

template <std::size_t R, std::size_t C, typename Scalar, bool RowMajor = false>
struct Matrix : MatrixBase<Scalar, Matrix<R, C, Scalar>> {

    Scalar data[R * C]{}; 

    static constexpr std::size_t rows{R};
    static constexpr std::size_t cols{C};
    static constexpr std::size_t coefficients{R * C};

    using col_type = Matrix<R, 1, Scalar>;
    using row_type = Matrix<1, C, Scalar>;
    using transpose_type = Matrix<C, R, Scalar>;

    template <typename... Arguments>
    static constexpr Matrix<R, C, Scalar> make(Arguments... values) noexcept {
        static_assert(sizeof...(Arguments) == R * C,
                      "Incorrect number of arguments");
        return {.data = {values...}};
    }

    constexpr size_t offset(size_t r, size_t c) const {
        return (RowMajor) ? r * C + c : c * R + r;
    }

    constexpr Scalar& operator()(std::size_t r, std::size_t c) {
        return data[offset(r, c)];
    }

    constexpr const Scalar& operator()(std::size_t r, std::size_t c) const {
        return data[offset(r, c)];
    }

    constexpr const Scalar* ptr() const { return &data[0]; }

    //! set identity
    constexpr Matrix& set_identity() {
        for (std::size_t r = 0; r < rows; r++)
            for (std::size_t c = 0; c < cols; c++)
                (*this)(r, c) = (c == r) ? Scalar(1) : Scalar(0);
        return *this;
    }

    constexpr Matrix& set_uniform(const Scalar& v) {
        std::fill(std::begin(data), std::end(data), v);
        return *this;
    }

    template <std::size_t Rs, std::size_t Cs, bool RowMajorSlice = RowMajor>
    auto slice(std::size_t r, std::size_t c) const {
        Matrix<Rs, Cs, Scalar, RowMajorSlice> s;
        for (std::size_t ri = 0; ri < Rs; ri++)
            for (std::size_t ci = 0; ci < Cs; ci++)
                s(ri, ci) = (*this)(ri + r, ci + c);
        return s;
    }

    template <std::size_t Rs, std::size_t Cs, bool RowMajorSlice = RowMajor>
    Matrix& set_slice(const Matrix<Rs, Cs, Scalar, RowMajorSlice>& s,
                      std::size_t r, std::size_t c) {
        for (std::size_t ri = 0; ri < Rs; ri++)
            for (std::size_t ci = 0; ci < Cs; ci++)
                (*this)(ri + r, ci + c) = s(ri, ci);
        return *this;
    }

    template <std::size_t Rs, std::size_t Cs, bool RowMajorSlice = RowMajor>
    auto minor(std::size_t r0, std::size_t c0) const {
        Matrix<Rs, Cs, Scalar, RowMajorSlice> m;
        size_t r = 0;
        for (size_t ri = 0; ri < R; ri++) {
            size_t c = 0;
            if (ri == r0)
                continue;
            for (size_t ci = 0; ci < C; ci++) {
                if (ci == c0)
                    continue;
                m(r, c) = (*this)(ri, ci);
                c++;
            }
            r++;
        }
        return m;
    }

    Scalar determinant() const {
        Scalar det(0);
        for (size_t c = 0; c < C; c++)
            det += ((c % 2 == 0) ? (*this)(0, c) : -(*this)(0, c)) *
                   this->minor<R - 1, C - 1, RowMajor>(0, c).determinant();
        return det;
    }

    auto transposed() const {
        transpose_type res;
        for (size_t r = rows; r-- > 0;)
            for (size_t c = cols; c-- > 0;)
                res(c, r) = (*this)(r, c);
        return res;
    }

    auto inverse() const {
        Scalar invDet = Scalar(1) / this->determinant();
        Matrix<R, C, Scalar, RowMajor> inv;
        for (int j = 0; j < C; j++)
            for (int i = 0; i < R; i++) {
                const Scalar minorDet =
                    this->minor<R - 1, C - 1, RowMajor>(j, i).determinant();
                const Scalar coFactor =
                    ((i + j) % 2 == 1) ? -minorDet : minorDet;
                inv(i, j) = invDet * coFactor;
            }
        return inv;
    }

    inline bool row_major() const { return RowMajor; }

    inline bool square() const { return R == C; }

    inline const Matrix operator+(const Matrix& other) const {
        Matrix res(*this);
        for (size_t r = 0; r < R; r++)
            for (size_t c = 0; c < C; c++)
                res(r, c) += other(r, c);
        return res;
    }

    inline const Matrix operator-(const Matrix& other) const {
        Matrix res(*this);
        for (size_t r = 0; r < R; r++)
            for (size_t c = 0; c < C; c++)
                res(r, c) -= other(r, c);
        return res;
    }

    auto row(size_t row_) const {
        row_type r;
        for (size_t i = 0; i < cols; i++)
            r[i] = (*this)(row_, i);
        return r;
    }

    auto column(size_t col_) const {
        col_type c;
        for (size_t i = 0; i < rows; i++)
            c[i] = (*this)(i, col_);
        return c;
    }

    static constexpr auto identity() {
        Matrix res;
        for (std::size_t r = 0; r < rows; r++)
            for (std::size_t c = 0; c < cols; c++)
                res(r, c) = (c == r) ? Scalar(1) : Scalar(0);
        return res;
    }
};

template <> inline float Matrix<1, 1, float>::determinant() const {
    return (*this)(0, 0);
}

template <> inline double Matrix<1, 1, double>::determinant() const {
    return (*this)(0, 0);
}

template <typename Scalar, std::size_t R, std::size_t Ca, std::size_t Cb>
auto operator*(const Matrix<R, Ca, Scalar>& A, const Matrix<R, Cb, Scalar>& B) {
    Matrix<R, Cb, Scalar> result;
    result.zero(); // zero the output
    for (size_t r = 0; r < R; r++)
        for (size_t c = 0; c < Cb; c++)
            for (size_t iI = 0; iI < R; iI++)
                result(r, c) += A(r, iI) * B(iI, c); // inner product
    return result;
}
} // namespace paradiso

#endif
