// License: Apache 2.0. See LICENSE file in root directory.
// Copyright(c) 2020 Intel Corporation. All Rights Reserved.

#include "measurement.h"
#include "ux-window.h"
#include <rs-config.h>
#include <librealsense2/hpp/rs_export.hpp>

#include "opengl3.h"


using namespace rs2;

void measurement::enable() {
    measurement_active = true;
    config_file::instance().set(configurations::viewer::is_measuring, true);
}
void measurement::disable() {
    state.points.clear();
    state.edges.clear();
    state.polygons.clear();
    measurement_active = false;
    config_file::instance().set(configurations::viewer::is_measuring, false);
}
bool measurement::is_enabled() const { return measurement_active; }

bool measurement::display_mouse_picked_tooltip() const{
    return !(measurement_active && state.points.size() == 1) && !measurement_point_hovered;
}

bool measurement::manipulating() const { return dragging_measurement_point; }

std::vector<int> measurement_state::find_path(int from, int to)
{
    std::map<int, int> parents;
    std::deque<int> q;

    q.push_back(from);
    for (int i = 0; i < points.size(); i++)
    {
        parents[i] = -1;
    }

    while (q.size())
    {
        auto vert = q.front();
        q.pop_front();

        for (auto&& pair : edges)
        {
            int next = -1;
            if (pair.first == vert) next = pair.second;
            if (pair.second == vert) next = pair.first;
            if (next >= 0 && parents[next] == -1)
            {
                parents[next] = vert;
                q.push_back(next);
            }
        }
    }

    std::vector<int> path;
    parents[from] = -1;

    if (parents[to] != -1)
    {
        auto p = to;
        while (p != -1)
        {
            path.push_back(p);
            p = parents[p];
        }
    }

    return path;
}

void measurement::add_point(interest_point p)
{
    auto shift = ImGui::IsKeyDown(GLFW_KEY_LEFT_SHIFT) || ImGui::IsKeyDown(GLFW_KEY_RIGHT_SHIFT);

    if (is_enabled())
    {
        commit_state();

        if (state.points.size() >= 2 && !shift)
        {
            state.points.clear();
            state.edges.clear();
            state.polygons.clear();
        }

        int last = int(state.points.size());
        if (current_hovered_point == -1 ||
            current_hovered_point >= state.points.size())
        {
            state.points.push_back(p);
        }
        else
        {
            last = current_hovered_point;
        }

        int prev = last - 1;
        if (last_hovered_point >= 0 && last_hovered_point < state.points.size())
            prev = last_hovered_point;

        if (state.edges.size())
        {
            auto path = state.find_path(prev, last);
            if (path.size())
            {
                state.polygons.push_back(path);

                std::vector<float3> poly;
                for (auto&& idx : path) poly.push_back(state.points[idx].pos);

                auto area = calculate_area(poly);
                log_function( rsutils::string::from() << "Measured area of " << area_to_string(area));
            }
        }

        if (state.points.size() >= 2)
        {
            auto dist = state.points[last].pos - state.points[prev].pos;
            state.edges.push_back(std::make_pair(last, prev));
            log_function( rsutils::string::from() << "Measured distance of " << length_to_string(dist.length()));
        }

        last_hovered_point = int(state.points.size() - 1);

        commit_state();
    }
}

std::string measurement::area_to_string(float area)
{
    return rsutils::string::from() << std::setprecision(2) << area << " m";
}

std::string measurement::length_to_string(float distance)
{
    std::string label;
    if (is_metric())
    {
        if (distance < 0.01f)
        {
            label = rsutils::string::from() << std::setprecision(3) << distance * 1000.f << " mm";
        } else if (distance < 1.f) {
            label = rsutils::string::from() << std::setprecision(3) << distance * 100.f << " cm";
        } else {
            label = rsutils::string::from() << std::setprecision(3) << distance << " m";
        }
    } else
    {
        if (distance < 0.0254f)
        {
            label = rsutils::string::from() << std::setprecision(3) << distance * 1000.f << " mm";
        } else if (distance < 0.3048f) {
            label = rsutils::string::from() << std::setprecision(3) << distance / 0.0254 << " in";
        } else if (distance < 0.9144) {
            label = rsutils::string::from() << std::setprecision(3) << distance / 0.3048f << " ft";
        } else {
            label = rsutils::string::from() << std::setprecision(3) << distance / 0.9144 << " yd";
        }
    }
    return label;
}

float measurement::calculate_area(std::vector<float3> poly)
{
    if (poly.size() < 3) return 0.f;

    float3 total{ 0.f, 0.f, 0.f };

    for (int i = 0; i < poly.size(); i++)
    {
        auto v1 = poly[i];
        auto v2 = poly[(i+1) % poly.size()];
        auto prod = cross(v1, v2);
        total = total + prod;
    }

    auto a = poly[1] - poly[0];
    auto b = poly[2] - poly[0];
    auto n = cross(a, b);
    return std::abs( total * n.normalized() ) / 2;
}

void draw_sphere(const float3& pos, float r, int lats, int longs)
{
    for(int i = 0; i <= lats; i++)
    {
        float lat0 = float(M_PI) * (-0.5f + (float) (i - 1) / lats);
        float z0  = sin(lat0);
        float zr0 =  cos(lat0);

        float lat1 = float(M_PI) * (-0.5f + (float) i / lats);
        float z1 = sin(lat1);
        float zr1 = cos(lat1);

        glBegin(GL_QUAD_STRIP);
        for(int j = 0; j <= longs; j++)
        {
            float lng = 2.f * float(M_PI) * (float) (j - 1) / longs;
            float x = cos(lng);
            float y = sin(lng);

            glNormal3f(pos.x + x * zr0, pos.y + y * zr0, pos.z + z0);
            glVertex3f(pos.x + r * x * zr0, pos.y + r * y * zr0, pos.z + r * z0);
            glNormal3f(pos.x + x * zr1, pos.y + y * zr1, pos.z + z1);
            glVertex3f(pos.x + r * x * zr1, pos.y + r * y * zr1, pos.z + r * z1);
        }
        glEnd();
    }
}

rs2::float2 measurement::project_to_2d(rs2::float3 pos)
{
    int32_t vp[4];
    glGetIntegerv(GL_VIEWPORT, vp);
    check_gl_error();

    GLfloat model[16];
    glGetFloatv(GL_MODELVIEW_MATRIX, model);
    GLfloat proj[16];
    glGetFloatv(GL_PROJECTION_MATRIX, proj);

    rs2::matrix4 p(proj);
    rs2::matrix4 v(model);

    return translate_3d_to_2d(pos, p, v, rs2::matrix4::identity(), vp);
}

void measurement::draw_label(ux_window& win, float3 pos, float distance, int height, bool is_area)
{
    auto w_pos = project_to_2d(pos);
    std::string label = is_area ? area_to_string(distance) : length_to_string(distance);
    if (is_area) ImGui::PushFont(win.get_large_font());
    auto size = ImGui::CalcTextSize(label.c_str());
    if (is_area) ImGui::PopFont();

    std::string win_id = rsutils::string::from() << "measurement_" << id;
    id++;

    auto flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar;

    if (!is_area)
    {
        ImGui::PushStyleColor(ImGuiCol_Text, regular_blue);
        ImGui::PushStyleColor(ImGuiCol_WindowBg, almost_white_bg);
    }
    else
    {
        ImGui::PushStyleColor(ImGuiCol_Text, white);
        ImGui::PushStyleColor(ImGuiCol_WindowBg, transparent);
    }
    ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 10);
    ImGui::SetNextWindowPos(ImVec2(w_pos.x - size.x / 2, height - w_pos.y - size.y / 2 - 5));
    ImGui::SetNextWindowSize(ImVec2(size.x + 20, size.y - 15));
    ImGui::Begin(win_id.c_str(), nullptr, flags);

    if (is_area) ImGui::PushFont(win.get_large_font());
    ImGui::Text("%s", label.c_str());
    if (is_area) {
        ImGui::PopFont();
        ImGui::SameLine();
        ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7);
        ImGui::Text("%s", "2");
    }

    ImGui::End();
    ImGui::PopStyleVar();
    ImGui::PopStyleColor(2);
}

void measurement::draw_ruler(ux_window& win, float3 from, float3 to, float height, int selected)
{
    std::vector<float3> parts;
    parts.push_back(from);
    auto dir = to - from;
    auto l = dir.length();
    auto unit = is_metric() ? 0.01f : 0.0254f;
    if (l > 0.5f) unit = is_metric() ? 0.1f : 0.03048f;
    auto parts_num = l / unit;
    for (int i = 0; i < parts_num; i++)
    {
        auto t = i / (float)parts_num;
        parts.push_back(from + dir * t);
    }
    parts.push_back(to);

    glLineWidth(3.f);
    glBegin(GL_LINES);
    for (int i = 1; i < parts.size(); i++)
    {
        auto alpha = selected == 0 ? 0.4f : 0.9f;
        alpha = selected == 2 ? 1.f : alpha;
        if (i % 2 == 0) glColor4f(1.f, 1.f, 1.f, alpha);
        else glColor4f(light_blue.x, light_blue.y, light_blue.z, alpha);
        auto from = parts[i-1];
        auto to = parts[i]; // intentional shadowing
        glVertex3d(from.x, from.y, from.z);
        glVertex3d(to.x, to.y, to.z);
    }
    glEnd();

    if (selected == 2)
    {
        // calculate center of the ruler line
        float3 ctr = from + (to - from) / 2;
        float distance = (to - from).length();
        draw_label(win, ctr, distance, int(height));
    }
}

void measurement::mouse_pick(ux_window& win, float3 picked, float3 normal)
{
    _picked = picked; _normal = normal;
    mouse_picked_event.add_value(!input_ctrl.mouse_down);

    if (input_ctrl.click) {
        add_point({ _picked, _normal });
    }

    if (is_enabled())
    {
        if (point_hovered(win) < 0 && hovered_edge_id < 0)
            win.cross_hovered();
    }
}

void measurement::update_input(ux_window& win, const rs2::rect& viewer_rect)
{
    id = 0;

    if (ImGui::IsKeyPressed('Z') || ImGui::IsKeyPressed('z'))
        restore_state();

    input_ctrl.prev_mouse_down = input_ctrl.mouse_down;

    auto rect_copy = viewer_rect;
    rect_copy.y += 60;
    input_ctrl.click = false;
    if (win.get_mouse().mouse_down[0] && !input_ctrl.mouse_down)
    {
        input_ctrl.mouse_down = true;
        input_ctrl.down_pos = win.get_mouse().cursor;
        input_ctrl.selection_started = win.time();
    }
    if (input_ctrl.mouse_down && !win.get_mouse().mouse_down[0])
    {
        input_ctrl.mouse_down = false;
        if (win.time() - input_ctrl.selection_started < 0.5 &&
            (win.get_mouse().cursor - input_ctrl.down_pos).length() < 100)
        {
            if (rect_copy.contains(win.get_mouse().cursor))
            {
                input_ctrl.click = true;
                input_ctrl.click_time = float(glfwGetTime());
            }
        }
    }
}

int measurement::point_hovered(ux_window& win)
{
    for (int i = 0; i < state.points.size(); i++)
    {
        auto&& point = state.points[i];
        auto pos_2d = project_to_2d(point.pos);
        pos_2d.y = win.framebuf_height() - pos_2d.y;

        if ((pos_2d - win.get_mouse().cursor).length() < 15)
        {
            return i;
        }
    }
    return -1;
}

float distance_to_line(rs2::float2 a, rs2::float2 b, rs2::float2 p)
{
    const float l2 = dot(b - a, b - a);
    if (l2 == 0.0) return (p - a).length();
    const float t = clamp(dot(p - a, b - a) / l2, 0.f, 1.f);
    return (lerp(a, b, t) - p).length();
}

int measurement::edge_hovered(ux_window& win)
{
    for (int i = 0; i < state.edges.size(); i++)
    {
        auto&& a = state.points[state.edges[i].first];
        auto&& b = state.points[state.edges[i].second];

        auto a_2d = project_to_2d(a.pos);
        auto b_2d = project_to_2d(b.pos);

        auto cursor = win.get_mouse().cursor;
        cursor.y = win.framebuf_height() - cursor.y;

        if (distance_to_line(a_2d, b_2d, cursor) < 15)
        {
            return i;
        }
    }
    return -1;
}

void measurement::commit_state()
{
    state_history.push_back(state);
    if (state_history.size() > 100)
    {
        state_history.pop_front();
    }
}

void measurement::restore_state()
{
    auto new_state = state;
    while (state_history.size() && new_state == state)
    {
        new_state = state_history.back();
        state_history.pop_back();
    }
    state = new_state;
}

void measurement::draw(ux_window& win)
{
    auto shift = ImGui::IsKeyDown(GLFW_KEY_LEFT_SHIFT) || ImGui::IsKeyDown(GLFW_KEY_RIGHT_SHIFT);

    auto p_idx = point_hovered(win);
    if (p_idx >= 0 && !win.get_mouse().mouse_down[0])
    {
        _picked = state.points[p_idx].pos;
        _normal = state.points[p_idx].normal;
    }
    if (mouse_picked_event.eval() && is_enabled())
    {
        glDisable(GL_DEPTH_TEST);
        glLineWidth(2.f);
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

        float size = _picked.z * 0.03f;

        glBegin(GL_LINES);
        glColor3f(1.f, 1.f, 1.f);
        glVertex3d(_picked.x, _picked.y, _picked.z);
        auto nend = _picked + _normal * size * 0.3f;
        glVertex3d(nend.x, nend.y, nend.z);
        glEnd();

        glBegin(GL_TRIANGLES);

        if (input_ctrl.mouse_down) size -= _picked.z * 0.01f;
        size += _picked.z * 0.01f * single_wave(input_ctrl.click_period());

        auto axis1 = cross(vec3d{ _normal.x, _normal.y, _normal.z }, vec3d{ 0.f, 1.f, 0.f });
        auto faxis1 = float3 { axis1.x, axis1.y, axis1.z };
        faxis1.normalized();
        auto axis2 = cross(vec3d{ _normal.x, _normal.y, _normal.z }, axis1);
        auto faxis2 = float3 { axis2.x, axis2.y, axis2.z };
        faxis2.normalized();

        matrix4 basis = matrix4::identity();
        basis(0, 0) = faxis1.x;
        basis(0, 1) = faxis1.y;
        basis(0, 2) = faxis1.z;

        basis(1, 0) = faxis2.x;
        basis(1, 1) = faxis2.y;
        basis(1, 2) = faxis2.z;

        basis(2, 0) = _normal.x;
        basis(2, 1) = _normal.y;
        basis(2, 2) = _normal.z;

        const int segments = 50;
        for (int i = 0; i < segments; i++)
        {
            auto t1 = 2.f * float(M_PI) * ((float)i / segments);
            auto t2 = 2.f * float(M_PI) * ((float)(i+1) / segments);
            float4 xy1 { cosf(t1) * size, sinf(t1) * size, 0.f, 1.f };
            xy1 = basis * xy1;
            xy1 = float4 { _picked.x + xy1.x, _picked.y + xy1.y, _picked.z  + xy1.z, 1.f };
            float4 xy2 { cosf(t1) * size * 0.5f, sinf(t1) * size * 0.5f, 0.f, 1.f };
            xy2 = basis * xy2;
            xy2 = float4 { _picked.x + xy2.x, _picked.y + xy2.y, _picked.z  + xy2.z, 1.f };
            float4 xy3 { cosf(t2) * size * 0.5f, sinf(t2) * size * 0.5f, 0.f, 1.f };
            xy3 = basis * xy3;
            xy3 = float4 { _picked.x + xy3.x, _picked.y + xy3.y, _picked.z  + xy3.z, 1.f };
            float4 xy4 { cosf(t2) * size, sinf(t2) * size, 0.f, 1.f };
            xy4 = basis * xy4;
            xy4 = float4 { _picked.x + xy4.x, _picked.y + xy4.y, _picked.z  + xy4.z, 1.f };
            //glVertex3fv(&_picked.x);

            glColor4f(white.x, white.y, white.z, 0.5f);
            glVertex3fv(&xy1.x);
            glColor4f(white.x, white.y, white.z, 0.8f);
            glVertex3fv(&xy2.x);
            glVertex3fv(&xy3.x);

            glColor4f(white.x, white.y, white.z, 0.5f);
            glVertex3fv(&xy1.x);
            glVertex3fv(&xy4.x);
            glColor4f(white.x, white.y, white.z, 0.8f);
            glVertex3fv(&xy3.x);
        }
        //glVertex3fv(&_picked.x); glVertex3fv(&end.x);
        glEnd();

        if (state.points.size() == 1 || (shift && state.points.size()))
        {
            auto p0 = (last_hovered_point >= 0 && last_hovered_point < state.points.size())
                ? state.points[last_hovered_point] : state.points.back();
            draw_ruler(win, _picked, p0.pos, win.framebuf_height(), 2);
        }

        glDisable(GL_BLEND);
        glEnable(GL_DEPTH_TEST);
    }

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    hovered_edge_id = edge_hovered(win);

    for (auto&& poly : state.polygons)
    {
        auto ancor = state.points[poly.front()];

        std::vector<float3> points;
        points.push_back(ancor.pos);

        auto mid = ancor.pos;

        glDisable(GL_DEPTH_TEST);

        glBegin(GL_TRIANGLES);
        glColor4f(light_blue.x, light_blue.y, light_blue.z, 0.3f);
        for (int i = 0; i < poly.size() - 1; i++)
        {
            auto b = state.points[poly[i]];
            auto c = state.points[poly[i+1]];
            glVertex3fv(&ancor.pos.x);
            glVertex3fv(&b.pos.x);
            glVertex3fv(&c.pos.x);
            mid = mid + c.pos;
            points.push_back(c.pos);
        }
        glEnd();

        glEnable(GL_DEPTH_TEST);

        mid = mid * (1.f / poly.size());

        auto area = calculate_area(points);

        draw_label(win, mid, area, int(win.framebuf_height()), true);
    }

    for (int i = 0; i < state.edges.size(); i++)
    {
        auto&& pair = state.edges[i];
        glDisable(GL_DEPTH_TEST);
        int selected = 1;
        if (hovered_edge_id >= 0)
            selected = hovered_edge_id == i ? 2 : 0;
        draw_ruler(win, state.points[pair.second].pos, state.points[pair.first].pos, win.framebuf_height(), selected);
        glEnable(GL_DEPTH_TEST);
    }

    if (win.get_mouse().mouse_down[1]) {
        commit_state();
        state.points.clear();
        state.edges.clear();
        state.polygons.clear();
    }

    for (auto&& points: state.points)
    {
        glColor4f(light_blue.x, light_blue.y, light_blue.z, 0.9f);
        draw_sphere(points.pos, 0.011f, 20, 20);
    }
    glDisable(GL_DEPTH_TEST);

    current_hovered_point = -1;
    measurement_point_hovered = false;
    int hovered_point = point_hovered(win);
    if (hovered_point >= 0)
    {
        if (!shift) last_hovered_point = hovered_point;
        current_hovered_point = hovered_point;
        if (!input_ctrl.prev_mouse_down && win.get_mouse().mouse_down[0])
        {
            dragging_point_index = hovered_point;
            measurement_point_hovered = true;
            if (input_ctrl.click_period() > 0.5f)
            {
                dragging_measurement_point = true;
            }
        }
    }

    int i = 0;
    for (auto&& points: state.points)
    {
        if (measurement_point_hovered)
            glColor4f(white.x, white.y, white.z, dragging_point_index == i ? 0.8f : 0.1f);
        else
            glColor4f(white.x, white.y, white.z, 0.6f);

        draw_sphere(points.pos, dragging_point_index == i ? 0.012f : 0.008f, 20, 20);
        i++;
    }

    glEnable(GL_DEPTH_TEST);
    glDisable(GL_BLEND);

    if (!win.get_mouse().mouse_down[0] || input_ctrl.click_period() < 0.5f)
    {
        if (dragging_measurement_point && state.points.size() >= 2)
        {
            dragging_measurement_point = false;
            input_ctrl.click_time = 0;

            for (auto&& e : state.edges)
            {
                if (e.first == dragging_point_index || e.second == dragging_point_index)
                {
                    auto dist = state.points[e.first].pos - state.points[e.second].pos;
                    log_function( rsutils::string::from() << "Adjusted measurement to " << length_to_string(dist.length()));
                }
            }

            for (auto&& path : state.polygons)
            {
                if (std::find(path.begin(), path.end(), dragging_point_index) != path.end())
                {
                    std::vector<float3> poly;
                    for (auto&& idx : path) poly.push_back(state.points[idx].pos);

                    auto area = calculate_area(poly);
                    log_function( rsutils::string::from() << "Adjusted area of " << area_to_string(area));
                }
            }

            commit_state();
        }
        dragging_point_index = -1;
    }
    if (dragging_measurement_point && dragging_point_index >= 0)
    {
        state.points[dragging_point_index].pos = _picked;
    }

    if (point_hovered(win) >= 0 || hovered_edge_id >= 0)
        win.link_hovered();
}

void measurement::show_tooltip(ux_window& win)
{
    if (mouse_picked_event.eval() && ImGui::IsWindowHovered())
    {
        if (display_mouse_picked_tooltip() && hovered_edge_id  < 0)
        {
            std::string tt = rsutils::string::from() << std::fixed << std::setprecision(3)
                << _picked.x << ", " << _picked.y << ", " << _picked.z << " meters";
            ImGui::SetTooltip("%s", tt.c_str());
        }
    }
}