/**
 *
 * RenderPipeline
 *
 * Copyright (c) 2014-2016 tobspr <tobias.springer1@gmail.com>
 *
 * 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.
 *
 */


#include "internal_light_manager.h"

#include <algorithm>

NotifyCategoryDef(lightmgr, "");


/**
 * @brief Constructs the light manager
 * @details This constructs the light manager, initializing the light and shadow
 *   storage. You should set a command list and shadow manager before calling
 *   InternalLightManager::update. s
 */
InternalLightManager::InternalLightManager() {
    _shadow_update_distance = 100.0f;
    _cmd_list = NULL;
    _shadow_manager = NULL;
}

/**
 * @brief Adds a new light.
 * @details This adds a new light to the list of lights. This will throw an
 *   error and return if the light is already attached. You may only call
 *   this after the ShadowManager was already set.
 *
 *   While the light is attached, the light manager keeps a reference to it, so
 *   the light does not get destructed.
 *
 *   This also setups the shadows on the light, in case shadows are enabled.
 *   While a light is attached, you can not change whether it casts shadows or not.
 *   To do so, detach the light, change the setting, and re-add the light.
 *
 *   In case no free light slot is available, an error will be printed and no
 *   action will be performed.
 *
 *   If no shadow manager was set, an assertion will be triggered.
 *
 * @param light The light to add.
 */
void InternalLightManager::add_light(PT(RPLight) light) {
    nassertv(_shadow_manager != NULL); // Shadow manager not set yet!

    // Don't attach the light in case its already attached
    if (light->has_slot()) {
        lightmgr_cat.error() << "could not add light because it already is attached! "
                             << "Detach the light first, then try it again." << endl;
        return;
    }

    // Find a free slot
    size_t slot;
    if (!_lights.find_slot(slot)) {
        lightmgr_cat.error() << "Light limit of " << MAX_LIGHT_COUNT << " reached, "
                             << "all light slots used!" << endl;
        return;
    }

    // Reference the light because we store it, to avoid it getting destructed
    // on the python side while we still work with it. The reference will be
    // removed when the light gets detached.
    light->ref();

    // Reserve the slot
    light->assign_slot(slot);
    _lights.reserve_slot(slot, light);

    // Setup the shadows in case the light uses them
    if (light->get_casts_shadows()) {
        setup_shadows(light);
    }

    // Store the light on the gpu, to make sure the GPU directly knows about it.
    // We could wait until the next update cycle, but then we might be one frame
    // too late already.
    gpu_update_light(light);
}

/**
 * @brief Internal method to setup shadows for a light
 * @details This method gets called by the InternalLightManager::add_light method
 *   to setup a lights shadow sources, in case shadows are enabled on that light.
 *
 *   It finds a slot for all shadow sources of the ilhgt, and inits the shadow
 *   sources as well. If no slot could be found, an error is printed an nothing
 *   happens.
 *
 * @param light The light to init the shadow sources for
 */
void InternalLightManager::setup_shadows(RPLight* light) {

    // Init the lights shadow sources, and also call update once to make sure
    // the sources are properly initialized
    light->init_shadow_sources();
    light->update_shadow_sources();

    // Find consecutive slots, this is important for PointLights so we can just
    // store the first index of the source, and get the other slots by doing
    // first_index + 1, +2 and so on.
    size_t base_slot;
    size_t num_sources = light->get_num_shadow_sources();
    if (!_shadow_sources.find_consecutive_slots(base_slot, num_sources)) {
        lightmgr_cat.error() << "Failed to find slot for shadow sources! "
                             << "Shadow-Source limit of " << MAX_SHADOW_SOURCES
                             << " reached!" << endl;
        return;
    }

    // Init all sources
    for (int i = 0; i < num_sources; ++i) {
        ShadowSource* source = light->get_shadow_source(i);

        // Set the source as dirty, so it gets updated in the beginning
        source->set_needs_update(true);

        // Assign the slot to the source. Since we got consecutive slots, we can
        // just do base_slot + N.
        size_t slot = base_slot + i;
        _shadow_sources.reserve_slot(slot, source);
        source->set_slot(slot);
    }
}

/**
 * @brief Removes a light
 * @details This detaches a light. This prevents it from being rendered, and also
 *   cleans up all resources used by that light. If no reference is kept on the
 *   python side, the light will also get destructed.
 *
 *   If the light was not previously attached with InternalLightManager::add_light,
 *   an error will be triggered and nothing happens.
 *
 *   In case the light was set to cast shadows, all shadow sources are cleaned
 *   up, and their regions in the shadow atlas are freed.
 *
 *   All resources used by the light in the light and shadow storage are also
 *   cleaned up, by emitting cleanup GPUCommands.
 *
 *   If no shadow manager was set, an assertion will be triggered.
 *
 * @param light [description]
 */
void InternalLightManager::remove_light(PT(RPLight) light) {
    nassertv(_shadow_manager != NULL);

    if (!light->has_slot()) {
        lightmgr_cat.error() << "Could not detach light, light was not attached!" << endl;
        return;
    }

    // Free the lights slot in the light storage
    _lights.free_slot(light->get_slot());

    // Tell the GPU we no longer need the lights data
    gpu_remove_light(light);

    // Mark the light as detached. After this call, we can not call get_slot
    // anymore, so its important we do this after we unregistered the light
    // from everywhere.
    light->remove_slot();

    // Clear shadow related stuff, in case the light casts shadows
    if (light->get_casts_shadows()) {

        // Free the slots of all sources, and also unregister their regions from
        // the shadow atlas.
        for (size_t i = 0; i < light->get_num_shadow_sources(); ++i) {
            ShadowSource* source = light->get_shadow_source(i);
            if (source->has_slot()) {
                _shadow_sources.free_slot(source->get_slot());
            }
            if (source->has_region()) {
                _shadow_manager->get_atlas()->free_region(source->get_region());
                source->clear_region();
            }
        }

        // Remove all sources of the light by emitting a consecutive remove command
        gpu_remove_consecutive_sources(light->get_shadow_source(0),
                                       light->get_num_shadow_sources());

        // Finally remove all shadow sources. This is important in case the light
        // will be re-attached. Otherwise an assertion will get triggered.
        light->clear_shadow_sources();
    }

    // Since we referenced the light when we stored it, we have to decrease
    // the reference now. In case no reference was kept on the python side,
    // the light will get destructed soon.
    light->unref();
}

/**
 * @brief Internal method to remove consecutive sources from the GPU.
 * @details This emits a GPUCommand to consecutively remove shadow sources from
 *   the GPU. This is called when a light gets removed, to free the space its
 *   shadow sources took. Its not really required, because as long as the light
 *   is not used, there is no reference to the sources. However, it can't hurt to
 *   cleanup the memory.
 *
 *   All sources starting at first_source->get_slot() until
 *   first_source->get_slot() + num_sources will get cleaned up.
 *
 * @param first_source First source of the light
 * @param num_sources Amount of consecutive sources to clear
 */
void InternalLightManager::gpu_remove_consecutive_sources(ShadowSource *first_source,
                                                          size_t num_sources) {
    nassertv(_cmd_list != NULL);        // No command list set yet
    nassertv(first_source->has_slot()); // Source has no slot!
    GPUCommand cmd_remove(GPUCommand::CMD_remove_sources);
    cmd_remove.push_int(first_source->get_slot());
    cmd_remove.push_int(num_sources);
    _cmd_list->add_command(cmd_remove);
}

/**
 * @brief Internal method to remove a light from the GPU.
 * @details This emits a GPUCommand to clear a lights data. This sets the data
 *   to all zeros, marking that no light is stored anymore.
 *
 *   This throws an assertion in case the light is not currently attached. Be
 *   sure to call this before detaching the light.
 *
 * @param light The light to remove, must be attached.
 */
void InternalLightManager::gpu_remove_light(RPLight* light) {
    nassertv(_cmd_list != NULL);  // No command list set yet
    nassertv(light->has_slot());  // Light has no slot!
    GPUCommand cmd_remove(GPUCommand::CMD_remove_light);
    cmd_remove.push_int(light->get_slot());
    _cmd_list->add_command(cmd_remove);
}

/**
 * @brief Updates a lights data on the GPU
 * @details This method emits a GPUCommand to update a lights data. This can
 *   be used to initially store the lights data, or to update the data whenever
 *   the light changed.
 *
 *   This throws an assertion in case the light is not currently attached. Be
 *   sure to call this after attaching the light.
 *
 * @param light The light to update
 */
void InternalLightManager::gpu_update_light(RPLight* light) {
    nassertv(_cmd_list != NULL);  // No command list set yet
    nassertv(light->has_slot());  // Light has no slot!
    GPUCommand cmd_update(GPUCommand::CMD_store_light);
    cmd_update.push_int(light->get_slot());
    light->write_to_command(cmd_update);
    light->set_needs_update(false);
    _cmd_list->add_command(cmd_update);
}

/**
 * @brief Updates a shadow source data on the GPU
 * @details This emits a GPUCommand to update a given shadow source, storing all
 *   data of the source on the GPU. This can also be used to initially store a
 *   ShadowSource, since all data will be overridden.
 *
 *   This throws an assertion if the source has no slot yet.
 *
 * @param source The source to update
 */
void InternalLightManager::gpu_update_source(ShadowSource* source) {
    nassertv(_cmd_list != NULL);  // No command list set yet
    nassertv(source->has_slot()); // Source has no slot!
    GPUCommand cmd_update(GPUCommand::CMD_store_source);
    cmd_update.push_int(source->get_slot());
    source->write_to_command(cmd_update);
    _cmd_list->add_command(cmd_update);
}

/**
 * @brief Internal method to update all lights
 * @details This is called by the main update method, and iterates over the list
 *   of lights. If a light is marked as dirty, it will recieve an update of its
 *   data and its shadow sources.
 */
void InternalLightManager::update_lights() {
    for (auto iter = _lights.begin(); iter != _lights.end(); ++iter) {
        RPLight* light = *iter;
        if (light && light->get_needs_update()) {
            if (light->get_casts_shadows()) {
                light->update_shadow_sources();
            }
            gpu_update_light(light);
        }
    }
}

/**
 * @brief Compares shadow sources by their priority
 * @details Returns if a has a greater priority than b. This depends on the
 *   resolution of the source, and also if the source has a region or not.
 *   This method can be passed to std::sort.
 *
 * @param a First source
 * @param b Second source
 *
 * @return true if a is more important than b, else false
 */
bool InternalLightManager::compare_shadow_sources(const ShadowSource* a, const ShadowSource* b) const {

    // Make sure that sources which already have a region (but maybe outdated)
    // come after sources which have no region at all.
    if (a->has_region() != b->has_region()) {
        return b->has_region();
    }

    // Compare sources based on their distance to the camera
    float dist_a = (_camera_pos - a->get_bounds().get_center()).length_squared();
    float dist_b = (_camera_pos - a->get_bounds().get_center()).length_squared();

    // XXX: Should also compare based on source size, so that huge sources recieve
    // more updates

    return dist_b > dist_a;
}

/**
 * @brief Internal method to update all shadow sources
 * @details This updates all shadow sources which are marked dirty. It will sort
 *   the list of all dirty shadow sources by their resolution, take the first
 *   n entries, and update them. The amount of sources processed depends on the
 *   max_updates of the ShadowManager.
 */
void InternalLightManager::update_shadow_sources() {

    // Find all dirty shadow sources and make a list of them
    vector<ShadowSource*> sources_to_update;
     for (auto iter = _shadow_sources.begin(); iter != _shadow_sources.end(); ++iter) {
        ShadowSource* source = *iter;
        if (source) {
            const BoundingSphere& bounds = source->get_bounds();

            // Check if source is in range
            float distance_to_camera = (_camera_pos - bounds.get_center()).length() - bounds.get_radius();
            if (distance_to_camera < _shadow_update_distance) {
                if (source->get_needs_update()) {
                    sources_to_update.push_back(source);
                }
            } else {

                // Free regions of sources which are out of the update radius,
                // to make space for other regions
                if (source->has_region()) {
                    _shadow_manager->get_atlas()->free_region(source->get_region());
                    source->clear_region();
                }
            }
        }

    }

    // Sort the sources based on their importance, so that sources with a bigger
    // priority come first. This helps to get a better packing on the shadow atlas.
    // However, we also need to prioritize sources which have no current region,
    // because no shadows are worse than outdated-shadows.
    std::sort(sources_to_update.begin(), sources_to_update.end(), [this](const ShadowSource* a, const ShadowSource* b) {
        return this->compare_shadow_sources(a, b);
    });

    // Get a handle to the atlas, will be frequently used
    ShadowAtlas *atlas = _shadow_manager->get_atlas();

    // Free the regions of all sources which will get updated. We have to take into
    // account that only a limited amount of sources can get updated per frame.
    size_t update_slots = min(sources_to_update.size(),
                              _shadow_manager->get_num_update_slots_left());
    for(size_t i = 0; i < update_slots; ++i) {
        if (sources_to_update[i]->has_region()) {
           atlas->free_region(sources_to_update[i]->get_region());
        }
    }

    // Find an atlas spot for all regions which are supposed to get an update
    for (size_t i = 0; i < update_slots; ++i) {
        ShadowSource *source = sources_to_update[i];

        if(!_shadow_manager->add_update(source)) {
            // In case the ShadowManager lied about the number of updates left
            lightmgr_cat.error() << "ShadowManager ensured update slot, but slot is taken!" << endl;
            break;
        }

        // We have an update slot, and are guaranteed to get updated as soon
        // as possible, so we can start getting a new atlas position.
        size_t region_size = atlas->get_required_tiles(source->get_resolution());
        LVecBase4i new_region = atlas->find_and_reserve_region(region_size, region_size);
        LVecBase4f new_uv_region = atlas->region_to_uv(new_region);
        source->set_region(new_region, new_uv_region);

        // Mark the source as updated
        source->set_needs_update(false);
        gpu_update_source(source);
    }
}

/**
 * @brief Main update method
 * @details This is the main update method of the InternalLightManager. It
 *   processes all lights and shadow sources, updates them, and notifies the
 *   GPU about it. This should be called on a per-frame basis.
 *
 *   If the InternalLightManager was not initialized yet, an assertion is thrown.
 */
void InternalLightManager::update() {
    nassertv(_shadow_manager != NULL); // Not initialized yet!
    nassertv(_cmd_list != NULL);       // Not initialized yet!

    update_lights();
    update_shadow_sources();
}
