#include "pch.h"
#include "log.h"
#include "util.h"
#include <poly2tri.h>
#include "app.h"
#include "legacy_gl_framebuffer_dispatch.h"
#include "legacy_gl_runtime_dispatch.h"
#include "legacy_gl_sampler_dispatch.h"
#include "legacy_gl_shader_dispatch.h"
#include "legacy_ui_gl_dispatch.h"
#include "renderer_gl/opengl_capabilities.h"

template<>
std::vector<vertex_t> poly_remove_duplicate<vertex_t>(const std::vector<vertex_t>& v, const float tollerance)
{
    std::vector<vertex_t> ret;
    for (size_t i = 0; i < v.size(); i++)
    {
        if (glm::distance2(v[i].pos, v[(i + 1) % v.size()].pos) > tollerance)
            ret.push_back(v[i]);
    }
    return ret;
}

// params {x, y} and {origin, size} form
bool point_in_rect(const glm::vec2& p, const glm::vec4& r)
{
    return p.x > r.x && p.x < r.x+r.z && p.y > r.y && p.y < r.y+r.w;
}

// from {origin, size} to {min, max}
glm::vec4 rect_to_box(const glm::vec4& r)
{
    return { xy(r), xy(r) + zw(r) };
}

// from {min, max} to {origin, size}
glm::vec4 box_to_rect(const glm::vec4& b)
{
    return { xy(b), zw(b) - xy(b) };
}

// params and returns {origin, size} form
glm::vec4 rect_intersection(glm::vec4 a, glm::vec4 b)
{
    // convert from [x,y,w,h] to [x1,y1,x2,y1]
    a = glm::vec4(xy(a), xy(a) + zw(a));
    b = glm::vec4(xy(b), xy(b) + zw(b));
    // compute intersection
    auto o = glm::vec4(glm::max(xy(a), xy(b)), glm::min(zw(a), zw(b)));
    // back to rect form
    o = glm::vec4(xy(o), glm::max({ 0, 0 }, zw(o) - xy(o)));
    return o;
}

// params and returns {origin, size} form
glm::vec4 rect_union(glm::vec4 a, glm::vec4 b)
{
    // convert from rect [x,y,w,h] to bb [x1,y1,x2,y1]
    a = glm::vec4(xy(a), xy(a) + zw(a));
    b = glm::vec4(xy(b), xy(b) + zw(b));
    // compute union
    glm::vec4 o = { glm::min(xy(a), xy(b)), glm::max(zw(a), zw(b)) };
    // back to rect form
    o = glm::vec4(xy(o), glm::max({ 0, 0 }, zw(o) - xy(o)));
    return o;
}

float box_area(glm::vec4 b)
{
    return glm::compMul(box_size(b));
}

glm::vec2 box_size(glm::vec4 b)
{
    return zw(b) - xy(b);
}

// params and returns {min, max} form
glm::vec4 box_union(glm::vec4 a, glm::vec4 b)
{
    return { glm::min(xy(a), xy(b)), glm::max(zw(a), zw(b)) };
}

// params and returns {min, max} form
glm::vec4 box_intersection(glm::vec4 a, glm::vec4 b)
{
    return { glm::max(xy(a), xy(b)), glm::min(zw(a), zw(b)) };
}

bool ray_intersect(glm::vec3 ray_origin, glm::vec3 ray_dir, glm::vec3 plane_origin,
    glm::vec3 plane_normal, glm::vec3 plane_tangent, glm::vec3& out_hit, float& out_t)
{
    float den = glm::dot(ray_dir, plane_normal);
    if (den == 0)
        return false;  // no intersection
    float num = glm::dot(plane_origin - ray_origin, plane_normal);
    out_t = num / den;
    if (out_t > 0)
        out_hit = ray_origin + ray_dir * out_t;
    else
        // negative intersection
        return false;
    return true;
};

// see: http://geomalgorithms.com/a07-_distance.html
// dist3D_Line_to_Line(): get the 3D minimum distance between 2 lines
//    Input:  two 3D lines L1 and L2
//    Return: the shortest distance between L1 and L2
float lines_distance(const glm::vec3& p0a, const glm::vec3& p0b,
    const glm::vec3& p1a, const glm::vec3& p1b)
{
    glm::vec3 u = p0b - p0a;
    glm::vec3 v = p1b - p1a;
    glm::vec3 w = p0a - p1a;
    float    a = glm::dot(u,u); // always >= 0
    float    b = glm::dot(u,v);
    float    c = glm::dot(v,v); // always >= 0
    float    d = glm::dot(u,w);
    float    e = glm::dot(v,w);
    float    D = a*c - b*b;     // always >= 0
    float    sc, tc;

    // compute the line parameters of the two closest points
    if (D < 0.00001f) {          // the lines are almost parallel
        sc = 0.0;
        tc = (b>c ? d/b : e/c);    // use the largest denominator
    }
    else {
        sc = (b*e - c*d) / D;
        tc = (a*e - b*d) / D;
    }

    // get the difference of the two closest points
    glm::vec3 dP = w + (sc * u) - (tc * v);  // =  L1(sc) - L2(tc)

    return glm::length(dP);   // return the closest distance
}

bool segments_intersect_3d(const glm::vec3& p0a, const glm::vec3& p0b,
    const glm::vec3& p1a, const glm::vec3& p1b, glm::vec3& out_pt, glm::vec2& out_hit_uv)
{
    float denom = ((p1b.y - p1a.y)*(p0b.x - p0a.x)) -
                  ((p1b.x - p1a.x)*(p0b.y - p0a.y));

    float nume_a = ((p1b.x - p1a.x)*(p0a.y - p1a.y)) -
                   ((p1b.y - p1a.y)*(p0b.x - p1a.x));

    float nume_b = ((p0b.x - p0a.x)*(p0a.y - p1a.y)) -
                   ((p0b.y - p0a.y)*(p0a.x - p1a.x));

    if(denom == 0.0f)
    {
        if(nume_a == 0.0f && nume_b == 0.0f)
        {
            return 0;//COINCIDENT;
        }
        return 0;//PARALLEL;
    }

    float ua = nume_a / denom;
    float ub = nume_b / denom;

    if(ua >= 0.0f && ua <= 1.0f && ub >= 0.0f && ub <= 1.0f)
    {
        // Get the intersection point.
        out_pt.x = p0a.x + ua*(p0b.x - p0a.x);
        out_pt.y = p0a.y + ua*(p0b.y - p0a.y);

        return 1;//INTERESECTING;
    }

    return 0;//NOT_INTERESECTING;
}

// see: https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
bool segments_intersect(const glm::vec2& p0a, const glm::vec2& p0b,
    const glm::vec2& p1a, const glm::vec2& p1b, glm::vec2& out_pt, glm::vec2& out_hit_uv)
{
    auto cross2d = [](const glm::vec2& v, const glm::vec2& w)
        { return (v.x * w.y) - (v.y * w.x); };
    auto p = p0a;
    auto r = p0b - p0a;
    auto q = p1a;
    auto s = p1b - p1a;
    float den = cross2d(r, s);
    if (den == 0.f)
    {
        glm::vec4 is = rect_intersection({p, r}, {q, s});
        out_pt = xy(is) + zw(is) * 0.5f;
        return glm::all(glm::greaterThan(zw(is), glm::vec2(0, 0)));
    }
    out_hit_uv.x = cross2d(q - p, s) / den;
    out_hit_uv.y = cross2d(q - p, r) / den;
    out_pt = p + out_hit_uv.x * r;
    if (out_hit_uv.x >= 0 && out_hit_uv.x <= 1 && out_hit_uv.y >= 0 && out_hit_uv.y <= 1)
    {
        return true;
    }
    return false;
}

// return true if the point p in the right halfspace of the ab line
// computed using the 2d cross product 
bool point_side(glm::vec2 a, glm::vec2 b, glm::vec2 p)
{
    return (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x) >= 0.f;
}

// intersect 2 closed polygons
// a is a convex polygon
// a and b are a list of non repeating points
// returns the resulting intersection polygon points
std::vector<vertex_t> poly_intersect(const vertex_t* poly_begin, const vertex_t* poly_end, const std::vector<glm::vec2>& clip)
{
    // implementing the Sutherland-Hodgman algorithm
    // see https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm
    std::vector<vertex_t> ret(poly_begin, poly_end);
    for (int i = 0; i < clip.size(); i++)
    {
        std::vector<vertex_t> tmp;
        glm::vec2 edge[2] = { clip[i], clip[(i + 1) % clip.size()] };
        for (int j = 0; j < ret.size(); j++)
        {
            vertex_t s[2] = { ret[j], ret[(j + 1) % ret.size()] };
            bool side0 = point_side(edge[0], edge[1], s[0].pos);
            bool side1 = point_side(edge[0], edge[1], s[1].pos);
            if (side0 != side1) // intersecting
            {
                glm::vec2 pt;
                glm::vec2 hit_uv;
                segments_intersect(edge[0], edge[1], s[0].pos, s[1].pos, pt, hit_uv);
                vertex_t v;
                v.pos = glm::lerp(s[0].pos, s[1].pos, hit_uv.y);
                v.uvs = glm::lerp(s[0].uvs, s[1].uvs, hit_uv.y);
                v.uvs2 = glm::lerp(s[0].uvs2, s[1].uvs2, hit_uv.y);
                if (side0) // outgoing
                {
                    tmp.push_back(s[0]);
                    tmp.push_back(v);
                }
                else // ingoing
                {
                    tmp.push_back(v);
                }
            }
            else if (side0 && side1)
            {
                tmp.push_back(s[0]);
            }
        }
        ret = std::move(tmp);
    }
    return poly_remove_duplicate(ret);
}

std::vector<glm::vec2> poly_intersect(const std::vector<glm::vec2>& poly, const std::vector<glm::vec2>& clip)
{
    // implementing the Sutherland-Hodgman algorithm
    // see https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm
    std::vector<glm::vec2> ret = poly;
    for (int i = 0; i < clip.size(); i++)
    {
        std::vector<glm::vec2> tmp;
        glm::vec2 edge[2] = { clip[i], clip[(i + 1) % clip.size()] };
        for (int j = 0; j < ret.size(); j++)
        {
            glm::vec2 s[2] = { ret[j], ret[(j + 1) % ret.size()] };
            bool side0 = point_side(edge[0], edge[1], s[0]);
            bool side1 = point_side(edge[0], edge[1], s[1]);
            if (side0 != side1) // intersecting
            {
                glm::vec2 pt;
                glm::vec2 hit_uv;
                segments_intersect(edge[0], edge[1], s[0], s[1], pt, hit_uv);
                if (side0) // outgoing
                {
                    tmp.push_back(s[0]);
                    tmp.push_back(pt);
                }
                else // ingoing
                {
                    tmp.push_back(pt);
                }
            }
            else if (side0 && side1)
            {
                tmp.push_back(s[0]);
            }
        }
        ret = std::move(tmp);
    }
    return poly_remove_duplicate(ret);
}

// clip the polygon to the near clip plane
// poly is the polygon in camera coordinates
std::vector<glm::vec3> poly_clip_near(const std::vector<glm::vec3>& poly, float near_plane_distance)
{
    // implementing the Sutherland-Hodgman algorithm in 3D
    // see https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm

    auto o = glm::vec3(0, 0, -near_plane_distance);
    auto n = glm::vec3(0, 0, -1);
    auto t = glm::vec3(0, 1, 0);
    std::vector<glm::vec3> ret;
    for (int j = 0; j < poly.size(); j++)
    {
        glm::vec3 s[2] = { poly[j], poly[(j + 1) % poly.size()] };
        bool side0 = glm::dot(n, s[0] - o) >= 0.f;
        bool side1 = glm::dot(n, s[1] - o) >= 0.f;
        if (side0 != side1) // intersecting
        {
            glm::vec3 pt;
            float hit_t;
            if (!ray_intersect(s[0], glm::normalize(s[1] - s[0]), o, n, t, pt, hit_t))
            {
                LOG("error ray_intersect");
            }
            if (side0) // outgoing
            {
                ret.push_back(s[0]);
                ret.push_back(pt);
            }
            else // ingoing
            {
                ret.push_back(pt);
            }
        }
        else if (side0 && side1)
        {
            ret.push_back(s[0]);
        }
    }
    return poly_remove_duplicate(ret);
}


std::vector<vertex_t> triangulate_simple(const std::vector<vertex_t>& vertices)
{
    std::vector<vertex_t> ret;
    std::vector<p2t::Point> points(vertices.size());
    std::vector<p2t::Point*> points_ptr(vertices.size());
    for (size_t i = 0; i < vertices.size(); i++)
    {
        points[i] = { vertices[i].pos.x, vertices[i].pos.y };
        points_ptr[i] = &points[i];
    }

    auto cdt = std::make_unique<p2t::CDT>(points_ptr);
    cdt->Triangulate();
    auto tr = cdt->GetTriangles();
    for (auto t : tr)
    {
        vertex_t vertex;
        for (int i = 0; i < 3; i++)
        {
            auto index = std::distance(points.data(), t->GetPoint(i));
            ret.push_back(vertices[index]);
        }
    }

    return ret;
}

std::vector<vertex_t> triangulate(const std::vector<glm::vec2>& points)
{
    std::vector<vertex_t> tmp;
    for (auto pt : points)
        tmp.push_back(pt);
    return triangulate(tmp);
}

std::vector<vertex_t> triangulate(const std::vector<vertex_t>& points)
{
    struct Segment
    {
        const vertex_t* a = nullptr;
        const vertex_t* b = nullptr;
        Segment* prev = nullptr;
        std::shared_ptr<Segment> next = nullptr;
        bool end = false;
    };
    std::vector<std::shared_ptr<vertex_t>> new_points;
    std::shared_ptr<Segment> root = std::make_shared<Segment>();
    std::shared_ptr<Segment> node = root;
    for (int i = 0; i < points.size(); i++)
    {
        node->a = &points[i];
        if (i == points.size() - 1)
        {
            node->b = &points[0];
            node->next = root;
            node->end = true;
            root->prev = node.get();
        }
        else
        {
            node->b = &points[i + 1];
            node->next = std::make_shared<Segment>();
            node->next->prev = node.get();
        }
        node = node->next;
    }

    node = root;
    std::stack<std::shared_ptr<Segment>> todo;
    std::vector<std::shared_ptr<Segment>> polys;
    todo.push(root);
    while (!todo.empty())
    {
        node = todo.top();
        todo.pop();
        polys.push_back(node);
        while (node)
        {
            std::shared_ptr<Segment> other = node->next;
            while (other)
            {
                if (node->a->pos == other->a->pos || node->a->pos == other->b->pos ||
                    node->b->pos == other->a->pos || node->b->pos == other->b->pos)
                {
                    other = other->end ? nullptr : other->next;
                    continue;
                }
                glm::vec2 s0a(node->a->pos);
                glm::vec2 s0b(node->b->pos);
                glm::vec2 s1a(other->a->pos);
                glm::vec2 s1b(other->b->pos);
                glm::vec2 hit_uv;
                glm::vec2 is;
                if (segments_intersect(s0a, s0b, s1a, s1b, is, hit_uv))
                {
                    new_points.push_back(std::make_unique<vertex_t>());
                    auto p = new_points.back().get();
                    p->pos = glm::lerp(node->a->pos, node->b->pos, hit_uv.x);
                    p->uvs = glm::lerp(node->a->uvs, node->b->uvs, hit_uv.x);
                    p->uvs2 = glm::lerp(node->a->uvs2, node->b->uvs2, hit_uv.x);
                    auto poly_root = std::make_shared<Segment>();
                    poly_root->a = p;
                    poly_root->b = node->b;
                    poly_root->next = node->next;
                    todo.push(poly_root);
                    other->a = p;
                    node->b = p;
                    auto poly_end = std::make_shared<Segment>();
                    poly_end->a = other->prev->b;
                    poly_end->b = p;
                    poly_end->end = true;
                    poly_end->prev = other->prev;
                    other->prev->next = poly_end;
                    other->prev = node.get();
                    node->next = other;
                    break;
                }
                other = other->end ? nullptr : other->next;
            }
            node = node->end ? nullptr : node->next;
        }
    }

    std::vector<vertex_t> ret;
    for (auto poly : polys)
    {
        std::vector<const vertex_t*> outline;
        node = poly;
        while (node)
        {
            if (outline.empty() || // if empty insert right away
                (outline.back() != node->a && // insert only if different than the last post
                (outline.front() != node->a || !node->end))) // if is the end check against the first one
            {
                outline.push_back(node->a);
            }
            auto current = node;
            node = node->end ? nullptr : node->next;
            current->next = nullptr;
        }

        if (outline.size() > 2)
        {
            std::vector<p2t::Point> points(outline.size());
            std::vector<p2t::Point*> points_ptr(outline.size());
            for (size_t i = 0; i < outline.size(); i++)
            {
                points[i] = { outline[i]->pos.x, outline[i]->pos.y };
                points_ptr[i] = &points[i];
            }

            p2t::CDT* cdt = new p2t::CDT(points_ptr); // TODO: remove duplicates
            cdt->Triangulate();
            auto tr = cdt->GetTriangles();
            for (auto t : tr)
            {
                for (int i = 0; i < 3; i++)
                {
                    auto index = std::distance(points.data(), t->GetPoint(i));
                    ret.push_back(*outline[index]);
                }
            }
        }
    }

    return ret;
}


glm::vec4 rand_color()
{
    float r = (rand() % 256) / 256.f;
    float g = (rand() % 256) / 256.f;
    float b = (rand() % 256) / 256.f;
    return { r, g, b, 1.f };
}

glm::vec3 convert_long_rgb(uint32_t hex)
{
    uint8_t b = (hex >> 0) & 0xFF;
    uint8_t g = (hex >> 8) & 0xFF;
    uint8_t r = (hex >> 16) & 0xFF;
    return glm::vec3(r, g, b) / 255.f;
}

uint32_t convert_rgb_long(glm::vec3 rgb)
{
    uint8_t r = (uint8_t)(rgb.r * 255.f);
    uint8_t g = (uint8_t)(rgb.g * 255.f);
    uint8_t b = (uint8_t)(rgb.b * 255.f);
    return (r << 16) + (g << 8) + b;
}

glm::vec3 convert_hsv2rgb(const glm::vec3 c)
{
    glm::vec4 K = glm::vec4(1.0f, 2.0f / 3.0f, 1.0f / 3.0f, 3.0f);
    glm::vec3 p = glm::abs(glm::fract(glm::vec3(c.x) + xyz(K)) * 6.0f - glm::vec3(K.w));
    auto tmp = glm::clamp(p - glm::vec3(K.x), 0.0f, 1.0f);
    return c.z * glm::mix(glm::vec3(K.x), tmp, c.y);
}

glm::vec3 convert_rgb2hsv(const glm::vec3 c)
{
    glm::vec4 K = glm::vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    //glm::vec4 p = mix(glm::vec4(c.bg, K.wz), glm::vec4(c.gb, K.xy), glm::step(c.b, c.g));
    //glm::vec4 q = mix(glm::vec4(p.xyw, c.r), glm::vec4(c.r, p.yzx), glm::step(p.x, c.r));
    glm::vec4 p = c.g < c.b ? glm::vec4(c.b, c.g, K.w, K.z) : glm::vec4(c.g, c.b, K.x, K.y);
    glm::vec4 q = c.r < p.x ? glm::vec4(p.x, p.y, p.w, c.r) : glm::vec4(c.r, p.y, p.z, p.x);
    float d = q.x - glm::min(q.w, q.y);
    float e = 1.0e-10f;
    return glm::vec3(fabs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

std::string unescape(const std::string& s)
{
    std::string res;
    std::string::const_iterator it = s.begin();
    while (it != s.end())
    {
        char c = *it++;
        if (c == '\\' && it != s.end())
        {
            switch (*it++) {
            case '\\': c = '\\'; break;
            case 'n': c = '\n'; break;
            case 't': c = '\t'; break;
                // all other escapes
            default:
                // invalid escape sequence - skip it. alternatively you can copy it as is, throw an exception...
                continue;
            }
        }
        res += c;
    }

    return res;
}

std::wstring str2wstr(const std::string& str)
{
    if (str.empty()) return {};
    try
    {
        std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converterX;
        return converterX.from_bytes(str);
    }
    catch (...)
    {
        LOG("str2wstr error: %s", str.c_str());
        return {};
    }
}

std::string wstr2str(const std::wstring & wstr)
{
    if (wstr.empty()) return {};
    try
    {
        std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converterX;
        return converterX.to_bytes(wstr);
    }
    catch (...)
    {
        LOGW(L"wstr2str error: %s", wstr.c_str());
        return {};
    }
}

bool str_iequals(const std::string& a, const std::string& b)
{
    size_t sz = a.size();
    if (b.size() != sz)
        return false;
    for (size_t i = 0; i < sz; ++i)
        if (std::tolower(a[i]) != std::tolower(b[i]))
            return false;
    return true;
}

std::string str_replace(const std::string& string, const std::string& search, const std::string& replace)
{
    std::string ret = string;
    // Get the first occurrence
    size_t pos = ret.find(search);
    // Repeat till end is reached
    while(pos != std::string::npos)
    {
        // Replace this occurrence of Sub String
        ret.replace(pos, search.size(), replace);
        // Get the next occurrence from the current position
        pos = ret.find(search, pos + replace.size());
    }
    return ret;
}

static const char* gl2str(std::uint32_t err)
{
    return pp::renderer::gl::opengl_error_name(err);
}

double now_seconds()
{
    time_t timer;
    struct tm y2k = { 0 };
    double seconds;

    y2k.tm_hour = 0;   y2k.tm_min = 0; y2k.tm_sec = 0;
    y2k.tm_year = 100; y2k.tm_mon = 0; y2k.tm_mday = 1;

    time(&timer);  /* get current time; same as: timer = time(NULL)  */
    seconds = difftime(timer, mktime(&y2k));
    return seconds;
}

void check_OpenGLError(const char* stmt, const char* fname, int line)
{
    std::uint32_t err = 0U;
    while ((err = pp::legacy::gl_runtime::query_opengl_error()) != pp::renderer::gl::no_error_code())
    {
        LOG("OpenGL error %08x (%s), at %s:%i - for %s", err, gl2str(err), fname, line, stmt);
    }
}

bool copy_framebuffer_to_texture_target(
    uint32_t texture_target,
    int destination_x,
    int destination_y,
    int source_x,
    int source_y,
    int width,
    int height,
    int level) noexcept
{
    const auto status = pp::renderer::gl::copy_opengl_framebuffer_to_texture_2d(
        pp::renderer::gl::OpenGlTexture2DFramebufferCopy {
            .texture_target = texture_target,
            .level = level,
            .destination_x = destination_x,
            .destination_y = destination_y,
            .source_x = source_x,
            .source_y = source_y,
            .width = width,
            .height = height,
        },
        pp::legacy::gl_framebuffer::framebuffer_to_texture_copy_dispatch());
    if (!status.ok()) {
        LOG("OpenGL framebuffer-to-texture copy failed: %s", status.message);
        return false;
    }
    return true;
}

bool copy_framebuffer_to_texture_2d(
    int destination_x,
    int destination_y,
    int source_x,
    int source_y,
    int width,
    int height,
    int level) noexcept
{
    return copy_framebuffer_to_texture_target(
        pp::renderer::gl::texture_2d_target(),
        destination_x,
        destination_y,
        source_x,
        source_y,
        width,
        height,
        level);
}

size_t curl_data_handler(void *contents, size_t size, size_t nmemb, void *userp)
{
    auto buffer = reinterpret_cast<std::string*>(userp);
    buffer->append((char*)contents, size * nmemb);
    return size * nmemb;
}

size_t curl_data_write(void *ptr, size_t size, size_t nmemb, FILE *stream)
{
    size_t written = fwrite(ptr, size, nmemb, stream);
    return written;
}

/// @param[in] nb_elements : size of your for loop
/// @param[in] functor(start, end) :
/// your function processing a sub chunk of the for loop.
/// "start" is the first index to process (included) until the index "end"
/// (excluded)
/// @code
///     for(int i = start; i < end; ++i)
///         computation(i);
/// @endcode
/// @param use_threads : enable / disable threads.
///
///
void parallel_for(size_t nb_elements, std::function<void(size_t i)> functor, bool use_threads)
{
    // -------
    size_t nb_threads_hint = std::thread::hardware_concurrency();
    size_t nb_threads = nb_threads_hint == 0 ? 8 : (nb_threads_hint);

    size_t batch_size = nb_elements / nb_threads;
    size_t batch_remainder = nb_elements % nb_threads;

    std::vector< std::thread > my_threads(nb_threads);

    if (use_threads)
    {
        // Multithread execution
        for (size_t i = 0; i < nb_threads; ++i)
        {
            size_t start = i * batch_size;
            my_threads[i] = std::thread([functor, start, batch_size]() {
                BT_SetTerminate();
                for (size_t j = start; j < start + batch_size; j++)
                    functor(j);
            });
        }
    }
    else
    {
        // Single thread execution (for easy debugging)
        for (size_t i = 0; i < nb_threads; ++i) {
            size_t start = i * batch_size;
            for (size_t j = start; j < start + batch_size; j++)
                functor(j);
        }
    }

    // Deform the elements left
    size_t start = nb_threads * batch_size;
    for (size_t j = start; j < start + batch_remainder; j++)
        functor(j);

    // Wait for the other thread to finish their task
    if (use_threads)
        std::for_each(my_threads.begin(), my_threads.end(), std::mem_fn(&std::thread::join));
}

void gl_state::save()
{
    assert(App::I->is_render_thread());

    const auto snapshot = pp::renderer::gl::snapshot_opengl_state(
        pp::renderer::gl::OpenGlStateSnapshotDispatch {
            .is_enabled = pp::legacy::ui_gl::is_opengl_state_enabled,
            .get_integer = pp::legacy::gl_framebuffer::query_opengl_integer,
            .get_float = pp::legacy::ui_gl::get_opengl_float,
            .active_texture = pp::legacy::ui_gl::activate_opengl_texture,
        });
    if (!snapshot.ok()) {
        LOG("OpenGL state snapshot failed: %s", snapshot.status().message);
        return;
    }

    const auto& state = snapshot.value();
    blend = static_cast<GLboolean>(state.blend_enabled);
    depth_test = static_cast<GLboolean>(state.depth_test_enabled);
    scissor_test = static_cast<GLboolean>(state.scissor_test_enabled);
    for (std::size_t i = 0; i < state.viewport.size(); ++i) {
        vp[i] = static_cast<GLint>(state.viewport[i]);
        cc[i] = state.clear_color[i];
    }
    for (std::size_t i = 0; i < state.texture_2d_bindings.size(); ++i) {
        tex[i] = static_cast<GLint>(state.texture_2d_bindings[i]);
        sampler[i] = static_cast<GLint>(state.sampler_bindings[i]);
    }
    cube = static_cast<GLint>(state.cube_map_binding);
    program = static_cast<GLint>(state.program);
    fbd = static_cast<GLint>(state.draw_framebuffer);
    fbr = static_cast<GLint>(state.read_framebuffer);
    active_tex = static_cast<GLint>(state.active_texture);
}

void gl_state::restore()
{
    assert(App::I->is_render_thread());

    pp::renderer::gl::OpenGlSavedState state {};
    state.blend_enabled = blend;
    state.depth_test_enabled = depth_test;
    state.scissor_test_enabled = scissor_test;
    for (std::size_t i = 0; i < state.viewport.size(); ++i) {
        state.viewport[i] = static_cast<std::int32_t>(vp[i]);
        state.clear_color[i] = cc[i];
    }
    for (std::size_t i = 0; i < state.texture_2d_bindings.size(); ++i) {
        state.texture_2d_bindings[i] = static_cast<std::int32_t>(tex[i]);
        state.sampler_bindings[i] = static_cast<std::int32_t>(sampler[i]);
    }
    state.cube_map_binding = static_cast<std::int32_t>(cube);
    state.program = static_cast<std::int32_t>(program);
    state.draw_framebuffer = static_cast<std::int32_t>(fbd);
    state.read_framebuffer = static_cast<std::int32_t>(fbr);
    state.active_texture = static_cast<std::int32_t>(active_tex);

    const auto status = pp::renderer::gl::restore_opengl_state(
        state,
        pp::renderer::gl::OpenGlStateRestoreDispatch {
            .enable = pp::legacy::ui_gl::enable_opengl_state,
            .disable = pp::legacy::ui_gl::disable_opengl_state,
            .viewport = pp::legacy::ui_gl::set_opengl_viewport,
            .clear_color = pp::legacy::ui_gl::set_opengl_clear_color,
            .bind_framebuffer = pp::legacy::ui_gl::bind_opengl_framebuffer,
            .use_program = pp::legacy::gl_shader::use_opengl_program,
            .active_texture = pp::legacy::ui_gl::activate_opengl_texture,
            .bind_texture = pp::legacy::ui_gl::bind_opengl_texture,
            .bind_sampler = pp::legacy::gl_sampler::bind_opengl_sampler,
        });
    if (!status.ok())
        LOG("OpenGL state restore failed: %s", status.message);
}
