/*
 * Copyright (c) 1999-2021 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_TRANSFORM_HPP
#define PW_CORE_MATRIX_TRANSFORM_HPP

#include <pw/core/math.hpp>
#include <pw/core/matrix.hpp>
#include <pw/core/vector.hpp>
#include <pw/core/frustum.hpp>

namespace pw {

struct matrix_transform {

    template <typename Scalar>
    constexpr static auto
    scale_matrix(vector<Scalar, 4>&& diag) noexcept -> matrix<Scalar, 4, 4> {
        return matrix<Scalar, 4, 4>::from_diagonal(
            std::forward<vector<Scalar, 4>>(diag));
    }

    template <typename Scalar>
    constexpr static auto
    frustum_matrix(const frustum<Scalar>& f) -> matrix<Scalar, 4, 4> {

        const auto Sx{Scalar{2} * f.z_near / (f.right - f.left)};
        const auto Sy{Scalar{2} * f.z_near / (f.top - f.bottom)};

        const auto A{(f.right + f.left) / (f.right - f.left)};
        const auto B{(f.top + f.bottom) / (f.top - f.bottom)};
        const auto C{-(f.z_far + f.z_near) / (f.z_far - f.z_near)};
        const auto D{-Scalar{2} * f.z_far * f.z_near / (f.z_far - f.z_near)};

        // clang-format off
        return {vector{Sx, 0, A, 0},
                vector{0, Sy, B, 0},
                vector{0, 0, C, D},
                vector{0, 0, -1, Scalar{1}}};
        // clang-format on
    }

    template <typename Scalar>
    constexpr static auto
    look_at_matrix(vector3<Scalar> position, vector3<Scalar> target,
                   vector3<Scalar> up) -> matrix<Scalar, 4, 4> {

        const auto lofs = (target - position).normalized(); // line of sight
        const auto side = lofs.cross(up).normalized();      // side vector
        const auto upvc = side.cross(lofs).normalized();    // upvector

        return {side.unproject(0), upvc.unproject(0),
                lofs.unproject(0) * Scalar{-1}, position.unproject(1)};
    }
};

#if 0

    //    /// creates a projection from a frustum planes with a reversed depth
    //    mapped to [0..1] pub fn make_projection_rh_from_frustum_reversed(
    //        left: f32,
    //        right: f32,
    //        bottom: f32,
    //        top: f32,
    //        z_near: f32,
    //        z_far: f32,
    //    ) -> Mat4 {
    //        assert!(z_near > 0.0 && z_far > 0.0);

    //        // info!("near {:?}", z_near);

    //        //
    //        // reversed z 0..1 projection
    //        //
    //        let a = (right + left) / (right - left); // position in frame
    //        horizontal let b = (top + bottom) / (top - bottom); // position in
    //        frame vertical

    //        let c = z_near / (z_far - z_near); // lower bound
    //        let d = z_far * z_near / (z_far - z_near); // upper bound

    //        let sx = 2.0 * z_near / (right - left); // scale x
    //        let sy = 2.0 * z_near / (top - bottom); // scale y

    //        // reverse z 0..1
    //        // --------------
    //        // sx   0   a   0
    //        // 0    sy  b   0
    //        // 0    0   c   d
    //        // 0    0  -1   0

    //        Mat4::from_cols(
    //            Vec4::new(sx,   0.0,    0.0,    0.0),
    //            Vec4::new(0.0,  sy,     0.0,    0.0),
    //            Vec4::new(a,    b,      c,     -1.0),
    //            Vec4::new(0.0,  0.0,    d,      0.0),
    //        )
    //    }
    inline static matrix_<4, 4, T>
    perspective_projection(const T& field_of_view, const T& aspect_ratio,
                           const T& z_near, const T& z_far) {
        const auto tan_half = tan(field_of_view / T(2));
        const auto right    = tan_half * z_near / aspect_ratio;
        const auto left     = -right;
        const auto top      = right / aspect_ratio;
        const auto bottom   = -top;

        return perspective_frustum_rh(left, right, bottom, top, z_near, z_far);
    }

    inline static matrix_<4, 4, T>
    orthographic_frustum(T left, T right, T bottom, T top, T z_near, T z_far) {

        matrix_<4, 4, T> ortho;
        ortho.fill(0);

        ortho(0, 0) = static_cast<T>(2) / (right - left);
        ortho(1, 1) = static_cast<T>(2) / (top - bottom);
        ortho(2, 2) = -static_cast<T>(2) / (z_far - z_near);

        ortho(3, 0) = -(right + left) / (right - left);
        ortho(3, 1) = -(top + bottom) / (top - bottom);
        ortho(3, 2) = -(z_far + z_near) / (z_far - z_near);

        ortho(3, 3) = 1;

        return ortho;
    }

    inline static matrix_<4, 4, T> orthographic_projection(T width, T height,
                                                           T z_near, T z_far) {
        return orthographic_frustum(-width / 2, width / 2, -height / 2,
                                    height / 2, z_near, z_far);
    }

    inline static matrix_<4, 4, T> look_at(const vector3_<T>& position,
                                           const vector3_<T>& target,
                                           const vector3_<T>& up) {
        matrix_<4, 4, T> view_matrix;
        view_matrix.set_identity();

        const vector3_<T> los =
            (target - position).normalized(); // line of sight
        const vector3_<T> sid =
            vector3_<T>::cross(los, up).normalized(); // side vector
        const vector3_<T> upd =
            vector3_<T>::cross(sid, los).normalized(); // upvector

        // set base vectors
        view_matrix.set_slice(sid, 0, 0);
        view_matrix.set_slice(upd, 0, 1);
        view_matrix.set_slice(los * T(-1), 0, 2);
        view_matrix.set_slice(position, 0, 3);

        return view_matrix;
    }

#endif

} // namespace pw

#endif
