/*
 * 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_VECTOR_HPP
#define PW_CORE_VECTOR_HPP

#include <pw/core/globals.hpp>

#include <cmath>
#include <concepts>
#include <functional>

namespace pw {

template <typename T, auto N>
concept Vector2 = (N == 2);

template <typename T, auto N>
concept Vector3 = (N == 3);

template <typename T, auto N>
concept Vector4 = (N == 4);

template <typename Scalar, std::size_t N> struct vector final {

    static_assert(N > 0, "Undefined vector space.");

    using value_type      = Scalar;
    using size_type       = decltype(N);
    using difference_type = std::ptrdiff_t;
    using reference       = value_type&;
    using const_reference = const value_type&;
    using pointer         = value_type*;
    using const_pointer   = const value_type*;

    static constexpr size_type coefficients{N};

    Scalar v_[N]{};

    auto&& data(this auto&& self) {
        return std::forward<decltype(self)>(self).v_;
    }

    auto&& operator[](this auto&& self, size_type e) {
        return std::forward<decltype(self)>(self).v_[e];
    }

    static constexpr auto basis(const auto& d) noexcept {
        return [&d]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return vector{(d == Ss) ? Scalar(1) : Scalar(0)...};
        }(std::make_index_sequence<N>{});
    }

    template <typename... Args>
    static constexpr auto
    make(Args&&... values) noexcept -> vector<Scalar, sizeof...(Args)> {
        static_assert(sizeof...(Args) == N, "Incorrect number of arguments.");
        return {{Scalar(values)...}};
    }

    static constexpr auto all(Scalar value) noexcept -> vector {
        return [&value]<std::size_t... Is>(std::index_sequence<Is...>) {
            return vector{value + Scalar{Is * 0}...};
        }(std::make_index_sequence<N>{});
    }

    template <typename... Args>
    constexpr auto swizzle(Args&&... indices) const noexcept
        -> vector<Scalar, sizeof...(Args)> {
        return {{Scalar{v_[indices]}...}};
    }

    constexpr auto minor(std::unsigned_integral auto d0) const noexcept {
        return [this, &d0]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return vector<Scalar, N - 1>{
                (*this).v_[(Ss < d0) ? Ss : Ss + 1]...};
        }(std::make_index_sequence<N - 1>{});
    }

    static constexpr auto sequence(Scalar factor = Scalar{1},
                                   Scalar offset = Scalar{0}) noexcept {
        return [&]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return vector<Scalar, N>{{Scalar{Ss} * factor + offset...}};
        }(std::make_index_sequence<N>{});
    }

    static constexpr auto lerp(const vector& A, const vector& B,
                               const Scalar& t) -> vector {
        return [&]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return vector<Scalar, N>{{A[Ss] + t * (B[Ss] - A[Ss])...}};
        }(std::make_index_sequence<N>{});
    }

    constexpr Scalar dot(const auto& other) const {
        return [this, &other]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return (... + (other[Ss] * (*this)[Ss]));
        }(std::make_index_sequence<N>{});
    }

    constexpr auto squared_norm() const noexcept { return dot(*this); }

    constexpr auto norm() const noexcept {
        return std::sqrt(this->squared_norm());
    }

    constexpr vector operator*(const Scalar& v) const noexcept {
        return [this, &v]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return vector{{(*this)[Ss] * v...}};
        }(std::make_index_sequence<N>{});
    }

    constexpr vector operator/(const Scalar& v) const noexcept {
        return (*this).operator*(Scalar{1} / v);
    }

    constexpr vector operator+(const vector& v) const noexcept {
        return [this, &v]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return vector{{(*this)[Ss] + v[Ss]...}};
        }(std::make_index_sequence<N>{});
    }

    constexpr vector operator-(const vector& v) const noexcept {
        return [this, &v]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return vector{{(*this)[Ss] - v[Ss]...}};
        }(std::make_index_sequence<N>{});
    }

    constexpr vector& operator*=(const Scalar& v) noexcept {
        [this, &v]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            (((*this)[Ss] *= v), ...);
        }(std::make_index_sequence<N>{});
        return *this;
    }

    constexpr vector& operator/=(const Scalar& v) noexcept {
        return operator*=(Scalar{1} / v);
    }

    constexpr vector& operator+=(const Scalar& v) noexcept {
        [this, &v]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            (((*this)[Ss] += v), ...);
        }(std::make_index_sequence<N>{});
        return *this;
    }

    constexpr vector& operator-=(const Scalar& v) noexcept {
        return operator+=(-v);
    }

    constexpr vector min(const vector& v) const noexcept {
        return [this, &v]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return pw::vector<Scalar, N>{(std::min(v[Ss], (*this)[Ss]))...};
        }(std::make_index_sequence<N>{});
    }

    constexpr vector max(const vector& v) const noexcept {
        return [this, &v]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return pw::vector<Scalar, N>{(std::max(v[Ss], (*this)[Ss]))...};
        }(std::make_index_sequence<N>{});
    }

    constexpr auto project() const noexcept -> vector<Scalar, N - 1> {
        return [this]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return vector<Scalar, N - 1>{{(*this)[Ss] / (*this)[N - 1]...}};
        }(std::make_index_sequence<N - 1>{});
    }

    constexpr auto
    unproject(const Scalar& w) const noexcept -> vector<Scalar, N + 1> {
        return [this, &w]<std::size_t... Ss>(std::index_sequence<Ss...>) {
            return vector<Scalar, N + 1>{{(Ss < N) ? (*this)[Ss] : w...}};
        }(std::make_index_sequence<N + 1>{});
    }

    constexpr vector cross(const auto& rhs) const noexcept
        requires(Vector3<Scalar, N>)
    {
        return {(*this)[1] * rhs[2] - rhs[1] * (*this)[2],
                (*this)[2] * rhs[0] - rhs[2] * (*this)[0],
                (*this)[0] * rhs[1] - rhs[0] * (*this)[1]};
    }

    constexpr vector normalized() const noexcept { return (*this) / norm(); }

    auto&& x(this auto&& self)
        requires(Vector2<Scalar, N> || Vector3<Scalar, N> || Vector4<Scalar, N>)
    {
        return std::forward<decltype(self)>(self).v_[0];
    }

    auto&& y(this auto&& self)
        requires(Vector2<Scalar, N> || Vector3<Scalar, N> || Vector4<Scalar, N>)
    {
        return std::forward<decltype(self)>(self).v_[1];
    }

    auto&& z(this auto&& self)
        requires(Vector3<Scalar, N> || Vector4<Scalar, N>)
    {
        return std::forward<decltype(self)>(self).v_[2];
    }

    auto&& w(this auto&& self)
        requires(Vector4<Scalar, N>)
    {
        return std::forward<decltype(self)>(self).v_[3];
    }

    static constexpr vector right() noexcept
        requires(Vector3<Scalar, N>)
    {
        return vector{1, 0, 0};
    };

    static constexpr vector up() noexcept
        requires(Vector3<Scalar, N>)
    {
        return vector{0, 1, 0};
    };

    static constexpr vector forward() noexcept
        requires(Vector3<Scalar, N>)
    {
        return vector{0, 0, -1};
    };

    static constexpr vector x_axis() noexcept
        requires(Vector2<Scalar, N> || Vector3<Scalar, N> || Vector4<Scalar, N>)
    {
        return vector::basis(0);
    };

    static constexpr vector y_axis() noexcept
        requires(Vector2<Scalar, N> || Vector3<Scalar, N> || Vector4<Scalar, N>)
    {
        return vector::basis(1);
    };

    static constexpr vector z_axis() noexcept
        requires(Vector3<Scalar, N> || Vector4<Scalar, N>)
    {
        return vector::basis(2);
    };

    static constexpr vector w_axis() noexcept
        requires(Vector4<Scalar, N>)
    {
        return vector::basis(3);
    };

    //
    // part of tuple-protocol
    //
    template <size_type idx> auto get(this auto&& self) -> decltype(auto) {
        static_assert(idx < N, "Out of bounds access to a member.");
        return std::forward_like<decltype(self)>(self.v_[idx]);
    }

    //
    // Iterators
    //
    constexpr const_pointer begin() const { return &v_[0]; }

    constexpr const_pointer end() const { return &v_[N]; }
};

// deduction guide for vector
template <class T, class... U, class CT = std::common_type_t<T, U...>>
vector(T, U...) -> vector<CT, 1 + sizeof...(U)>;

//
// Vector Aliases
//

template <typename Scalar> using vector2 = vector<Scalar, 2>;
template <typename Scalar> using vector3 = vector<Scalar, 3>;
template <typename Scalar> using vector4 = vector<Scalar, 4>;

using vector2f = vector2<float>;
using vector2d = vector2<double>;
using vector2i = vector2<int>;

using vector3f = vector3<float>;
using vector3d = vector3<double>;
using vector3i = vector3<int>;

using vector4f = vector4<float>;
using vector4d = vector4<double>;
using vector4i = vector4<int>;

} // namespace pw

//
// tuple - protocol
//
template <class Scalar, std::size_t N>
struct std::tuple_size<pw::vector<Scalar, N>>
    : std::integral_constant<std::size_t, N> {};

template <std::size_t I, class Scalar, std::size_t N>
struct std::tuple_element<I, pw::vector<Scalar, N>> {
    using type = Scalar;
};

#endif
