
// Copyright 2019 Shlomi Steinberg
// 

#pragma once

#include <memory>
#include <array>

#include "illuminant.hpp"
#include "glm.hpp"
#include "common.hpp"
#include "orthogonal_frame.hpp"

class light {
private:
    std::unique_ptr<illuminant_base> illuminant;
    const glm::vec3 L;
    const glm::vec3 light_rotate_vec;
    const float light_dist_start;
    const float light_dist_end;
    const float light_rotate_start;
    const float light_rotate_end;
    const float intensity;
    const float extend;
    const bool area;

public:
    light(std::unique_ptr<illuminant_base> &&illuminant, const glm::vec3 &L,
          float light_dist_start, float light_dist_end,
          glm::vec3 light_rotate_vec, float light_rotate_start, float light_rotate_end, 
          float intensity, float extend, bool area=false) noexcept 
        : illuminant(std::move(illuminant)), L(glm::normalize(L)), 
          light_dist_start(light_dist_start), light_dist_end(light_dist_end),
          light_rotate_vec(std::move(light_rotate_vec)), light_rotate_start(light_rotate_start), light_rotate_end(light_rotate_end),
          extend(extend), intensity(intensity), area(area) 
    {}
    light(light&&) noexcept = default;
    light &operator=(light&&) noexcept = default;
    ~light() noexcept = default;

    inline auto position(float t) const noexcept {
        const auto d = glm::mix(light_dist_start, light_dist_end, t*t*t*t);
        const auto p = glm::mix(light_rotate_start, light_rotate_end, t);
        const auto r = glm::rotate(glm::mat4(1.f), p, light_rotate_vec) * glm::vec4{ L*d, 1.f };
        return glm::vec3{ r.x, r.y, r.z };
    }
    inline auto get_intensity(float t, float scale) const noexcept {
        if (!area)
            return intensity;
        return intensity / glm::sqrt(glm::length(position(t)) * scale);
    }

    inline auto vertices(float t, float scale) const noexcept {
        const auto p = position(t);
        const auto d = glm::length(p);

        std::array<glm::vec3, 4> vs = {
            glm::vec3{ -extend*.5f,-extend*.5f,d },
            glm::vec3{  extend*.5f,-extend*.5f,d },
            glm::vec3{ -extend*.5f, extend*.5f,d },
            glm::vec3{  extend*.5f, extend*.5f,d },
        };

        glm::vec3 tss,bss;
        const auto n = p/d;
        orthogonal_frame(n, tss, bss);
        const auto tbn = glm::mat3(tss,bss,n);
        for (auto &v : vs)
            v = tbn*v*scale;

        return vs;
    }

    inline auto solid_angle(float t, const glm::vec3 &p) const noexcept {
        const auto P = position(t);
        const auto area = sqr(extend);
        const auto dist2 = glm::dot(P-p,P-p);
        return 4.f * glm::asin(area / (area + 4.f*dist2));
    }

    // Coherence radius at a point p with normal n
    inline auto coherence_radius(float sr) const noexcept {
        static constexpr float mean_wavelength_um = 0.5f;
        return mean_wavelength_um / glm::sqrt(glm::pi<float>() * sr);
    }
    inline auto coherence_length() const noexcept { return illuminant->coherence_length(); }
    inline auto is_area() const noexcept { return area; }

    inline const auto* get_illuminant() const noexcept { return illuminant.get(); }
};
