#include "viewport.hpp"
#include <assert.h>


/**
 * @file
 * @brief Per-frame container datastructures and raster viewport to frame coordinates translation.
 */


ZViewport::ZViewport(const viewport_spec_t& viewport)
    : config{viewport}, ray_cache{config.resolution.x, config.resolution.y}, camera{0.0F, 0.0F, config.camera_z} {
    assert(config.viewport.x > 0.0F && config.viewport.y > 0.0F && config.viewport.z > 0.0F);

    for (dcoord_t x = 0; x < ray_cache.width(); ++x) {
        for (dcoord_t y = 0; y < ray_cache.width(); ++y) {
            ray_cache.at(x, y) = precompute_ray((fcoord_t)x / (fcoord_t)(ray_cache.width() - 1),
                                                (fcoord_t)y / (fcoord_t)(ray_cache.height() - 1));
        }
    }
}


const ZViewport::viewport_spec_t& ZViewport::get_config() const {
    return config;
}


/** @brief From camera position through viewport to behind z axis (possibly overestimated). */
ray_t ZViewport::precompute_ray(fcoord_t u, fcoord_t v) const {
    const vertex_t target = {(u - 0.5F) * config.viewport.x, (v - 0.5F) * config.viewport.y,
                             config.viewport.z - camera.z};
    const vertex_t target_min = (target * vertex_t::scale_factor(1.0F, camera.z, target.z)).with_z(1.0F - camera.z);
    const vertex_t target_max = (target * vertex_t::scale_factor(0.0F, camera.z, target.z)).with_z(0.0F - camera.z);
    return ray_t{target_min, target_max - target_min, target_max};
}


void ZViewport::update(vertex_t new_camera) {
    assert(new_camera.z > config.viewport.z);
    camera = {new_camera.x, new_camera.y, camera.z};
}


const vertex_t& ZViewport::get_camera() const {
    return camera;
}


ray_t ZViewport::get_projected_ray(dcoord_t x, dcoord_t y) const {
    const ray_t& ray = ray_cache.at(x, y);
    return {ray.pos + camera, ray.dir, ray.dst + camera};
}


box_t ZViewport::get_viewbox(const area_t& view) const {
    const vertex_t& target_min = ray_cache.at(view.lo.x, view.lo.y).dst + camera;
    const vertex_t& target_max = ray_cache.at(view.hi.x - 1, view.hi.y - 1).dst + camera;
    return {target_min.with_z(0.0F), target_max.with_z(1.0F)}; // assuming all objects are contained in [0,1]z
}


context_t::context_t(const ZViewport::viewport_spec_t& viewport)
    : viewport(viewport),
      trace_info(viewport.resolution.w, viewport.resolution.h),
      light_info(viewport.resolution.w, viewport.resolution.h) {
    corners.at(0).viewport = {{0, 0}, {viewport.resolution.x / 2, viewport.resolution.y / 2}};
    corners.at(1).viewport = {{0, viewport.resolution.y / 2}, {viewport.resolution.x / 2, viewport.resolution.y}};
    corners.at(2).viewport = {{viewport.resolution.x / 2, 0}, {viewport.resolution.x, viewport.resolution.y / 2}};
    corners.at(3).viewport = {{viewport.resolution.x / 2, viewport.resolution.y / 2},
                              {viewport.resolution.x, viewport.resolution.y}};
}


std::unique_ptr<context_t> ContextFactory::make() {
    return std::make_unique<context_t>(viewport);
};

ContextFactory::ContextFactory(const ZViewport::viewport_spec_t& viewport) : viewport(viewport) {
#if 1
    for (int i = 0; i < 5; ++i) {
        push(make()); // prevent unusual startup delays by some possibly somewhat local pre-allocation
    }
#endif
}