/*
 * This file is part of vimix - video live mixer
 *
 * **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
**/

#include <string>
#include <sstream>
#include <iomanip>

#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtx/vector_angle.hpp>

#include "ImGuiToolkit.h"

#include "Mixer.h"
#include "defines.h"
#include "Source.h"
#include "SourceCallback.h"
#include "Settings.h"
#include "Decorations.h"
#include "UserInterfaceManager.h"
#include "BoundingBoxVisitor.h"
#include "ActionManager.h"
#include "MixingGroup.h"
#include "Log.h"

#include "MixingView.h"

// internal utility
float sin_quad_texture(float x, float y);
uint textureMixingQuadratic();



MixingView::MixingView() : View(MIXING), limbo_scale_(MIXING_MIN_THRESHOLD)
{
    scene.root()->scale_ = glm::vec3(MIXING_DEFAULT_SCALE, MIXING_DEFAULT_SCALE, 1.0f);
    scene.root()->translation_ = glm::vec3(0.0f, 0.0f, 0.0f);
    // read default settings
    if ( Settings::application.views[mode_].name.empty() )
        // no settings found: store application default
        saveSettings();
    else
        restoreSettings();
    Settings::application.views[mode_].name = "Mixing";

    // Mixing scene background
    limbo_ = new Mesh("mesh/disk.ply");
    limbo_->scale_ = glm::vec3(limbo_scale_, limbo_scale_, 1.f);
    limbo_->shader()->color = glm::vec4( COLOR_LIMBO_CIRCLE, 1.f );
    scene.bg()->attach(limbo_);

    // interactive limbo scaling slider
    limbo_slider_root_ = new Group;
    limbo_slider_root_->translation_ = glm::vec3(0.0f, limbo_scale_, 0.f);
    scene.bg()->attach(limbo_slider_root_);
    limbo_slider_ = new Disk();
    limbo_slider_->translation_ = glm::vec3(0.f, -0.01f, 0.f);
    limbo_slider_->scale_ = glm::vec3(0.1f, 0.1f, 1.f);
    limbo_slider_->color = glm::vec4( COLOR_LIMBO_CIRCLE, 1.f );
    limbo_slider_root_->attach(limbo_slider_);
    limbo_up_ = new Mesh("mesh/triangle_point.ply");
    limbo_up_->scale_ = glm::vec3(0.8f, 0.8f, 1.f);
    limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
    limbo_slider_root_->attach(limbo_up_);
    limbo_down_ = new Mesh("mesh/triangle_point.ply");
    limbo_down_->translation_ = glm::vec3(0.f, -0.02f, 0.f);
    limbo_down_->scale_ = glm::vec3(0.8f, 0.8f, 1.f);
    limbo_down_->rotation_ = glm::vec3(0.f, 0.f, M_PI);
    limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
    limbo_slider_root_->attach(limbo_down_);

    mixingCircle_ = new Mesh("mesh/disk.ply");
    mixingCircle_->shader()->color = glm::vec4( 1.f, 1.f, 1.f, 1.f );
    scene.bg()->attach(mixingCircle_);

    circle_ = new Mesh("mesh/circle.ply");
    circle_->shader()->color = glm::vec4( COLOR_CIRCLE, 1.0f );
    scene.bg()->attach(circle_);

    // Mixing scene foreground

    // button frame (non interactive; no picking detected on Mesh)
    Mesh *tmp = new Mesh("mesh/disk.ply");
    tmp->scale_ = glm::vec3(0.033f, 0.033f, 1.f);
    tmp->translation_ = glm::vec3(0.f, 1.f, 0.f);
    tmp->shader()->color = glm::vec4( COLOR_CIRCLE, 0.9f );
    scene.fg()->attach(tmp);
    // interactive button
    button_white_ = new Disk();
    button_white_->scale_ = glm::vec3(0.026f, 0.026f, 1.f);
    button_white_->translation_ = glm::vec3(0.f, 1.f, 0.f);
    button_white_->color = glm::vec4( 0.85f, 0.85f, 0.85f, 1.0f );
    scene.fg()->attach(button_white_);
    // button frame
    tmp = new Mesh("mesh/disk.ply");
    tmp->scale_ = glm::vec3(0.033f, 0.033f, 1.f);
    tmp->translation_ = glm::vec3(0.f, -1.f, 0.f);
    tmp->shader()->color = glm::vec4( COLOR_CIRCLE, 0.9f );
    scene.fg()->attach(tmp);
    // interactive button
    button_black_ = new Disk();
    button_black_->scale_ = glm::vec3(0.026f, 0.026f, 1.f);
    button_black_->translation_ = glm::vec3(0.f, -1.f, 0.f);
    button_black_->color = glm::vec4( 0.1f, 0.1f, 0.1f, 1.0f );
    scene.fg()->attach(button_black_);
    // moving slider
    slider_root_ = new Group;
    scene.fg()->attach(slider_root_);
    // interactive slider
    slider_ = new Disk();
    slider_->scale_ = glm::vec3(0.08f, 0.08f, 1.f);
    slider_->translation_ = glm::vec3(0.0f, 1.0f, 0.f);
    slider_->color = glm::vec4( COLOR_CIRCLE, 0.9f );
    slider_root_->attach(slider_);
    // dark mask in front
    tmp = new Mesh("mesh/disk.ply");
    tmp->scale_ = glm::vec3(0.075f, 0.075f, 1.f);
    tmp->translation_ = glm::vec3(0.0f, 1.0f, 0.f);
    tmp->shader()->color = glm::vec4( COLOR_SLIDER_CIRCLE, 1.0f );
    slider_root_->attach(tmp);


//    stashCircle_ = new Disk();
//    stashCircle_->scale_ = glm::vec3(0.5f, 0.5f, 1.f);
//    stashCircle_->translation_ = glm::vec3(2.f, -1.0f, 0.f);
//    stashCircle_->color = glm::vec4( COLOR_STASH_CIRCLE, 0.6f );
//    scene.bg()->attach(stashCircle_);

}


void MixingView::draw()
{
    // set texture
    if (mixingCircle_->texture() == 0)
        mixingCircle_->setTexture(textureMixingQuadratic());

    // temporarily force shaders to use opacity blending for rendering icons
    Shader::force_blending_opacity = true;
    // draw scene of this view
    View::draw();
    // restore state
    Shader::force_blending_opacity = false;

    // display popup menu
    if (show_context_menu_ == MENU_SELECTION) {
        ImGui::OpenPopup( "MixingSelectionContextMenu" );
        show_context_menu_ = MENU_NONE;
    }
    if (ImGui::BeginPopup("MixingSelectionContextMenu")) {

        // colored context menu
        ImGui::PushStyleColor(ImGuiCol_Text, ImGuiToolkit::HighlightColor());
        ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.36f, 0.36f, 0.36f, 0.44f));

        // special action of Mixing view: link or unlink
        SourceList selected = Mixer::selection().getCopy();
        if ( Mixer::manager().session()->canlink( selected )) {
            // the selected sources can be linked
            if (ImGui::Selectable( ICON_FA_LINK "  Link" )){
                // assemble a MixingGroup
                Mixer::manager().session()->link(selected, scene.fg() );
                Action::manager().store(std::string("Sources linked."));
                // clear selection and select one of the sources of the group
                Source *cur = Mixer::selection().front();
                Mixer::manager().unsetCurrentSource();
                Mixer::selection().clear();
                Mixer::manager().setCurrentSource( cur );
            }
        }
        else {
            // the selected sources cannot be linked: offer to unlink!
            if (ImGui::Selectable( ICON_FA_UNLINK "  Unlink" )){
                // dismantle MixingGroup(s)
                Mixer::manager().session()->unlink(selected);
                Action::manager().store(std::string("Sources unlinked."));
            }
        }

        ImGui::Separator();

        // manipulation of sources in Mixing view
        if (ImGui::Selectable( ICON_FA_CROSSHAIRS "  Center" )){
            glm::vec2 center = glm::vec2(0.f, 0.f);
            for (SourceList::iterator  it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
                // compute barycenter (1)
                center += glm::vec2((*it)->group(View::MIXING)->translation_);
            }
            // compute barycenter (2)
            center /= Mixer::selection().size();
            for (SourceList::iterator  it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
                (*it)->group(View::MIXING)->translation_ -= glm::vec3(center, 0.f);
                (*it)->touch();
            }
            Action::manager().store(std::string("Selection: Mixing Center"));
        }
        if (ImGui::Selectable( ICON_FA_HAYKAL "  Dispatch" )){
            glm::vec2 center = glm::vec2(0.f, 0.f);
            // distribute with equal angle
            float angle = 0.f;
            for (SourceList::iterator  it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {

                glm::vec2 P = center + glm::rotate(glm::vec2(0.f, 1.2), angle);
                (*it)->group(View::MIXING)->translation_.x = P.x;
                (*it)->group(View::MIXING)->translation_.y = P.y;
                (*it)->touch();
                angle -= glm::two_pi<float>() / float(Mixer::selection().size());
            }
            Action::manager().store(std::string("Selection: Mixing Dispatch"));
        }
        if (ImGui::Selectable( ICON_FA_FAN "  Distribute" )){
            SourceList list;
            glm::vec2 center = glm::vec2(0.f, 0.f);
            for (SourceList::iterator  it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
                list.push_back(*it);
                // compute barycenter (1)
                center += glm::vec2((*it)->group(View::MIXING)->translation_);
            }
            // compute barycenter (2)
            center /= list.size();
            // sort the vector of sources in clockwise order around the center pos_
            list = mixing_sorted( list, center);
            // average distance
            float d = 0.f;
            for (SourceList::iterator it = list.begin(); it != list.end(); ++it) {
                d += glm::distance(glm::vec2((*it)->group(View::MIXING)->translation_), center);
            }
            d /= list.size();
            // distribute with equal angle
            float angle = 0.f;
            for (SourceList::iterator it = list.begin(); it != list.end(); ++it) {
                glm::vec2 P = center + glm::rotate(glm::vec2(0.f, d), angle);
                (*it)->group(View::MIXING)->translation_.x = P.x;
                (*it)->group(View::MIXING)->translation_.y = P.y;
                (*it)->touch();
                angle -= glm::two_pi<float>() / float(list.size());
            }
            Action::manager().store(std::string("Selection: Mixing Distribute in circle"));
        }
        if (ImGui::Selectable( ICON_FA_ELLIPSIS_V "     Align & Distribute" )){
            SourceList list;
            glm::vec2 center = glm::vec2(0.f, 0.f);
            for (SourceList::iterator  it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
                list.push_back(*it);
                // compute barycenter (1)
                center += glm::vec2((*it)->group(View::MIXING)->translation_);
            }
            // compute barycenter (2)
            center /= list.size();
            // distribute with equal angle
            float i = 0.f;
            float sign = -1.f;
            for (SourceList::iterator it = list.begin(); it != list.end(); ++it, sign*=-1.f) {
                (*it)->group(View::MIXING)->translation_.x = center.x;
                (*it)->group(View::MIXING)->translation_.y = center.y + sign * i;
                if (sign<0) i+=0.32f;
                (*it)->touch();
            }
            Action::manager().store(std::string("Selection: Mixing Align & Distribute"));
        }
        if (ImGui::Selectable( ICON_FA_CLOUD_SUN " Expand & hide" )){
            SourceList::iterator  it = Mixer::selection().begin();
            for (; it != Mixer::selection().end(); ++it) {
                (*it)->call( new SetAlpha(0.f) );
            }
            Action::manager().store(std::string("Selection: Mixing Expand & hide"));
        }
        if (ImGui::Selectable( ICON_FA_SUN "  Compress & show" )){
            SourceList::iterator  it = Mixer::selection().begin();
            for (; it != Mixer::selection().end(); ++it) {
                (*it)->call( new SetAlpha(0.999f) );
            }
            Action::manager().store(std::string("Selection: Mixing Compress & show"));
        }
        ImGui::PopStyleColor(2);
        ImGui::EndPopup();
    }
}

void MixingView::resize ( int scale )
{
    float z = CLAMP(0.01f * (float) scale, 0.f, 1.f);
    z *= z;
    z *= MIXING_MAX_SCALE - MIXING_MIN_SCALE;
    z += MIXING_MIN_SCALE;
    scene.root()->scale_.x = z;
    scene.root()->scale_.y = z;

    // Clamp translation to acceptable area
    glm::vec2 res = resolution();
    glm::vec3 border(2.3f * res.x/res.y, 2.3f, 0.f);
    scene.root()->translation_ = glm::clamp(scene.root()->translation_, -border, border);
}

int  MixingView::size ()
{
    float z = (scene.root()->scale_.x - MIXING_MIN_SCALE) / (MIXING_MAX_SCALE - MIXING_MIN_SCALE);
    return (int) ( sqrt(z) * 100.f);
}

void MixingView::centerSource(Source *s)
{
    // calculate screen area visible in the default view
    GlmToolkit::AxisAlignedBoundingBox view_box;
    glm::mat4 modelview = GlmToolkit::transform(scene.root()->translation_, scene.root()->rotation_, scene.root()->scale_);
    view_box.extend( Rendering::manager().unProject(glm::vec2(0.f, Rendering::manager().mainWindow().height()), modelview) );
    view_box.extend( Rendering::manager().unProject(glm::vec2(Rendering::manager().mainWindow().width(), 0.f), modelview) );

    // check if upper-left corner of source is in view box
    glm::vec3 pos_source = s->group(mode_)->translation_ + glm::vec3( -s->group(mode_)->scale_.x, s->group(mode_)->scale_.y, 0.f);
    if ( !view_box.contains(pos_source)) {
        // not visible so shift view
        glm::vec2 screenpoint = glm::vec2(500.f, 20.f) * Rendering::manager().mainWindow().dpiScale();
        glm::vec3 pos_to = Rendering::manager().unProject(screenpoint, scene.root()->transform_);
        glm::vec4 pos_delta = glm::vec4(pos_to.x, pos_to.y, 0.f, 0.f) - glm::vec4(pos_source.x, pos_source.y, 0.f, 0.f);
        pos_delta = scene.root()->transform_ * pos_delta;
        scene.root()->translation_ += glm::vec3(pos_delta);
    }
}

void MixingView::update(float dt)
{
    View::update(dt);

//    // always update the mixinggroups
//    for (auto g = groups_.begin(); g != groups_.end(); g++)
//        (*g)->update(dt);

    // a more complete update is requested
    // for mixing, this means restore position of the fading slider
    if (View::need_deep_update_ > 0) {

        //
        // Set limbo scale according to session
        //
        const float p = CLAMP(Mixer::manager().session()->activationThreshold(), MIXING_MIN_THRESHOLD, MIXING_MAX_THRESHOLD);
        limbo_slider_root_->translation_.y = p;
        limbo_->scale_ = glm::vec3(p, p, 1.f);

        //
        // prevent invalid scaling
        //
        float s = CLAMP(scene.root()->scale_.x, MIXING_MIN_SCALE, MIXING_MAX_SCALE);
        scene.root()->scale_.x = s;
        scene.root()->scale_.y = s;
    }

    // the current view is the mixing view
    if (Mixer::manager().view() == this ){

        //
        // Set slider to match the actual fading of the session
        //
        float f = Mixer::manager().session()->fading();

        // reverse calculate angle from fading & move slider
        slider_root_->rotation_.z  = SIGN(-slider_root_->rotation_.z) * asin(f) * -2.f;

        // visual feedback on mixing circle
        f = 1.f - f;
        mixingCircle_->shader()->color = glm::vec4(f, f, f, 1.f);

        // update the selection overlay
        updateSelectionOverlay();
    }

}


std::pair<Node *, glm::vec2> MixingView::pick(glm::vec2 P)
{
    // get picking from generic View
    std::pair<Node *, glm::vec2> pick = View::pick(P);

    // deal with internal interactive objects
    if ( pick.first == button_white_ || pick.first == button_black_ ) {

        // animate clic
        pick.first->update_callbacks_.push_back(new BounceScaleCallback(0.3f));

        // animated fading in session
        if (pick.first == button_white_)
            Mixer::manager().session()->setFadingTarget(0.f, 500.f);
        else
            Mixer::manager().session()->setFadingTarget(1.f, 500.f);

    }
    else if ( overlay_selection_icon_ != nullptr && pick.first == overlay_selection_icon_ ) {

        openContextMenu(MENU_SELECTION);
    }
    else {
        // get if a source was picked
        Source *s = Mixer::manager().findSource(pick.first);
        if (s != nullptr) {
            // pick on the lock icon; unlock source
            if ( UserInterface::manager().ctrlModifier() && pick.first == s->lock_) {
                lock(s, false);
                pick = { s->locker_, pick.second };
//                pick = { nullptr, glm::vec2(0.f) };
            }
            // pick on the open lock icon; lock source and cancel pick
            else if ( UserInterface::manager().ctrlModifier() && pick.first == s->unlock_ ) {
                lock(s, true);
                pick = { nullptr, glm::vec2(0.f) };
            }
            // pick a locked source ; cancel pick
            else if ( !UserInterface::manager().ctrlModifier() && s->locked() ) {
                pick = { nullptr, glm::vec2(0.f) };
            }
            // pick the symbol: ask to show editor
            else if ( pick.first == s->symbol_ ) {
                UserInterface::manager().showSourceEditor(s);
            }
            // pick on the mixing group rotation icon
            else if ( pick.first == s->rotation_mixingroup_ ) {
                if (UserInterface::manager().shiftModifier())
                    s->mixinggroup_->setAction( MixingGroup::ACTION_GRAB_ONE );
                else
                    s->mixinggroup_->setAction( MixingGroup::ACTION_ROTATE_ALL );
            }
            // pick source of a mixing group
            else if ( s->mixinggroup_ != nullptr ) {
                if (UserInterface::manager().ctrlModifier()) {
                    SourceList linked = s->mixinggroup_->getCopy();
                    linked.remove(s);
                    if (linked.size() > 0) {
                        // avoid de-selection of current source if in a mixing group
                        if (Mixer::selection().contains(s) && Mixer::selection().size() < 2)
                            Mixer::selection().clear();
                        // instead, select all sources linked
                        if (Mixer::selection().empty())
                            Mixer::selection().add(linked);
                        // the source will be re-selected in UserInterface::handleMouse()
                    }
                }
                else if (UserInterface::manager().shiftModifier())
                    s->mixinggroup_->setAction( MixingGroup::ACTION_GRAB_ONE );
                else
                    s->mixinggroup_->setAction( MixingGroup::ACTION_GRAB_ALL );
            }
        }
    }

    return pick;
}



View::Cursor MixingView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pair<Node *, glm::vec2> pick)
{
    View::Cursor ret = Cursor();
    ret.type = Cursor_ResizeAll;

    // unproject
    glm::vec3 gl_Position_from = Rendering::manager().unProject(from, scene.root()->transform_);
    glm::vec3 gl_Position_to   = Rendering::manager().unProject(to, scene.root()->transform_);

    // No source is given
    if (!s) {

        // if interaction with slider
        if (pick.first == slider_) {

            // apply rotation to match angle with mouse cursor
            float angle = glm::orientedAngle( glm::normalize(glm::vec2(0.f, 1.0)), glm::normalize(glm::vec2(gl_Position_to)));

            // snap on 0 and PI angles
            if ( ABS_DIFF(angle, 0.f) < 0.05)
                angle = 0.f;
            else if ( ABS_DIFF(angle, M_PI) < 0.05)
                angle = M_PI;

            // animate slider (rotation angle on its parent)
            slider_root_->rotation_.z = angle;

            // calculate fading from angle
            float f = sin( ABS(angle) * 0.5f);
            Mixer::manager().session()->setFadingTarget(f);

            // cursor feedback
            slider_->color = glm::vec4( COLOR_CIRCLE_OVER, 0.9f );
            std::ostringstream info;
            info << ICON_FA_ADJUST << " Output fading " << 100 - int(f * 100.0) << " %";
            return Cursor(Cursor_Hand, info.str() );
        }
        else if (pick.first == limbo_slider_) {

            // move slider scaling limbo area
            const float p = CLAMP(gl_Position_to.y, MIXING_MIN_THRESHOLD, MIXING_MAX_THRESHOLD);
            limbo_slider_root_->translation_.y = p;
            limbo_->scale_ = glm::vec3(p, p, 1.f);
            Mixer::manager().session()->setActivationThreshold(p);

            // color change of arrow indicators
            limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, p < MIXING_MAX_THRESHOLD ? 0.15f : 0.01f );
            limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, p > MIXING_MIN_THRESHOLD ? 0.15f : 0.01f );

            std::ostringstream info;
            info << ICON_FA_SNOWFLAKE " Deactivation limit";
            return Cursor(Cursor_Hand, info.str() );
        }

        // nothing to do
        return Cursor();
    }
    //
    // Interaction with source
    //
    // compute delta translation
    s->group(mode_)->translation_ = s->stored_status_->translation_ + gl_Position_to - gl_Position_from;

    // manage mixing group
    if (s->mixinggroup_ != nullptr  ) {
        // inform mixing groups to follow the current source
        if (Source::isCurrent(s) && s->mixinggroup_->action() > MixingGroup::ACTION_UPDATE) {
            s->mixinggroup_->follow(s);
            // special cursor for rotation
            if (s->mixinggroup_->action() == MixingGroup::ACTION_ROTATE_ALL)
                ret.type = Cursor_Hand;
        }
        else
            s->mixingGroup()->setAction(MixingGroup::ACTION_NONE);
    }

    // request update
    s->touch();

//    // trying to enter stash
//    if ( glm::distance( glm::vec2(s->group(mode_)->translation_), glm::vec2(stashCircle_->translation_)) < stashCircle_->scale_.x) {

//        // refuse to put an active source in stash
//        if (s->active())
//            s->group(mode_)->translation_ = s->stored_status_->translation_;
//        else {
//            Mixer::manager().conceal(s);
//            s->group(mode_)->scale_ = glm::vec3(MIXING_ICON_SCALE) - glm::vec3(0.1f, 0.1f, 0.f);
//        }
//    }
//    else if ( Mixer::manager().concealed(s) ) {
//        Mixer::manager().uncover(s);
//        s->group(mode_)->scale_ = glm::vec3(MIXING_ICON_SCALE);
//    }


    std::ostringstream info;
    if (s->active()) {
        info << "Alpha " << std::fixed << std::setprecision(3) << s->blendingShader()->color.a << "  ";
        info << ( (s->blendingShader()->color.a > 0.f) ? ICON_FA_EYE : ICON_FA_EYE_SLASH);
    }
    else
        info << "Inactive  " << ICON_FA_SNOWFLAKE;

    // store action in history
    current_action_ = s->name() + ": " + info.str();

    // update cursor
    ret.info = info.str();
    return ret;
}

void MixingView::terminate(bool force)
{
    View::terminate(force);

    // terminate all mixing group actions
    for (auto g = Mixer::manager().session()->beginMixingGroup();
         g != Mixer::manager().session()->endMixingGroup(); ++g)
        (*g)->setAction( MixingGroup::ACTION_FINISH );

}

View::Cursor MixingView::over (glm::vec2 pos)
{
    View::Cursor ret = Cursor();
    std::pair<Node *, glm::vec2> pick = View::pick(pos);

    // deal with internal interactive objects
    if ( pick.first == slider_ ) {
        slider_->color = glm::vec4( COLOR_CIRCLE_OVER, 0.9f );
        ret.type = Cursor_Hand;
    }
    else
        slider_->color = glm::vec4( COLOR_CIRCLE, 0.9f );

    if ( pick.first == limbo_slider_ ) {
        limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, limbo_slider_root_->translation_.y < MIXING_MAX_THRESHOLD ? 0.1f : 0.01f );
        limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, limbo_slider_root_->translation_.y > MIXING_MIN_THRESHOLD ? 0.1f : 0.01f );
        ret.type = Cursor_Hand;
    }
    else {
        limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
        limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
    }


    return ret;
}

void MixingView::arrow (glm::vec2 movement)
{
    static float accumulator = 0.f;
    accumulator += dt_;

    glm::vec3 gl_Position_from = Rendering::manager().unProject(glm::vec2(0.f), scene.root()->transform_);
    glm::vec3 gl_Position_to   = Rendering::manager().unProject(movement, scene.root()->transform_);
    glm::vec3 gl_delta = gl_Position_to - gl_Position_from;

    bool first = true;
    glm::vec3 delta_translation(0.f);
    for (auto it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {

        // individual move with SHIFT
        if ( !Source::isCurrent(*it) && UserInterface::manager().shiftModifier() )
            continue;

        Group *sourceNode = (*it)->group(mode_);
        glm::vec3 dest_translation(0.f);

        if (first) {
            // dest starts at current
            dest_translation = sourceNode->translation_;

            // + ALT : discrete displacement
            if (UserInterface::manager().altModifier()) {
                if (accumulator > 100.f) {
                    dest_translation += glm::sign(gl_delta) * 0.1f;
                    dest_translation.x = ROUND(dest_translation.x, 10.f);
                    dest_translation.y = ROUND(dest_translation.y, 10.f);
                    accumulator = 0.f;
                }
                else
                    break;
            }
            else {
                // normal case: dest += delta
                dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR * dt_;
                accumulator = 0.f;
            }

            // store action in history
            std::ostringstream info;
            if ((*it)->active()) {
                info << "Alpha " << std::fixed << std::setprecision(3) << (*it)->blendingShader()->color.a << "  ";
                info << ( ((*it)->blendingShader()->color.a > 0.f) ? ICON_FA_EYE : ICON_FA_EYE_SLASH);
            }
            else
                info << "Inactive  " << ICON_FA_SNOWFLAKE;
            current_action_ = (*it)->name() + ": " + info.str();

            // delta for others to follow
            delta_translation = dest_translation - sourceNode->translation_;
        }
        else {
            // dest = current + delta from first
            dest_translation = sourceNode->translation_ + delta_translation;
        }

        // apply & request update
        sourceNode->translation_ = dest_translation;
        (*it)->touch();

        first = false;
    }

}

void MixingView::setAlpha(Source *s)
{
    if (!s)
        return;

    // move the mixing node of the source
    Group *sourceNode = s->group(mode_);
    glm::vec2 mix_pos(sourceNode->translation_);

    for(NodeSet::iterator it = scene.ws()->begin(); it != scene.ws()->end(); ++it) {
        // avoid superposing icons: distribute equally
        if ( glm::distance(glm::vec2((*it)->translation_), mix_pos) < DELTA_ALPHA) {
            mix_pos += glm::vec2(-0.03f, 0.03f);
        }
    }

    sourceNode->translation_.x = mix_pos.x;
    sourceNode->translation_.y = mix_pos.y;

    // request update
    s->touch();
}


void MixingView::updateSelectionOverlay()
{
    View::updateSelectionOverlay();

    if (overlay_selection_->visible_) {
        // calculate bbox on selection
        GlmToolkit::AxisAlignedBoundingBox selection_box = BoundingBoxVisitor::AABB(Mixer::selection().getCopy(), this);
        overlay_selection_->scale_ = selection_box.scale();
        overlay_selection_->translation_ = selection_box.center();

        // slightly extend the boundary of the selection
        overlay_selection_frame_->scale_ = glm::vec3(1.f) + glm::vec3(0.01f, 0.01f, 1.f) / overlay_selection_->scale_;
    }
}

#define CIRCLE_PIXELS 64
#define CIRCLE_PIXEL_RADIUS 1024.f
//#define CIRCLE_PIXELS 256
//#define CIRCLE_PIXEL_RADIUS 16384.0
//#define CIRCLE_PIXELS 1024
//#define CIRCLE_PIXEL_RADIUS 262144.0

float sin_quad_texture(float x, float y) {
//    return 0.5f + 0.5f * cos( M_PI * CLAMP( ( ( x * x ) + ( y * y ) ) / CIRCLE_PIXEL_RADIUS, 0.f, 1.f ) );
    float D = sqrt( ( x * x )/ CIRCLE_PIXEL_RADIUS + ( y * y )/ CIRCLE_PIXEL_RADIUS );
    return 0.5f + 0.5f * cos( M_PI * CLAMP( D * sqrt(D), 0.f, 1.f ) );
}

uint textureMixingQuadratic()
{
    static GLuint texid = 0;
    if (texid == 0) {
        // generate the texture with alpha exactly as computed for sources
        GLubyte matrix[CIRCLE_PIXELS*CIRCLE_PIXELS * 4];
        int l = -CIRCLE_PIXELS / 2 + 1;

        for (int i = 0; i < CIRCLE_PIXELS ; ++i) {
            int c = -CIRCLE_PIXELS / 2 + 1;
            for (int j = 0; j < CIRCLE_PIXELS ; ++j) {
                // distance to the center
                GLfloat distance = sin_quad_texture( (float) c , (float) l  );
//                distance = 1.f - (GLfloat) ((c * c) + (l * l)) / CIRCLE_PIXEL_RADIUS;  // quadratic
//                distance = 1.f - (GLfloat) sqrt( (GLfloat) ((c * c) + (l * l))) / (GLfloat) sqrt(CIRCLE_PIXEL_RADIUS); // linear

                // transparency
                GLfloat alpha = 255.f * CLAMP( distance , 0.f, 1.f);
                GLubyte A = static_cast<GLubyte>(alpha);

                // luminance adjustment
                GLfloat luminance = 255.f * CLAMP( 0.2f + 0.75f * distance, 0.f, 1.f);
                GLubyte L = static_cast<GLubyte>(luminance);

                // fill pixel RGBA
                matrix[ i * CIRCLE_PIXELS * 4 + j * 4 + 0 ] = L;
                matrix[ i * CIRCLE_PIXELS * 4 + j * 4 + 1 ] = L;
                matrix[ i * CIRCLE_PIXELS * 4 + j * 4 + 2 ] = L;
                matrix[ i * CIRCLE_PIXELS * 4 + j * 4 + 3 ] = A;

                ++c;
            }
            ++l;
        }
        // setup texture
        glGenTextures(1, &texid);
        glBindTexture(GL_TEXTURE_2D, texid);
        glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, CIRCLE_PIXELS, CIRCLE_PIXELS);
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, CIRCLE_PIXELS, CIRCLE_PIXELS, GL_BGRA, GL_UNSIGNED_BYTE, matrix);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glBindTexture(GL_TEXTURE_2D, 0);
    }
    return texid;
}
