#include "state.hpp"


/**
 * @file
 * @brief Mainly heuristics to reduce the number of objects and lights that need to be traced.
 */


/** @brief An object is fully occluded by another one from the given position. */
static bool is_occluded(const Object* obj, const vertex_t& pos, const scene_t& visible_scene) {
    return std::any_of(visible_scene.begin(), visible_scene.end(), [obj, &pos](const auto& other) {
        return other.get() != obj && other->intersect_full(pos, obj->box());
    });
}


/** @brief A box volume is fully occluded by an object from the given position. */
static bool is_occluded(const box_t& box, const vertex_t& pos, const scene_t& visible_scene) {
    return std::any_of(visible_scene.begin(), visible_scene.end(),
                       [&box, &pos](const auto& other) { return other->intersect_full(pos, box); });
}


/** @brief An object can possibly be reached by any active light. */
static bool can_lit(const Object* obj, const std::list<lightbox_t>& effective_lights) {
    return std::any_of(effective_lights.begin(), effective_lights.end(), [obj](const auto& light) {
        return light.box.intersects_xy(obj->box()) && obj->can_intersect(light.rad.pos);
    });
}


/** @brief An object is not occluded and can in turn occlude any active light. */
static bool can_occlude(const Object* obj, const std::list<lightbox_t>& effective_lights, const box_t& viewport,
                        const scene_t& visible_scene) {
    for (const auto& light : effective_lights) {
        box_t effective_box{vertex_t::minof(viewport.lo, light.rad.pos), vertex_t::maxof(viewport.hi, light.rad.pos)};
        if (obj->box().intersects_xy(effective_box) && light.box.intersects_xy(obj->box()) &&
            !is_occluded(obj, light.rad.pos, visible_scene)) {
            return true;
        }
    }
    return false;
}


/** @brief An object might not be seen but can still obstruct view. */
static bool can_occlude(const Object* obj, const vertex_t& pos, const box_t& viewport, const scene_t& visible_scene) {
    return viewport.intersects_xy(obj->box()) && obj->can_intersect(pos) && !is_occluded(obj, pos, visible_scene);
}


/** @brief A lightbox can be cut down if an object fully divides it. */
static void cut_lightbox(lightbox_t& lightbox, const scene_t& full_scene) {
#if USE_LIGHTBOX > 0
    for (const auto& obj : full_scene) {
        lightbox.box.cut_down(lightbox.rad.pos, obj->box());
    }
#if USE_LIGHTBOX > 1
    // try again, a some smaller box might allow a new cut
    for (const auto& obj : full_scene) {
        lightbox.box.cut_down(lightbox.rad.pos, obj->box());
    }
#endif
#endif
}


/** @brief Lightbox factory, depending on the max light radius. */
static lightbox_t make_lightbox(const PointLight* light, const vertex_t& pos) {
    const disc_t rad{pos.with_z(0.5F), vertex_t::setted(light->get_max_dist()).with_z(0.5F)};
    return lightbox_t{light, rad, rad.get_box()};
}


/** @brief Lightbox, possibly reduced if an object splits through. */
static lightbox_t make_lightbox(const PointLight* light, const vertex_t& pos, const scene_t& full_scene) {
    lightbox_t lightbox = make_lightbox(light, pos);
    cut_lightbox(lightbox, full_scene);
    return lightbox;
}


LightStack::LightStack(const scene_t& scene, const lights_t& lights) {
    for (const auto& light : lights) {
        push(light.second, light.first, scene);
    }
}


void LightStack::push(const PointLight* light, const vertex_t& pos, const scene_t& scene) {
    lightboxes.push_back(make_lightbox(light, pos, scene));
}


bool LightStack::push(const vertex_t& pos, const scene_t& scene) {
    if (!stack.empty()) {
        const PointLight* light = stack.back();
        stack.pop_back();
        lightboxes.push_back(make_lightbox(light, pos, scene));
        return true;
    }
    return false;
}


const PointLight* LightStack::pop_nearest(const vertex_t& pos, fcoord_t max) {
    fcoord_t min_dist = max * max;
    std::list<lightbox_t>::iterator nearest = lightboxes.end();
    std::list<lightbox_t>::iterator it = lightboxes.begin();
    for (; it != lightboxes.end(); ++it) {
        const fcoord_t dist = it->rad.pos.from(pos).sqlen();
        if (dist <= min_dist) {
            min_dist = dist;
            nearest = it;
        }
    }

    if (nearest != lightboxes.end()) {
        const PointLight* light = nearest->light;
        stack.push_back(light);
        lightboxes.erase(nearest);
        return light;
    }

    return nullptr;
}


const std::list<lightbox_t>& LightStack::get() const {
    return lightboxes;
}


size_t LightStack::size() const {
    return stack.size();
}


vertex_t State::clip(const vertex_t& pos, const vertex_t& off) const {
    ray_t movement{pos.with_z(0.5F), off.with_z(), pos.with_z(0.5F) + off.with_z()};
    box_t movement_box{movement};
    bool intersects = false;
    bool intersects_box = false;
    vertex_t hit{}, nrm{};
    for (const auto& obj : scene) {
        if (obj->box().intersects_xy(movement_box)) {
            intersects_box = true;
            if (obj->intersect(movement, hit, nrm)) {
                intersects = true;
                movement = {movement.pos, hit - movement.pos, hit};
#if USE_LOG_STATS > 0
                LOG("Clip to %.3f/%.3f", movement.dst.x, movement.dst.y);
#endif
            }
        }
    }
    if (!intersects_box) { // sanity check, ends up in void
        return pos;
    } else if (intersects) {
        return hit.with_z(pos.z) + nrm;
    } else {
        return pos + off;
    }
}


State::State(const scene_t& scene, const lights_t& lights, vertex_t raster, vertex_t viewport, vertex_t camera,
             res_t resolution, bool subsample, fcoord_t sensitivity)
    : sampler(resolution, subsample),
      contexts(ZViewport::viewport_spec_t{raster, viewport, camera.z, sampler.resolution_in()}),
      scene(scene),
      lights(lights),
      lightboxes(scene, lights),
      camera(camera),
      sensitivity(sensitivity),
      player(viewport.x / 2.0F, {1.0F, 1.0F, 1.0F}),
      renderer(contexts, sampler),
      light_tracer(renderer.queue()),
      tracer(light_tracer.queue()) {}


bool State::tick(const Renderer::input_t& in) {
    Timer timer;
    auto ctx = contexts.pop();
    ctx->timer.reset();

    // collect or drop a light upon key press
#if USE_LIGHT_PICKUP > 0
    const fcoord_t pickup_dist = ctx->viewport.get_config().raster.x / 2.0F; // half a tile
    if (in.action.action) {
        const PointLight* nearest = lightboxes.pop_nearest(camera, pickup_dist);
        if (nearest != nullptr) {
            if (nearest->get_color() > ones || nearest->get_color() < zeroes) {
                return false; // win/stop when picking up end light
            }
        } else {
            lightboxes.push(camera, scene);
        }
    }
#if USE_LIGHT_PICKUP > 1
    player =
        PointLight(std::min(ctx->viewport.get_config().viewport.x * 0.5F,
                            ctx->viewport.get_config().viewport.x * ((fcoord_t)lightboxes.size() / 20.0F + 0.125F)),
                   {1.0F, 1.0F, 1.0F});
#endif
#endif

    // move camera, viewport, player position
    if (in.action.move) {
#if NO_CLIP
        camera += in.offset;
#else
        camera = clip(camera, in.offset);
#endif
    }
    if (in.action.beam) {
        camera = in.offset.with_z(camera.z);
    }
    ctx->viewport.update(camera);
#if USE_LOG_STATS > 0
    LOG("Move to %.3f/%.3f", camera.x, camera.y);
#endif

    // preprocess scene for each corner separately
    lightbox_t player_box = make_lightbox(&player, ctx->viewport.get_camera(), scene);
    for (size_t n = 0; n < 4; ++n) {
        sub_frame_t& corner = ctx->corners.at(n);
        const UNUSED box_t bounding_box = ctx->viewport.get_viewbox(corner.viewport);

        // collect lights that might contribute
        corner.effective_lights.clear();
        corner.effective_lights.push_back(player_box); // NB: copy to begin
        for (const auto& light : lightboxes.get()) {
#if TRACE_ALL
            ctx->effective_lights.push_back(light.get());
#else
            if (light.box.intersects_xy(bounding_box)) { // could shine into scene (pos + max dist in viewport)
                if (!is_occluded(light.box, player_box.rad.pos, scene)) {
                    corner.effective_lights.push_back(light);
                }
            }
#endif
        }

        // collect objects that might be seen or interact with the lights
        corner.visible_scene.clear();
        corner.effective_scene.clear();
        for (const auto& obj : scene) {
#if TRACE_ALL
            ctx->visible_scene.push_back(obj.get());
            ctx->effective_scene.push_back(obj.get());
#else
            if (obj->box().intersects_xy(bounding_box) && obj->can_intersect(camera) &&
                can_lit(obj.get(), corner.effective_lights) && !is_occluded(obj.get(), player_box.rad.pos, scene)) {
                corner.visible_scene.push_back(obj.get());
            }
            if (can_occlude(obj.get(), corner.effective_lights, bounding_box, scene) ||
                can_occlude(obj.get(), player_box.rad.pos, bounding_box, scene)) {
                corner.effective_scene.push_back(obj.get());
            }
#endif
        }
    }

    // start frame rendering by enqueueing to the object tracer as first processor
    tracer.queue().submit(std::move(ctx));
#if USE_LOG_STATS > 1
    LOG("Update in %4lu ms", timer.measure());
#endif
    return true;
}


Renderer::input_t State::get_input() {
    return renderer.get_input(sensitivity);
}


unsigned State::congestion_hint() {
    const unsigned curr_congestion = tracer.queue().congestion_count() + light_tracer.queue().congestion_count() +
                                     renderer.queue().congestion_count();
    return curr_congestion - std::exchange(congestion_count, curr_congestion);
}


const SlidingAverage<msec_t>& State::duration_hint() {
    return renderer.duration_hint();
}