#include "pch.h"
#include "log.h"
#include "canvas.h"
#include "app.h"
#include "legacy_canvas_draw_merge_services.h"
#include "legacy_canvas_stroke_erase_services.h"
#include "legacy_gl_renderbuffer_dispatch.h"
#include "legacy_canvas_stroke_commit_services.h"
#include "legacy_canvas_stroke_composite_services.h"
#include "legacy_canvas_stroke_edge_services.h"
#include "legacy_canvas_stroke_execution_services.h"
#include "legacy_canvas_stroke_shader_services.h"
#include "legacy_canvas_stroke_services.h"
#include "legacy_ui_gl_dispatch.h"
#include "legacy_ui_overlay_services.h"
#include "app_core/document_canvas.h"
#include "texture.h"
#include "node_progress_bar.h"
#include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h"
#include "util.h"
#include <array>
#include <thread>
#include <algorithm>
#include <cstdint>
#include <numeric>

#ifdef __APPLE__
#include <Foundation/Foundation.h>
#endif

namespace {

GLint current_canvas_stroke_internal_format()
{
    const auto renderer_features = ShaderManager::render_device_features();
    if (renderer_features.float32_linear_filtering)
        return static_cast<GLint>(pp::renderer::gl::rgba32f_internal_format());
    if (renderer_features.float16_render_targets)
        return static_cast<GLint>(pp::renderer::gl::rgba16f_internal_format());
    return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
}

GLint rgba8_internal_format()
{
    return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
}

pp::renderer::RenderDeviceFeatures canvas_render_device_features() noexcept
{
    return ShaderManager::render_device_features();
}

pp::paint_renderer::CanvasStrokeRasterizationPlan canvas_stroke_rasterization_plan(
    int width,
    int height) noexcept
{
    return pp::panopainter::plan_legacy_canvas_stroke_rasterization(
        canvas_render_device_features(),
        width,
        height);
}

pp::paint_renderer::CanvasStrokeFeedbackPlan canvas_destination_feedback_plan(
    int width,
    int height) noexcept
{
    return canvas_stroke_rasterization_plan(width, height).feedback;
}

pp::paint_renderer::CanvasStrokeMaterialPlan canvas_stroke_material_plan(
    const Brush& brush,
    bool destination_feedback_needed) noexcept
{
    return pp::panopainter::plan_legacy_canvas_stroke_material(
        pp::paint_renderer::CanvasStrokeMaterialRequest {
            .destination_feedback_needed = destination_feedback_needed,
            .pattern_enabled = brush.m_pattern_enabled,
            .pattern_eachsample = brush.m_pattern_eachsample,
            .wet_blend = brush.m_tip_wet > 0.F,
            .mix_blend = brush.m_tip_mix > 0.F,
            .noise_enabled = brush.m_tip_noise > 0.F,
            .dual_brush_enabled = brush.m_dual_enabled,
            .dual_blend_mode = brush.m_dual_blend_mode,
            .pattern_blend_mode = brush.m_pattern_blend_mode,
            .dual_alpha = brush.m_dual_opacity,
        });
}

pp::renderer::Extent2D canvas_stroke_extent(int width, int height) noexcept
{
    return pp::renderer::Extent2D {
        .width = static_cast<std::uint32_t>(std::max(width, 0)),
        .height = static_cast<std::uint32_t>(std::max(height, 0)),
    };
}

pp::paint_renderer::CanvasBlendGatePlan draw_merge_blend_gate_plan(
    int width,
    int height,
    const std::vector<std::shared_ptr<Layer>>& layers,
    const Brush* brush) noexcept
{
    std::vector<int> layer_blend_modes;
    layer_blend_modes.reserve(layers.size());
    for (const auto& layer : layers) {
        if (!layer) {
            continue;
        }
        layer_blend_modes.push_back(layer->m_blend_mode);
    }

    const auto plan = pp::paint_renderer::plan_canvas_blend_gate(
        canvas_render_device_features(),
        pp::paint_renderer::CanvasBlendGateRequest {
            .extent = pp::renderer::Extent2D {
                .width = static_cast<std::uint32_t>(std::max(width, 0)),
                .height = static_cast<std::uint32_t>(std::max(height, 0)),
            },
            .layer_blend_modes = layer_blend_modes,
            .has_stroke_blend_mode = brush != nullptr,
            .stroke_blend_mode = brush ? brush->m_blend_mode : 0,
        });
    if (plan) {
        return plan.value();
    }

    pp::paint_renderer::CanvasBlendGatePlan fallback;
    fallback.shader_blend = true;
    fallback.complex_blend = true;
    fallback.compatibility_fallback = true;
    return fallback;
}

GLenum depth_test_state()
{
    return static_cast<GLenum>(pp::renderer::gl::depth_test_state());
}

GLenum scissor_test_state()
{
    return static_cast<GLenum>(pp::renderer::gl::scissor_test_state());
}

GLenum blend_state()
{
    return static_cast<GLenum>(pp::renderer::gl::blend_state());
}

GLint texture_filter_linear()
{
    return static_cast<GLint>(pp::renderer::gl::linear_texture_filter());
}

GLint texture_filter_linear_mipmap_linear()
{
    return static_cast<GLint>(pp::renderer::gl::linear_mipmap_linear_texture_filter());
}

GLint texture_filter_nearest()
{
    return static_cast<GLint>(pp::renderer::gl::nearest_texture_filter());
}

GLint texture_wrap_repeat()
{
    return static_cast<GLint>(pp::renderer::gl::repeat_texture_wrap());
}

GLint texture_wrap_clamp_to_border()
{
    return static_cast<GLint>(pp::renderer::gl::clamp_to_border_texture_wrap());
}

pp::renderer::gl::OpenGlPixelFormat texture_format_for_image_channels(int channel_count)
{
    return pp::renderer::gl::texture_format_for_channel_count(static_cast<std::uint32_t>(channel_count));
}

void set_active_texture_unit(std::uint32_t unit_index)
{
    pp::legacy::ui_gl::activate_texture_unit(unit_index, "Canvas");
}

void unbind_texture_2d()
{
    pp::legacy::ui_gl::unbind_texture_2d("Canvas");
}

void apply_canvas_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height)
{
    pp::legacy::ui_gl::apply_viewport(x, y, width, height, "Canvas");
}

pp::renderer::gl::OpenGlViewportRect query_canvas_viewport()
{
    return pp::legacy::ui_gl::query_viewport_rect("Canvas");
}

std::array<float, 4> query_canvas_clear_color()
{
    return pp::legacy::ui_gl::query_clear_color("Canvas");
}

void apply_canvas_clear_color(std::array<float, 4> color)
{
    pp::legacy::ui_gl::set_clear_color(color, "Canvas");
}

void apply_canvas_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height)
{
    pp::legacy::ui_gl::apply_scissor_rect(x, y, width, height, "Canvas");
}

void apply_canvas_capability(std::uint32_t state, bool enabled)
{
    pp::legacy::ui_gl::set_capability(state, enabled, "Canvas");
}

bool query_canvas_capability(std::uint32_t state)
{
    return pp::legacy::ui_gl::query_capability(state, "Canvas");
}

GLuint allocate_canvas_depth_renderbuffer(int width, int height)
{
    return static_cast<GLuint>(pp::legacy::gl_renderbuffer::allocate_depth_renderbuffer(
        width,
        height,
        "OpenGL canvas depth renderbuffer allocation"));
}

void attach_canvas_depth_renderbuffer(GLuint renderbuffer)
{
    pp::legacy::gl_renderbuffer::attach_depth_renderbuffer(
        static_cast<std::uint32_t>(renderbuffer),
        "OpenGL canvas depth renderbuffer attachment");
}

void delete_canvas_renderbuffer(GLuint renderbuffer)
{
    pp::legacy::gl_renderbuffer::delete_renderbuffer(
        static_cast<std::uint32_t>(renderbuffer),
        "OpenGL canvas renderbuffer delete");
}

}


Canvas* Canvas::I;
std::vector<CanvasMode*> Canvas::modes[] = {
    { new CanvasModePen, new CanvasModeBasicCamera },       // brush
    { new CanvasModePen, new CanvasModeBasicCamera },       // eraser
    { new CanvasModeLine, new CanvasModeBasicCamera },      // line
    { new CanvasModeCamera, new CanvasModeBasicCamera },    // parallax
    { new CanvasModeGrid, new CanvasModeBasicCamera },      // grids
    { new CanvasModeTransform, new CanvasModeBasicCamera }, // import
    { new CanvasModeTransform, new CanvasModeBasicCamera }, // cut
    { new CanvasModeTransform, new CanvasModeBasicCamera }, // copy
    { new CanvasModeFill, new CanvasModeBasicCamera },      // fill
    { new CanvasModeMaskFree, new CanvasModeBasicCamera },  // mask-free
    { new CanvasModeMaskLine, new CanvasModeBasicCamera },  // mask-poly
    { new CanvasModeFloodFill, new CanvasModeBasicCamera },  // flood-fill
};
glm::vec3 Canvas::m_plane_origin[6] = {
    { 0, 0,-1}, // front
    { 1, 0, 0}, // right
    { 0, 0, 1}, // back
    {-1, 0, 0}, // left
    { 0, 1, 0}, // top
    { 0,-1, 0}, // bottom
};
glm::vec3 Canvas::m_plane_normal[6] = {
    { 0, 0, 1}, // front
    {-1, 0, 0}, // right
    { 0, 0,-1}, // back
    { 1, 0, 0}, // left
    { 0,-1, 0}, // top
    { 0, 1, 0}, // bottom
};
glm::vec3 Canvas::m_plane_tangent[6] = {
    {0, 1, 0}, // front
    {0, 1, 0}, // right
    {0, 1, 0}, // back
    {0, 1, 0}, // left
    {0, 0,-1}, // top
    {0, 0, 1}, // bottom
};
// only rotation
glm::mat4 Canvas::m_plane_transform[6] = {
    glm::lookAt(glm::vec3(), { 0, 0,-1}, {0, 1, 0}), // front
    glm::lookAt(glm::vec3(), {-1, 0, 0}, {0, 1, 0}), // right
    glm::lookAt(glm::vec3(), { 0, 0, 1}, {0, 1, 0}), // back
    glm::lookAt(glm::vec3(), { 1, 0, 0}, {0, 1, 0}), // left
    glm::lookAt(glm::vec3(), { 0, 1, 0}, {0, 0,-1}), // top
    glm::lookAt(glm::vec3(), { 0,-1, 0}, {0, 0, 1}), // bottom
};

void Canvas::pick_start()
{
    for (int i = 0; i < 6; i++)
        m_pick_ready[i] = false;
}
void Canvas::pick_update(int plane)
{
    // check if already updated
    if (m_pick_ready[plane])
        return;
    
    App::I->render_task([this, plane]
    {
        std::array<bool, 6> faces{ false };
        faces[plane] = true;
        draw_merge(true, faces);

        int i = plane;
        if (!m_pick_data[plane])
            m_pick_data[plane] = std::make_unique<glm::u8vec4[]>(m_width * m_height);
        m_layers_merge.rtt(i).readPixelsRgba8(
            0,
            0,
            m_width,
            m_height,
            m_pick_data[plane].get());
    });
    
    m_pick_ready[plane] = true;
}
glm::vec4 Canvas::pick_get(glm::vec2 canvas_loc)
{
    glm::vec3 ray_origin;
    glm::vec3 ray_dir;
    glm::vec3 hit_pos;
    glm::vec3 hit_normal;
    glm::vec2 fb_pos;
    int plane_id;
    if (point_trace(canvas_loc, ray_origin, ray_dir, hit_pos, fb_pos, hit_normal, plane_id))
    {
        pick_update(plane_id);
        int i = (int)fb_pos.x + (int)fb_pos.y * m_width;
        return glm::vec4(m_pick_data[plane_id][i]) / 255.f;
    }
    return {0,0,0,1};
}
void Canvas::pick_end()
{
    for (int i = 0; i < 6; i++)
        m_pick_data[i].release();
}
void Canvas::clear(const glm::vec4& c/*={0,0,0,1}*/)
{
    auto a = new ActionLayerClear;
    a->m_layer = m_layers[m_current_layer_idx];
    a->m_frame = layer().m_frame_index;
    a->m_snap = std::make_shared<LayerFrame::Snapshot>(a->m_layer->snapshot());
    a->m_color = c;
    ActionManager::add(a);

    m_layers[m_current_layer_idx]->clear(c);
    m_unsaved = true;
}
void Canvas::clear_all()
{
    for (auto& l : m_layers)
        l->clear({0, 0, 0, 0});
}
void Canvas::stroke_end()
{
    if (!m_current_stroke)
        return;
    if (m_current_stroke->has_sample())
    {
        m_commit_delayed = true;
    }
    else
    {
        m_show_tmp = false;
        stroke_commit();
        m_current_stroke = nullptr;
    }
}
void Canvas::stroke_cancel()
{
    if (!m_current_stroke)
        return;
    m_current_stroke = nullptr;
    m_show_tmp = false;
}
void Canvas::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz)
{
    gl_state gl;
    gl.save();
    const auto mix_shell = make_canvas_stroke_mix_pass_shell(*this, bb_min, bb_sz);
    [[maybe_unused]] const auto mix_result = pp::panopainter::execute_legacy_canvas_stroke_mix_pass_shell(
        mix_shell.setup.begin,
        mix_shell.setup.end,
        mix_shell.request);

    gl.restore();
}

std::array<std::vector<vertex_t>, 6> Canvas::stroke_draw_project(std::array<vertex_t, 4>& B, bool project_3d /*= false*/, glm::mat4 mv /*= glm::mat4(1)*/) const
{
    // intersect P with the current face to clip diverging points from the plane
    const auto unp_vp = zw(m_box);
    const auto unp_inv = glm::inverse(m_proj * m_mv);
    std::array<std::vector<vertex_t>, 6> ret;
    for (int i = 0; i < 6; i++)
    {
        struct ray_t {
            glm::vec3 o;
            glm::vec3 d;
            vertex_t v;
            ray_t(glm::vec3 o, glm::vec3 d, vertex_t v) : o(o), d(d), v(v) { }
        };
        std::vector<ray_t> rays;
        if (project_3d)
        {
            rays.reserve(B.size());
            for (auto const& b : B)
                rays.emplace_back(glm::vec3(0), b.pos, b);
        }
        else
        {
            auto P = poly_intersect(B.data(), B.data() + 4, m_plane_shape[i]);
            rays.reserve(P.size());
            for (auto const& p : P)
            {
                glm::vec3 ray_origin, ray_dir;
                auto clip_space = glm::vec2(p.pos.x, unp_vp.y - p.pos.y - 1.f) / unp_vp * 2.f - 1.f;
                auto wp0 = unp_inv * glm::vec4(clip_space, 0, 1);
                auto wp1 = unp_inv * glm::vec4(clip_space, .5, 1);
                ray_origin = xyz(wp0 / wp0.w);
                ray_dir = glm::normalize(xyz(wp1 / wp1.w) - ray_origin);
                rays.emplace_back(ray_origin, ray_dir, p);
            }
        }
        glm::mat4 plane_camera = glm::lookAt(m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i]);
        std::vector<vertex_t> face_ret;
        face_ret.reserve(rays.size());
        for (auto const& r : rays)
        {
            glm::vec3 hit;
            float hit_t;
            if (ray_intersect(r.o, r.d, m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i], hit, hit_t))
            {
                glm::vec4 plane_local = plane_camera * glm::vec4(hit, 1);

                //P[j].uvs2 = xy(P[j].pos) / glm::vec2(App::I->width, App::I->height);
                vertex_t v;
                v.pos.x = -(plane_local.x * 0.5f - 0.5f) * m_width;
                v.pos.y = (plane_local.y * 0.5f + 0.5f) * m_height;

                // Black magic - BEWARE!
                // interpolation perspective correction, use the current camera projection to correct the interpolation
                // because the new shape will have z fixed with an ortho projection when drawn to the face
                // we need to imitate the same perspective as the once in the camera
                // see: https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/perspective-correct-interpolation-vertex-attributes
                auto hit_cam = mv * glm::vec4(hit, 1);
                v.pos.z = 0;
                v.pos.w = hit_cam.z;
                v.uvs = r.v.uvs * hit_cam.z;
                v.uvs2 = r.v.uvs2 * hit_cam.z;
                face_ret.emplace_back(v);
            }
            else
            {
                break;
            }
        }
        if (face_ret.size() >= 3)
            ret[i] = std::move(face_ret);
    }
    return ret;
}

static void execute_canvas_draw_merge_layer_composite(
    bool is_temporary_erase,
    bool is_temporary_paint,
    bool use_blend,
    const pp::panopainter::LegacyCanvasDrawMergeLayerCompositeExecution& execution)
{
    pp::panopainter::execute_legacy_canvas_draw_merge_layer_composite(
        is_temporary_erase,
        is_temporary_paint,
        use_blend,
        execution);
}

static void execute_canvas_draw_merge_layer_texture(
    const pp::panopainter::LegacyCanvasDrawMergeTextureAlphaUniforms& uniforms,
    const pp::panopainter::LegacyCanvasDrawMergeLayerTextureExecution& execution)
{
    pp::panopainter::execute_legacy_canvas_draw_merge_layer_texture(uniforms, execution);
}

static void execute_canvas_draw_merge_layer_blend(
    const pp::panopainter::LegacyCanvasDrawMergeLayerBlendUniforms& uniforms,
    const pp::panopainter::LegacyCanvasDrawMergeLayerBlendExecution& execution)
{
    pp::panopainter::execute_legacy_canvas_draw_merge_layer_blend(uniforms, execution);
}

static void execute_canvas_draw_merge_final_plane_composite(
    const pp::panopainter::LegacyCanvasDrawMergeFinalPlaneCompositeUniforms& uniforms,
    const pp::panopainter::LegacyCanvasDrawMergeFinalPlaneCompositeExecution& execution)
{
    pp::panopainter::execute_legacy_canvas_draw_merge_final_plane_composite(uniforms, execution);
}

static auto make_canvas_stroke_mix_pass_shell(
    Canvas& canvas,
    const glm::vec2& bb_min,
    const glm::vec2& bb_sz)
{
    return pp::panopainter::make_legacy_canvas_stroke_mix_pass_shell(
        [&] {
            canvas.m_mixer.bindFramebuffer();
            canvas.apply_canvas_viewport(0, 0, canvas.m_mixer.getWidth(), canvas.m_mixer.getHeight());
            canvas.apply_canvas_capability(canvas.depth_test_state(), false);
            canvas.apply_canvas_capability(canvas.scissor_test_state(), true);
            canvas.apply_canvas_capability(canvas.blend_state(), false);
            canvas.apply_canvas_scissor(
                static_cast<std::int32_t>(bb_min.x),
                static_cast<std::int32_t>(bb_min.y),
                static_cast<std::int32_t>(bb_sz.x),
                static_cast<std::int32_t>(bb_sz.y));
        },
        [&] {
            canvas.m_mixer.unbindFramebuffer();
        });
}

template <typename SetActiveTextureUnit>
static auto make_canvas_stroke_commit_callbacks(
    Canvas& canvas,
    const glm::vec4& vp,
    const glm::vec4& cc,
    bool blend,
    SetActiveTextureUnit&& set_active_texture_unit,
    ActionStroke* action,
    const Stroke* current_stroke,
    const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence,
    const pp::paint_renderer::CanvasStrokeCommitMaterialPlan& stroke_material)
{
    const auto& b = current_stroke->m_brush;
    auto bind_commit_inputs = [&](int i) {
        pp::panopainter::bind_legacy_canvas_stroke_commit_face_inputs(
            sequence,
            [&](int texture_slot) {
                set_active_texture_unit(texture_slot);
            },
            [&](pp::paint_renderer::CanvasStrokeCommitTextureRole role) {
                switch (role) {
                case pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch:
                    canvas.m_tex2[i].bind();
                    break;
                case pp::paint_renderer::CanvasStrokeCommitTextureRole::stroke:
                    canvas.m_tmp[i].bindTexture();
                    break;
                case pp::paint_renderer::CanvasStrokeCommitTextureRole::selection_mask:
                    canvas.m_smask.rtt(i).bindTexture();
                    break;
                case pp::paint_renderer::CanvasStrokeCommitTextureRole::dual_stroke:
                    canvas.m_tmp_dual[i].bindTexture();
                    break;
                case pp::paint_renderer::CanvasStrokeCommitTextureRole::pattern:
                    b->m_pattern_texture ? b->m_pattern_texture->bind() : unbind_texture_2d();
                    break;
                }
            },
            [&](pp::paint_renderer::CanvasStrokeCommitTextureRole role, int texture_slot) {
                switch (role) {
                case pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch:
                    canvas.m_sampler.bind(texture_slot);
                    break;
                case pp::paint_renderer::CanvasStrokeCommitTextureRole::stroke:
                    canvas.m_sampler_nearest.bind(texture_slot);
                    break;
                case pp::paint_renderer::CanvasStrokeCommitTextureRole::selection_mask:
                case pp::paint_renderer::CanvasStrokeCommitTextureRole::dual_stroke:
                    canvas.m_sampler.bind(texture_slot);
                    break;
                case pp::paint_renderer::CanvasStrokeCommitTextureRole::pattern:
                    canvas.m_sampler_stencil.bind(texture_slot);
                    break;
                }
            });
    };
    return pp::panopainter::make_legacy_canvas_stroke_commit_callbacks(
        [&]() {
            canvas.m_dirty = false;
            canvas.m_dirty_stroke = true; // new stroke ready for timelapse capture
            App::I->redraw = true;
            canvas.m_unsaved = true;
            App::I->title_update();
        },
        []() {},
        [&]() {
            canvas.apply_canvas_viewport(0, 0, canvas.m_width, canvas.m_height);
            canvas.apply_canvas_capability(canvas.blend_state(), false);
        },
        [&]() {
            blend ? canvas.apply_canvas_capability(canvas.blend_state(), true) : canvas.apply_canvas_capability(canvas.blend_state(), false);
            canvas.apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
            canvas.apply_canvas_clear_color(cc);
            set_active_texture_unit(0);
        },
        [&]() { stamp_canvas_stroke_commit_action(canvas, action); },
        [&]() {
            canvas.stroke_commit_timelapse();
        },
        [&](int i) { capture_canvas_stroke_commit_layer_state(canvas, action, i); },
        [&](int i) { apply_canvas_stroke_commit_dirty_mutation(canvas, i); },
        [&](int i) { copy_canvas_stroke_commit_layer_image(canvas, i); },
        [&](int i) {
            bind_commit_inputs(i);
        },
        [&](int) {
            pp::panopainter::execute_legacy_canvas_stroke_commit_erase(
                [&]() {
                    pp::panopainter::setup_legacy_stroke_erase_shader(
                        pp::panopainter::LegacyStrokeEraseUniforms {
                            .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
                            .texture_slot = 0,
                            .stroke_texture_slot = 1,
                            .mask_texture_slot = 2,
                            .alpha = 1.0f,
                            .mask_enabled = canvas.m_smask_active,
                        });
                },
                [&]() {
                    canvas.m_plane.draw_fill();
                });
        },
        [&](int) {
            glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale);
            if (b->m_pattern_flipx) patt_scale.x *= -1.f;
            if (b->m_pattern_flipy) patt_scale.y *= -1.f;

            pp::panopainter::execute_legacy_canvas_stroke_commit_paint(
                [&]() {
                    pp::panopainter::setup_legacy_stroke_composite_shader(
                        pp::panopainter::LegacyStrokeCompositeUniforms {
                            .resolution = canvas.m_size,
                            .pattern = {
                                .scale = patt_scale,
                                .invert = static_cast<float>(b->m_pattern_invert),
                                .brightness = b->m_pattern_brightness,
                                .contrast = b->m_pattern_contrast,
                                .depth = b->m_pattern_depth,
                                .blend_mode = b->m_pattern_blend_mode,
                                .offset = canvas.m_pattern_offset,
                            },
                            .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
                            .layer_alpha = 1.0f,
                            .alpha_lock = canvas.m_layers[canvas.m_current_layer_idx]->m_alpha_locked,
                            .mask_enabled = canvas.m_smask_active,
                            .use_fragcoord = false,
                            .blend_mode = b->m_blend_mode,
                            .use_dual = stroke_material.composite_pass.use_dual,
                            .dual_blend_mode = stroke_material.composite_pass.dual_blend_mode,
                            .dual_alpha = stroke_material.composite_pass.dual_alpha,
                            .use_pattern = stroke_material.composite_pass.use_pattern,
                        });
                },
                [&]() {
                    canvas.m_plane.draw_fill();
                });
        },
        [&](int i) {
            pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source(
                sequence,
                [&]() {
                    pp::panopainter::setup_legacy_stroke_dilate_shader(
                        pp::panopainter::LegacyStrokeDilateUniforms {
                            .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
                        });
                },
                [&](int texture_slot) {
                    set_active_texture_unit(texture_slot);
                },
                [&]() {
                    canvas.m_tex2[i].bind();
                },
                [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
                    copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height);
                },
                pp::panopainter::LegacyCanvasStrokeCommitCopyExtent {
                    .width = canvas.m_width,
                    .height = canvas.m_height,
                });
        },
        [&](int) {
            pp::panopainter::execute_legacy_canvas_stroke_commit_dilate([&]() {
                canvas.m_plane.draw_fill();
            });
        },
        [&](int i) {
            canvas.m_layers[canvas.m_current_layer_idx]->rtt(i).unbindFramebuffer();
        });
}

glm::vec4 Canvas::stroke_draw_samples(
    int i,
    std::vector<vertex_t>& P,
    bool copy_stroke_destination)
{
    constexpr std::array destination_texture_binding {
        pp::panopainter::LegacyCanvasStrokeTextureBinding {
            .input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination,
            .slot = 1,
        },
    };
    const auto result = pp::panopainter::execute_legacy_canvas_stroke_face_sample_polygon(
        make_stroke_draw_samples_request(
            i,
            P,
            copy_stroke_destination),
        destination_texture_binding,
        make_stroke_draw_samples_destination_texture_dispatch(i));

    return result.dirty_bounds;
}

pp::panopainter::LegacyCanvasStrokeTextureInputDispatch Canvas::make_stroke_draw_samples_destination_texture_dispatch(
    int face_index)
{
    return pp::panopainter::make_legacy_canvas_stroke_destination_texture_dispatch(
        [&](int texture_slot) {
            set_active_texture_unit(texture_slot);
        },
        [&, face_index] {
            m_tex[face_index].bind(); // bg, copy of framebuffer (copied before drawing)
        },
        [&, face_index] {
            m_tex[face_index].unbind();
        });
}

pp::panopainter::LegacyStrokeFaceSamplePolygonExecutionRequest Canvas::make_stroke_draw_samples_request(
    int face_index,
    std::vector<vertex_t>& polygon_vertices,
    bool copy_stroke_destination) const
{
    return pp::panopainter::LegacyStrokeFaceSamplePolygonExecutionRequest {
        .context = "Canvas::stroke_draw_samples",
        .target_size = { m_width, m_height },
        .polygon_vertices = polygon_vertices,
        .face_index = face_index,
        .copy_stroke_destination = copy_stroke_destination,
        .copy_framebuffer_to_destination_texture = [](
            int,
            int src_x,
            int src_y,
            int dst_x,
            int dst_y,
            int width,
            int height) {
            copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height);
        },
        .upload_brush_vertices = [&](int, std::span<const vertex_t> vertices) {
            m_brush_shape.update_vertices(
                const_cast<vertex_t*>(vertices.data()),
                static_cast<int>(vertices.size()));
        },
        .draw_brush_shape = [&](int) {
            m_brush_shape.draw_fill();
        },
    };
}

template <typename BuildRequest>
static auto execute_canvas_stroke_commit_sequence(
    BuildRequest&& build_request)
{
    return pp::panopainter::execute_legacy_canvas_stroke_commit_sequence(
        build_request());
}

template <typename SetActiveTextureUnit>
static auto make_canvas_stroke_commit_request(
    Canvas& canvas,
    const glm::vec4& vp,
    const glm::vec4& cc,
    bool blend,
    SetActiveTextureUnit&& set_active_texture_unit,
    ActionStroke* action,
    const Stroke* current_stroke,
    const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence,
    const pp::paint_renderer::CanvasStrokeCommitMaterialPlan& stroke_material)
{
    const auto commit_callbacks = make_canvas_stroke_commit_callbacks(
        canvas,
        vp,
        cc,
        blend,
        std::forward<SetActiveTextureUnit>(set_active_texture_unit),
        action,
        current_stroke,
        sequence,
        stroke_material);

    const std::array<pp::panopainter::LegacyCanvasStrokeCommitFace, 6> faces {
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = canvas.m_dirty_face[0] },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = canvas.m_dirty_face[1] },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = canvas.m_dirty_face[2] },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 3, .dirty = canvas.m_dirty_face[3] },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 4, .dirty = canvas.m_dirty_face[4] },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 5, .dirty = canvas.m_dirty_face[5] },
    };

    return pp::panopainter::make_legacy_canvas_stroke_commit_request(faces, sequence, commit_callbacks);
}

static void stamp_canvas_stroke_commit_action(
    Canvas& canvas,
    ActionStroke* action)
{
    action->m_layer_idx = canvas.m_current_layer_idx;
    action->m_frame_idx = canvas.layer().m_frame_index;
    action->m_canvas = &canvas;
    ActionManager::add(action);
}

static void capture_canvas_stroke_commit_layer_state(
    Canvas& canvas,
    ActionStroke* action,
    int i)
{
    canvas.m_layers[canvas.m_current_layer_idx]->rtt(i).bindFramebuffer();

    glm::vec2 box_sz = zw(canvas.m_dirty_box[i]) - xy(canvas.m_dirty_box[i]);
    action->m_image[i] = std::make_unique<uint8_t[]>(
        static_cast<std::size_t>(box_sz.x * box_sz.y * 4));
    canvas.m_layers[canvas.m_current_layer_idx]->rtt(i).readPixelsRgba8(
        static_cast<int>(canvas.m_dirty_box[i].x),
        static_cast<int>(canvas.m_dirty_box[i].y),
        static_cast<int>(box_sz.x),
        static_cast<int>(box_sz.y),
        action->m_image[i].get());

    action->m_box[i] = canvas.m_dirty_box[i];
    action->m_old_box[i] = canvas.m_layers[canvas.m_current_layer_idx]->box(i);
    action->m_old_dirty[i] = canvas.m_layers[canvas.m_current_layer_idx]->face(i);
}

static void apply_canvas_stroke_commit_dirty_mutation(
    Canvas& canvas,
    int i)
{
    if (!canvas.m_layers[canvas.m_current_layer_idx]->m_alpha_locked) {
        auto& lbox = canvas.m_layers[canvas.m_current_layer_idx]->box(i);
        lbox = glm::vec4(
            glm::min(xy(canvas.m_dirty_box[i]), xy(lbox)),
            glm::max(zw(canvas.m_dirty_box[i]), zw(lbox)));
    }
    canvas.m_layers[canvas.m_current_layer_idx]->face(i) = true;
}

static void copy_canvas_stroke_commit_layer_image(
    Canvas& canvas,
    int i)
{
    set_active_texture_unit(0);
    canvas.m_tex2[i].bind();
    copy_framebuffer_to_texture_2d(0, 0, 0, 0, canvas.m_width, canvas.m_height);
    canvas.m_tex2[i].unbind();
}

std::vector<Canvas::StrokeFrame> Canvas::stroke_draw_compute(Stroke& stroke) const
{
    auto samples = stroke.compute_samples();
    return pp::panopainter::plan_legacy_canvas_stroke_frames(
        pp::panopainter::LegacyCanvasStrokeComputeRequest {
            .previous_sample = stroke.m_prev_sample,
            .samples = samples,
            .zoom = App::I->zoom,
            .mixer_size = glm::vec2(App::I->width, App::I->height),
            .model_view = m_mv,
        },
        [&](std::array<vertex_t, 4>& brush_quad, bool project_3d, glm::mat4 model_view) {
            return stroke_draw_project(brush_quad, project_3d, model_view);
        },
        [](
            glm::vec4 mixer_rect,
            glm::vec4 color,
            float flow,
            float opacity,
            std::array<std::vector<vertex_t>, 6>&& shapes) {
            return StrokeFrame {
                .col = color,
                .flow = flow,
                .opacity = opacity,
                .shapes = std::move(shapes),
                .m_mixer_rect = mixer_rect,
            };
        });
}

void Canvas::stroke_draw_pad_pass(
    const std::array<pp::panopainter::LegacyCanvasStrokePadFace, 6>& pad_faces,
    bool copy_stroke_destination,
    const std::array<pp::panopainter::LegacyCanvasStrokeTextureBinding, 1>& pad_destination_texture_binding,
    const pp::panopainter::LegacyCanvasStrokeTextureInputDispatch& pad_destination_texture_dispatch,
    const pp::renderer::Extent2D& stroke_extent,
    const glm::vec4& pad_color)
{
    pp::panopainter::setup_legacy_stroke_pad_shader(
        pp::panopainter::LegacyStrokePadUniforms {
            .color = pad_color,
            .uses_destination_feedback = copy_stroke_destination,
        });
    [[maybe_unused]] const auto pad_result = pp::panopainter::execute_legacy_canvas_stroke_pad_face_callbacks(
        pad_faces,
        stroke_extent,
        copy_stroke_destination,
        [&](std::span<const vertex_t> pad_quad) {
            m_brush_shape.update_vertices(
                const_cast<vertex_t*>(pad_quad.data()),
                static_cast<int>(pad_quad.size()));
        },
        [&](int face_index) {
            m_tmp[face_index].bindFramebuffer();
        },
        [&](int) {
            pp::panopainter::bind_legacy_canvas_stroke_texture_inputs(
                pad_destination_texture_binding,
                pad_destination_texture_dispatch);
        },
        [&](const pp::paint_renderer::CanvasStrokeCopyRegion& copy_region) {
            pp::panopainter::execute_legacy_canvas_stroke_pad_copy_region(
                copy_region,
                copy_framebuffer_to_texture_2d);
        },
        [&](int) {
            pp::panopainter::unbind_legacy_canvas_stroke_texture_inputs(
                pad_destination_texture_binding,
                pad_destination_texture_dispatch);
        },
        [&] {
            m_brush_shape.draw_fill();
        },
        [&](int face_index) {
            m_tmp[face_index].unbindFramebuffer();
        });
}

void Canvas::stroke_draw_pad_face_orchestration(
    const std::array<bool, 6>& box_dirty,
    const std::array<glm::vec4, 6>& box_face,
    bool copy_stroke_destination,
    const pp::renderer::Extent2D& stroke_extent,
    const glm::vec4& pad_color)
{
    stroke_draw_pad_face_callback_body(
        box_dirty,
        box_face,
        copy_stroke_destination,
        stroke_extent,
        pad_color);
}

void Canvas::stroke_draw_pad_face_callback_body(
    const std::array<bool, 6>& box_dirty,
    const std::array<glm::vec4, 6>& box_face,
    bool copy_stroke_destination,
    const pp::renderer::Extent2D& stroke_extent,
    const glm::vec4& pad_color)
{
    pp::panopainter::setup_legacy_stroke_pad_shader(
        pp::panopainter::LegacyStrokePadUniforms {
            .color = pad_color,
            .uses_destination_feedback = copy_stroke_destination,
        });
    const auto pad_faces = pp::panopainter::make_legacy_canvas_stroke_pad_faces(box_dirty, box_face);
    constexpr std::array pad_destination_texture_binding {
        pp::panopainter::LegacyCanvasStrokeTextureBinding {
            .input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination,
            .slot = 1,
        },
            };
    const auto pad_destination_texture_dispatch =
        pp::panopainter::make_legacy_canvas_stroke_pad_destination_texture_dispatch(
            [&](int texture_slot) {
                set_active_texture_unit(texture_slot);
            },
            [&](int dst_face_index) {
                m_tex[dst_face_index].bind();
            },
            [&](int dst_face_index) {
                m_tex[dst_face_index].unbind();
            },
            0);
    stroke_draw_pad_pass(
        pad_faces,
        copy_stroke_destination,
        pad_destination_texture_binding,
        pad_destination_texture_dispatch,
        stroke_extent,
        pad_color);
}

void Canvas::draw_merge_temporary_paint_branch(
    int layer_index,
    int plane_index,
    std::shared_ptr<Layer> layer,
    const Brush& brush,
    const glm::mat4& ortho,
    bool copy_blend_destination,
    bool draw_checkerboard)
{
    (void)draw_checkerboard;
    const auto stroke_material = canvas_stroke_material_plan(*brush, false);
    glm::vec2 patt_scale = glm::vec2(brush->m_pattern_scale);
    if (brush->m_pattern_flipx) patt_scale.x *= -1.f;
    if (brush->m_pattern_flipy) patt_scale.y *= -1.f;

    const auto execution = pp::panopainter::make_legacy_canvas_draw_merge_temporary_paint_composite(
        [&] {
            pp::panopainter::setup_legacy_stroke_composite_shader(
                pp::panopainter::LegacyStrokeCompositeUniforms {
                    .resolution = Canvas::I->m_size,
                    .pattern = {
                        .scale = patt_scale,
                        .invert = static_cast<float>(brush->m_pattern_invert),
                        .brightness = brush->m_pattern_brightness,
                        .contrast = brush->m_pattern_contrast,
                        .depth = brush->m_pattern_depth,
                        .blend_mode = brush->m_pattern_blend_mode,
                        .offset = Canvas::I->m_pattern_offset,
                    },
                    .mvp = ortho,
                    .layer_alpha = layer->m_opacity,
                    .alpha_lock = layer->m_alpha_locked,
                    .mask_enabled = m_smask_active,
                    .use_fragcoord = false,
                    .blend_mode = brush->m_blend_mode,
                    .use_dual = stroke_material.composite_pass.use_dual,
                    .dual_blend_mode = stroke_material.composite_pass.dual_blend_mode,
                    .dual_alpha = stroke_material.composite_pass.dual_alpha,
                    .use_pattern = stroke_material.composite_pass.use_pattern,
                });
        },
        [&] {
            m_sampler.bind(0);
            m_sampler.bind(1);
            m_sampler.bind(2);
            m_sampler.bind(3);
            m_sampler_stencil.bind(4);
        },
        [&] {
            set_active_texture_unit(0);
            m_layers[layer_index]->rtt(plane_index).bindTexture();
            set_active_texture_unit(1);
            m_tmp[plane_index].bindTexture();
            set_active_texture_unit(2);
            m_smask.rtt(plane_index).bindTexture();
            set_active_texture_unit(3);
            if (stroke_material.composite_pass.use_dual)
                m_tmp_dual[plane_index].bindTexture();
            set_active_texture_unit(4);
            brush->m_pattern_texture ? brush->m_pattern_texture->bind() : unbind_texture_2d();
        },
        [&] {
            m_plane.draw_fill();
        },
        [&] {
            set_active_texture_unit(3);
            if (stroke_material.composite_pass.use_dual)
                m_tmp_dual[plane_index].unbindTexture();
            set_active_texture_unit(2);
            m_smask.rtt(plane_index).unbindTexture();
            set_active_texture_unit(1);
            m_tmp[plane_index].unbindTexture();
            set_active_texture_unit(0);
            m_layers[layer_index]->rtt(plane_index).unbindTexture();
        });
    pp::panopainter::execute_legacy_canvas_draw_merge_temporary_composite(execution);
}

void Canvas::draw_merge_branch_orchestration(
    int plane_index,
    int layer_index,
    const std::shared_ptr<Layer>& layer,
    const Brush& brush,
    const glm::mat4& ortho,
    bool use_blend,
    bool copy_blend_destination,
    bool draw_checkerboard)
{
    if (!(m_show_tmp && m_current_layer_idx == layer_index) &&
        (!layer->m_visible ||
            layer->m_opacity == .0f ||
            !layer->face(plane_index)))
        return;

    if (use_blend)
    {
        m_merge_rtt.bindFramebuffer();
        m_merge_rtt.clear();
    }

    execute_canvas_draw_merge_layer_composite(
        m_current_stroke && m_current_mode == kCanvasMode::Erase && m_show_tmp && m_current_layer_idx == layer_index,
        m_current_stroke && m_show_tmp && m_current_layer_idx == layer_index,
        use_blend,
        pp::panopainter::LegacyCanvasDrawMergeLayerCompositeExecution {
            .execute_temporary_erase = [&] {
                pp::panopainter::execute_legacy_canvas_draw_merge_temporary_composite(
                    pp::panopainter::make_legacy_canvas_draw_merge_temporary_erase_composite(
                        [&] {
                            pp::panopainter::setup_legacy_stroke_erase_shader(
                                pp::panopainter::LegacyStrokeEraseUniforms {
                                    .mvp = ortho,
                                    .texture_slot = 0,
                                    .stroke_texture_slot = 1,
                                    .mask_texture_slot = 2,
                                    .alpha = layer->m_opacity,
                                    .mask_enabled = m_smask_active,
                                });
                        },
                        [&] {
                            m_sampler.bind(0);
                            m_sampler.bind(1);
                            m_sampler.bind(2);
                        },
                        [&] {
                            set_active_texture_unit(0);
                            m_layers[layer_index]->rtt(plane_index).bindTexture();
                            set_active_texture_unit(1);
                            m_tmp[plane_index].bindTexture();
                            set_active_texture_unit(2);
                            m_smask.rtt(plane_index).bindTexture();
                        },
                        [&] {
                            m_plane.draw_fill();
                        },
                        [&] {
                            m_smask.rtt(plane_index).unbindTexture();
                            set_active_texture_unit(1);
                            m_tmp[plane_index].unbindTexture();
                            set_active_texture_unit(0);
                            m_layers[layer_index]->rtt(plane_index).unbindTexture();
                        }));
            },
            .execute_temporary_paint = [&] {
                draw_merge_temporary_paint_branch(
                    layer_index,
                    plane_index,
                    layer,
                    brush,
                    ortho,
                    copy_blend_destination,
                    draw_checkerboard);
            },
            .execute_layer_texture = [&] {
                execute_canvas_draw_merge_layer_texture(
                    pp::panopainter::LegacyCanvasDrawMergeTextureAlphaUniforms {
                        .mvp = ortho,
                        .texture_slot = 0,
                        .alpha = layer->m_opacity,
                        .highlight = layer->m_hightlight,
                    },
                    pp::panopainter::LegacyCanvasDrawMergeLayerTextureExecution {
                        .bind_sampler = [&] {
                            m_cam_fov < 20.f ? m_sampler_nearest.bind(0) : m_sampler.bind(0);
                        },
                        .bind_layer_texture = [&] {
                            set_active_texture_unit(0);
                            m_layers[layer_index]->rtt(plane_index).bindTexture();
                        },
                        .draw = [&] {
                            m_plane.draw_fill();
                        },
                        .unbind_layer_texture = [&] {
                            m_layers[layer_index]->rtt(plane_index).unbindTexture();
                        },
                    });
            },
            .execute_layer_blend = [&] {
                execute_canvas_draw_merge_layer_blend(
                    pp::panopainter::LegacyCanvasDrawMergeLayerBlendUniforms {
                        .shader = {
                            .mvp = ortho,
                            .texture_slot = 0,
                            .destination_texture_slot = 2,
                            .use_destination_texture = copy_blend_destination,
                            .blend_mode = layer->m_blend_mode,
                            .alpha = 1.f,
                        },
                        .copy_destination = copy_blend_destination,
                    },
                    pp::panopainter::LegacyCanvasDrawMergeLayerBlendExecution {
                        .unbind_merge_framebuffer = [&] {
                            m_merge_rtt.unbindFramebuffer();
                        },
                        .bind_samplers = [&] {
                            m_sampler.bind(0);
                            m_sampler.bind(2);
                        },
                        .bind_merge_texture = [&] {
                            set_active_texture_unit(0);
                            m_merge_rtt.bindTexture();
                        },
                        .bind_destination_texture = [&] {
                            set_active_texture_unit(2);
                            m_merge_tex.bind();
                        },
                        .copy_destination_framebuffer = [&] {
                            copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height);
                        },
                        .draw = [&] {
                            m_plane.draw_fill();
                        },
                        .unbind_destination_texture = [&] {
                            set_active_texture_unit(2);
                            m_merge_tex.unbind();
                        },
                        .unbind_merge_texture = [&] {
                            set_active_texture_unit(0);
                            m_merge_rtt.unbindTexture();
                        },
                    });
            },
        });
}

void Canvas::draw_merge_final_plane_composite(
    const glm::mat4& ortho,
    bool draw_checkerboard)
{
    execute_canvas_draw_merge_final_plane_composite(
        pp::panopainter::LegacyCanvasDrawMergeFinalPlaneCompositeUniforms {
            .checkerboard = {
                .mvp = ortho,
                .colorize = false,
            },
            .texture = {
                .mvp = ortho,
                .texture_slot = 0,
            },
            .draw_checkerboard = draw_checkerboard,
        },
        pp::panopainter::LegacyCanvasDrawMergeFinalPlaneCompositeExecution {
            .bind_merged_texture_copy_target = [&] {
                set_active_texture_unit(2);
                m_merge_tex.bind();
            },
            .copy_merged_framebuffer = [&] {
                copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height);
            },
            .enable_blend = [&] {
                apply_canvas_capability(blend_state(), true);
            },
            .draw = [&] {
                m_plane.draw_fill();
            },
            .bind_sampler = [&] {
                m_sampler.bind(0);
            },
            .bind_merged_texture = [&] {
                set_active_texture_unit(0);
                m_merge_tex.bind();
            },
            .unbind_merged_texture = [&] {
                m_merge_tex.unbind();
            },
        });
}

void Canvas::stroke_draw_dual_pass(
    const std::vector<StrokeFrame>& frames_dual,
    const std::array<pp::panopainter::LegacyCanvasStrokeTextureBinding, 1>& dual_pass_texture_bindings,
    const pp::panopainter::LegacyCanvasStrokeTextureInputDispatch& dual_pass_brush_tip_dispatch,
    const pp::renderer::Extent2D& stroke_extent,
    const std::array<bool, 6>& include_dual_dirty,
    bool uses_pattern,
    bool copy_stroke_destination)
{
    [[maybe_unused]] const auto dual_result =
        pp::panopainter::execute_legacy_canvas_stroke_dual_pass(
            make_stroke_draw_dual_pass_request(
                frames_dual,
                dual_pass_texture_bindings,
                dual_pass_brush_tip_dispatch,
                stroke_extent,
                include_dual_dirty,
                uses_pattern,
                copy_stroke_destination));
}

pp::panopainter::LegacyCanvasStrokeDualPassRequest Canvas::make_stroke_draw_dual_pass_request(
    const std::vector<StrokeFrame>& frames_dual,
    const std::array<pp::panopainter::LegacyCanvasStrokeTextureBinding, 1>& dual_pass_texture_bindings,
    const pp::panopainter::LegacyCanvasStrokeTextureInputDispatch& dual_pass_brush_tip_dispatch,
    const pp::renderer::Extent2D& stroke_extent,
    const std::array<bool, 6>& include_dual_dirty,
    bool uses_pattern,
    bool copy_stroke_destination)
{
    return pp::panopainter::LegacyCanvasStrokeDualPassRequest {
        .context = "Canvas::stroke_draw",
        .bind_brush_tip = [&] {
            pp::panopainter::bind_legacy_canvas_stroke_texture_inputs(
                dual_pass_texture_bindings,
                dual_pass_brush_tip_dispatch);
        },
        .unbind_brush_tip = [&] {
            pp::panopainter::unbind_legacy_canvas_stroke_texture_inputs(
                dual_pass_texture_bindings,
                dual_pass_brush_tip_dispatch);
        },
        .setup_dual_shader = [&] {
            pp::panopainter::setup_legacy_canvas_stroke_dual_shader(
                uses_pattern);
        },
        .execute_frame_pass = [&] {
            stroke_draw_dual_pass_frame_pass(
                frames_dual,
                stroke_extent,
                include_dual_dirty,
                copy_stroke_destination);
        },
    };
}

void Canvas::stroke_draw_dual_pass_frame_pass(
    const std::vector<StrokeFrame>& frames_dual,
    const pp::renderer::Extent2D& stroke_extent,
    const std::array<bool, 6>& include_dual_dirty,
    bool copy_stroke_destination)
{
    pp::panopainter::execute_legacy_canvas_stroke_dual_pass_frame_callbacks(
        frames_dual,
        stroke_extent,
        std::span<glm::vec4>(m_dirty_box),
        std::span<glm::vec4>(),
        std::span<const bool>(include_dual_dirty),
        [&](auto& f) {
            pp::panopainter::apply_legacy_stroke_sample_uniforms(
                pp::panopainter::LegacyStrokeSampleUniforms {
                    .color = f.col,
                    .alpha = f.flow,
                    .opacity = f.opacity,
                });
        },
        [](auto&, int, auto&) {},
        [&](auto&, int i, auto& P) {
            return stroke_draw_samples(i, P, copy_stroke_destination);
        },
        m_tmp_dual,
        true);
}

void Canvas::stroke_draw()
{
    if (!(m_current_stroke && m_current_stroke->has_sample()))
    {
        stroke_commit_timelapse();
        //stroke_draw_mix({ 0,0 }, { m_mixer.getWidth(), m_mixer.getHeight() });
        return;
    }

    m_dirty = true;

    const auto vp = query_canvas_viewport();
    const auto cc = query_canvas_clear_color();
    
    const auto& brush = m_current_stroke->m_brush;
    auto ortho_proj = glm::ortho(0.f, (float)m_width, 0.f, (float)m_height, -1.f, 1.f);

    apply_canvas_viewport(0, 0, m_width, m_height);

    glm::vec2 patt_scale = glm::vec2(brush->m_pattern_scale);
    if (brush->m_pattern_flipx) patt_scale.x *= -1.f;
    if (brush->m_pattern_flipy) patt_scale.y *= -1.f;

    const auto stroke_rasterization = canvas_stroke_rasterization_plan(m_width, m_height);
    const bool copy_stroke_destination = stroke_rasterization.copy_stroke_destination;
    const auto stroke_material = canvas_stroke_material_plan(*brush, copy_stroke_destination);
    const auto stroke_extent = canvas_stroke_extent(m_width, m_height);

    apply_canvas_capability(blend_state(), false);
    pp::panopainter::setup_legacy_stroke_shader(
        pp::panopainter::LegacyStrokeShaderSetupUniforms {
            .resolution = glm::vec2(static_cast<float>(m_width), static_cast<float>(m_height)),
            .pattern = {
                .scale = patt_scale,
                .invert = static_cast<float>(brush->m_pattern_invert),
                .brightness = brush->m_pattern_brightness,
                .contrast = brush->m_pattern_contrast,
                .depth = brush->m_pattern_depth,
                .blend_mode = brush->m_pattern_blend_mode,
                .offset = m_pattern_offset,
            },
            .mvp = ortho_proj,
            .uses_destination_feedback = stroke_material.stroke_pass.uses_destination_feedback,
            .uses_pattern = stroke_material.stroke_pass.uses_pattern,
            .mix_alpha = brush->m_tip_mix,
            .wet = brush->m_tip_wet,
            .noise = brush->m_tip_noise,
            .set_opacity = true,
            .opacity = brush->m_tip_opacity,
        });

    // DRAW MAIN BRUSH
    constexpr std::array main_pass_texture_bindings {
        pp::panopainter::LegacyCanvasStrokeTextureBinding {
            .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
            .slot = 0,
        },
        pp::panopainter::LegacyCanvasStrokeTextureBinding {
            .input = pp::panopainter::LegacyCanvasStrokeTextureInput::pattern,
            .slot = 2,
        },
        pp::panopainter::LegacyCanvasStrokeTextureBinding {
            .input = pp::panopainter::LegacyCanvasStrokeTextureInput::mixer,
            .slot = 3,
        },
    };
    constexpr std::array live_pass_sampler_bindings {
        pp::panopainter::LegacyCanvasStrokeTextureBinding {
            .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
            .slot = 0,
        },
        pp::panopainter::LegacyCanvasStrokeTextureBinding {
            .input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination,
            .slot = 1,
        },
        pp::panopainter::LegacyCanvasStrokeTextureBinding {
            .input = pp::panopainter::LegacyCanvasStrokeTextureInput::pattern,
            .slot = 2,
        },
        pp::panopainter::LegacyCanvasStrokeTextureBinding {
            .input = pp::panopainter::LegacyCanvasStrokeTextureInput::mixer,
            .slot = 3,
        },
    };
    constexpr std::array main_pass_texture_unbindings {
        pp::panopainter::LegacyCanvasStrokeTextureBinding {
            .input = pp::panopainter::LegacyCanvasStrokeTextureInput::mixer,
            .slot = 3,
        },
        pp::panopainter::LegacyCanvasStrokeTextureBinding {
            .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
            .slot = 0,
        },
    };
    const pp::panopainter::LegacyCanvasStrokeSamplerDispatch live_pass_sampler_dispatch =
        pp::panopainter::make_legacy_canvas_stroke_live_pass_sampler_dispatch(
            [&](int slot) {
                m_sampler_brush.bind(slot);
            },
            [&] {
                m_sampler_brush.unbind();
            },
            [&](int slot) {
                m_sampler_nearest.bind(slot);
            },
            [&] {
                m_sampler_nearest.unbind();
            },
            [&](int slot) {
                m_sampler_stencil.bind(slot);
            },
            [&] {
                m_sampler_stencil.unbind();
            },
            [&](int slot) {
                m_sampler.bind(slot);
            },
            [&] {
                m_sampler.unbind();
            });
    const auto main_pass_texture_dispatch = pp::panopainter::make_legacy_canvas_stroke_main_pass_texture_dispatch(
            [&](int texture_slot) {
                set_active_texture_unit(texture_slot);
            },
            [&] {
                brush->m_tip_texture->bind();
            },
            [&] {
                brush->m_tip_texture->unbind();
            },
            [&] {
                brush->m_pattern_texture ?
                    brush->m_pattern_texture->bind() :
                    unbind_texture_2d();
            },
            [&] {
                m_mixer.bindTexture();
            },
            [&] {
                m_mixer.unbindTexture();
            });
    auto frames = stroke_draw_compute(*m_current_stroke);
    
    std::array<glm::vec4, 6> box_face = SIXPLETTE(glm::vec4(m_size, 0, 0));
    std::array<bool, 6> box_dirty = SIXPLETTE(false);
    const std::array<bool, 6> include_main_dirty = SIXPLETTE(true);
    glm::vec4 pad_color;
    [[maybe_unused]] const auto main_pass_result =
        pp::panopainter::execute_legacy_canvas_stroke_main_pass(
            pp::panopainter::LegacyCanvasStrokeMainPassExecutionRequest {
                .context = "Canvas::stroke_draw",
                .bind_samplers = [&] {
                    pp::panopainter::bind_legacy_canvas_stroke_sampler_inputs(
                        live_pass_sampler_bindings,
                        live_pass_sampler_dispatch);
                },
                .bind_textures = [&] {
                    pp::panopainter::bind_legacy_canvas_stroke_texture_inputs(
                        main_pass_texture_bindings,
                        main_pass_texture_dispatch);
                },
                .execute_frame_pass = [&] {
                    pp::panopainter::execute_legacy_canvas_stroke_main_pass_frame_callbacks(
                        frames,
                        stroke_extent,
                        std::span<glm::vec4>(m_dirty_box),
                        std::span<glm::vec4>(box_face),
                        std::span<const bool>(include_main_dirty),
                        [&](auto& f) {
                            if (brush->m_tip_mix > 0.f)
                            {
                                stroke_draw_mix(xy(f.m_mixer_rect), zw(f.m_mixer_rect));
                            }
                            pad_color = f.col;
                        },
                        [&](auto&, int i, auto&) {
                            m_dirty_face[i] = true;
                            box_dirty[i] = true;
                        },
                        [&](auto& f, int i, auto& P) {
                            pp::panopainter::use_legacy_stroke_shader();
                            pp::panopainter::apply_legacy_stroke_sample_uniforms(
                                pp::panopainter::LegacyStrokeSampleUniforms {
                                    .color = f.col,
                                    .alpha = f.flow,
                                    .opacity = f.opacity,
                                });
                            return stroke_draw_samples(i, P, copy_stroke_destination);
                        },
                        m_tmp);
                },
                .unbind_textures = [&] {
                    pp::panopainter::unbind_legacy_canvas_stroke_texture_inputs(
                        main_pass_texture_unbindings,
                        main_pass_texture_dispatch);
                },
                .unbind_samplers = [&] {
                    pp::panopainter::unbind_legacy_canvas_stroke_sampler_inputs(
                        live_pass_sampler_bindings,
                        live_pass_sampler_dispatch);
                },
            });

    // pad stroke
    // In order to mitigate color bleeding at the edge of shapes in transparent layers
    // we need to fill the area around the stroke with the same color because by default
    // the transparent area may have black or other undefined color.
    // This step is only useful for previewing the stroke because on commit the dilate
    // algorithm fixes this issue.
    // NOTE: at the moment this works on the whole canvas, but it can be optimized 
    // to only affect the current dirty box. In this case it may be necessary to do this
    // work on documents that doesn't have the padding, so on document loading.
    stroke_draw_pad_face_orchestration(
        box_dirty,
        box_face,
        copy_stroke_destination,
        stroke_extent,
        pad_color);

    // DRAW DUAL BRUSH

    if (stroke_material.dual_pass.enabled && m_dual_stroke)
    {
        const auto& dual_brush = m_dual_stroke->m_brush;
        constexpr std::array dual_pass_texture_bindings {
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
                .slot = 0,
            },
        };
        const auto dual_pass_brush_tip_dispatch =
            pp::panopainter::make_legacy_canvas_stroke_brush_tip_texture_dispatch(
                [&](int texture_slot) {
                    set_active_texture_unit(texture_slot);
                },
                [&](int) {
                    dual_brush->m_tip_texture ?
                        dual_brush->m_tip_texture->bind() :
                        unbind_texture_2d();
                },
                [&](int) {
                    dual_brush->m_tip_texture ?
                        dual_brush->m_tip_texture->unbind() :
                        unbind_texture_2d();
                },
                0);
        auto frames_dual = stroke_draw_compute(*m_dual_stroke);
        const std::array<bool, 6> include_dual_dirty =
            SIXPLETTE(stroke_material.composite_pass.dual_blend_mode == 0);
        stroke_draw_dual_pass(
            frames_dual,
            dual_pass_texture_bindings,
            dual_pass_brush_tip_dispatch,
            stroke_extent,
            include_dual_dirty,
            stroke_material.dual_pass.uses_pattern,
            copy_stroke_destination);
    }

    pp::panopainter::unbind_legacy_canvas_stroke_sampler_inputs(
        live_pass_sampler_bindings,
        live_pass_sampler_dispatch);
    
    apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
    apply_canvas_clear_color(cc);

    if (m_commit_delayed)
    {
        m_show_tmp = false;
        m_commit_delayed = false;
        stroke_commit();
        m_current_stroke = nullptr;
    }
}
bool Canvas::point_trace(glm::vec2 loc, glm::vec3& ray_origin, glm::vec3& ray_dir,
    glm::vec3& hit_pos, glm::vec2& fb_pos, glm::vec3& hit_normal, int& out_plane_id)
{
    point_unproject(loc, { 0, 0, zw(m_box) }, m_mv, m_proj, ray_origin, ray_dir);
    glm::vec3 hit;
    float hit_t;
    for (int i = 0; i < 6; i++)
    {
        if (ray_intersect(ray_origin, ray_dir, m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i], hit, hit_t))
        {
            glm::mat4 plane_camera = glm::lookAt(m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i]);
            glm::vec4 plane_local = plane_camera * glm::vec4(hit, 1);
            if (glm::abs(plane_local.x) < 1.f && glm::abs(plane_local.y) < 1.f)
            {
                fb_pos.x = -(plane_local.x * 0.5f - 0.5f) * m_width;
                fb_pos.y = (plane_local.y * 0.5f + 0.5f) * m_height;
                hit_pos = hit;
                hit_normal = m_plane_normal[i];
                out_plane_id = i;
                return true;
            }
            else continue;
        }
        else continue;
    }
    return false;
}
/*
bool Canvas::point_trace_plane(glm::vec2 loc, glm::vec3& hit_pos, glm::vec2& hit_fb_pos, int plane_id)
{
    auto ln = (loc / zw(m_box)) * 2.f - 1.f;
    auto p = m_plane_unproject[plane_id] * glm::vec4(ln, 1, 1);
    if (p.w <= 0)
    {

        return true;
    }
    return false;
}
*/
bool Canvas::point_trace_plane(glm::vec2 loc, glm::vec3& ray_origin, glm::vec3& ray_dir,
    glm::vec3& hit_pos, glm::vec3& hit_normal, glm::vec2& hit_fb_pos, int plane_id)
{
    point_unproject(loc, { 0, 0, zw(m_box) }, m_mv, m_proj, ray_origin, ray_dir);
    glm::vec3 hit;
    float hit_t;
    if (ray_intersect(ray_origin, ray_dir, m_plane_origin[plane_id],
                      m_plane_normal[plane_id], m_plane_tangent[plane_id], hit, hit_t))
    {
        glm::mat4 plane_camera = glm::lookAt(m_plane_origin[plane_id], m_plane_normal[plane_id], m_plane_tangent[plane_id]);
        glm::vec4 plane_local = plane_camera * glm::vec4(hit, 1);
        hit_pos = hit;
        hit_normal = m_plane_normal[plane_id];
        hit_fb_pos.x = -(plane_local.x * 0.5f - 0.5f);
        hit_fb_pos.y = (plane_local.y * 0.5f + 0.5f);
        return true;
    }
    return false;
}
void Canvas::point_unproject(glm::vec2 loc, glm::vec4 vp, glm::mat4 camera, glm::mat4 proj,
    glm::vec3& out_origin, glm::vec3& out_dir)
{
    auto clip_space = glm::vec2(loc.x, vp.w - loc.y - 1.f) / zw(vp) * 2.f - 1.f;
    auto inv = glm::inverse(proj * camera);
    auto wp0 = inv * glm::vec4(clip_space, 0, 1);
    auto wp1 = inv * glm::vec4(clip_space, .5, 1);
    out_origin = xyz(wp0 / wp0.w);
    out_dir = glm::normalize(xyz(wp1 / wp1.w) - out_origin);
};
void Canvas::point_unproject(glm::vec2 loc, glm::vec3& out_origin, glm::vec3& out_dir)
{
    auto clip_space = glm::vec2(loc.x, m_vp.w - loc.y - 1.f) / zw(m_vp) * 2.f - 1.f;
    auto inv = glm::inverse(m_proj * m_mv);
    auto wp0 = inv * glm::vec4(clip_space, 0, 1);
    auto wp1 = inv * glm::vec4(clip_space, .5, 1);
    out_origin = xyz(wp0 / wp0.w);
    out_dir = glm::normalize(xyz(wp1 / wp1.w) - out_origin);
}
glm::vec3 Canvas::point_trace(glm::vec2 loc)
{
    glm::vec3 ray_origin;
    glm::vec3 ray_dir;
    glm::vec3 hit_pos;
    glm::vec3 hit_normal;
    glm::vec2 fb_pos;
    int plane_id;
    if (point_trace(loc, ray_origin, ray_dir, hit_pos, fb_pos, hit_normal, plane_id))
        return hit_pos;
    return glm::vec3(0);
}
void Canvas::stroke_commit()
{
    if (!m_dirty || m_layers.empty())
        return;
    
    // save viewport and clear color states
    const auto vp = query_canvas_viewport();
    const auto cc = query_canvas_clear_color();
    auto blend = query_canvas_capability(blend_state());
    
    // allocate action to add to history
    auto action = new ActionStroke;
    action->was_saved = !m_unsaved;

    const auto& b = m_current_stroke->m_brush;
    const auto stroke_material = canvas_stroke_material_plan(*b, false);
    [[maybe_unused]] const auto commit_result = execute_canvas_stroke_commit_sequence(
        [&]() {
            return make_canvas_stroke_commit_request(
                *this,
                vp,
                cc,
                blend,
                [&](int texture_slot) {
                    set_active_texture_unit(texture_slot);
                },
                action,
                m_current_stroke,
                pp::paint_renderer::plan_canvas_stroke_commit_sequence(
                    pp::paint_renderer::CanvasStrokeCommitRequest {
                        .erase_mode = m_current_mode == kCanvasMode::Erase,
                        .alpha_locked = m_layers[m_current_layer_idx]->m_alpha_locked,
                        .selection_mask_active = m_smask_active,
                        .dual_stroke_enabled = stroke_material.composite_pass.use_dual,
                        .pattern_enabled = stroke_material.composite_pass.use_pattern,
                    }),
                stroke_material);
        });
}

void Canvas::stroke_commit_timelapse()
{
    if (m_encoder && App::I->rec_running)
    {
        auto t_now = std::chrono::high_resolution_clock::now();
        float dt = std::chrono::duration<float>(t_now - m_disrty_stroke_time).count();
        if (dt > 2.f && m_dirty_stroke && App::I->rec_mutex.try_lock())
        {
            draw_merge(true);
            App::I->rec_mutex.unlock();
            App::I->rec_cv.notify_one();
            LOG("rec frame generated");
            m_dirty_stroke = false;
            m_disrty_stroke_time = std::chrono::steady_clock::now();
        }
    }
}

void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SIXPLETTE(false)*/)
{
    assert(App::I->is_render_thread());

    apply_canvas_viewport(0, 0, m_width, m_height);
    auto ortho = glm::ortho<float>(-0.5f, 0.5f, -0.5f, 0.5f, -1.f, 1.f);
    const auto& b = m_current_stroke->m_brush;

    const auto blend_gate = draw_merge_blend_gate_plan(
        m_width,
        m_height,
        m_layers,
        m_current_stroke ? m_current_stroke->m_brush.get() : nullptr);
    const bool use_blend = blend_gate.shader_blend;
    const bool copy_blend_destination = use_blend && !blend_gate.reads_destination_color;

    // if not using shader blend, use gl rasterizer blend
    apply_canvas_capability(depth_test_state(), false);

    for (int plane_index = 0; plane_index < 6; plane_index++)
    {
        if (!faces[plane_index])
            continue;

        m_layers_merge.rtt(plane_index).bindFramebuffer();
        pp::panopainter::execute_legacy_canvas_draw_merge_plane_setup(
            pp::panopainter::LegacyCanvasDrawMergePlaneSetupUniforms {
                .checkerboard = {
                    .mvp = ortho,
                    .colorize = false,
                },
                .use_blend = use_blend,
                .draw_checkerboard = draw_checkerboard,
            },
            pp::panopainter::LegacyCanvasDrawMergePlaneSetupExecution {
                .clear_plane = [&] {
                    m_layers_merge.rtt(plane_index).clear({ 1, 1, 1, 0 });
                },
                .disable_blend = [&] {
                    apply_canvas_capability(blend_state(), false);
                },
                .enable_blend = [&] {
                    apply_canvas_capability(blend_state(), true);
                },
                .draw = [&] {
                    m_plane.draw_fill();
                },
            });

        for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
        {
            draw_merge_branch_orchestration(
                plane_index,
                layer_index,
                m_layers[layer_index],
                *b,
                ortho,
                use_blend,
                copy_blend_destination,
                draw_checkerboard);
        }

        if (use_blend)
        {
            draw_merge_final_plane_composite(ortho, draw_checkerboard);
        }
        
        m_layers_merge.rtt(plane_index).unbindFramebuffer();
    }
}

void Canvas::stroke_update(glm::vec3 point, float pressure)
{
    m_current_stroke->add_point(point, pressure);
    if (m_dual_stroke)
        m_dual_stroke->add_point(point, pressure);
}
void Canvas::stroke_start(glm::vec3 point, float pressure)
{
    assert(App::I->is_render_thread());

    // need to commit this now before starting a new stroke
    if (m_current_stroke && m_commit_delayed)
    {
        m_show_tmp = false;
        m_commit_delayed = false;
        stroke_commit();
        m_current_stroke = nullptr;
        m_dual_stroke = nullptr;
    }
    
    m_pattern_offset = m_current_brush->m_pattern_rand_offset ?
        glm::vec2((rand()%1000)*0.001f, (rand()%1000)*0.001f) :
        glm::vec2(0);

    m_current_stroke = std::make_unique<Stroke>();
    m_current_stroke->m_camera.rot = m_cam_rot;
    m_current_stroke->m_camera.fov = m_cam_fov;
    if (m_current_mode == kCanvasMode::Line)
        m_current_stroke->m_filter_points = false;
    m_current_stroke->randomize_prng();
    m_current_stroke->start(m_current_brush);
    m_current_stroke->add_point(point, pressure);

    // Generate a brush for the dual-brush
    if (m_current_brush->m_dual_enabled)
    {
        auto dual_brush = std::make_shared<Brush>();
        dual_brush->m_tip_flow = m_current_brush->m_dual_flow;
        dual_brush->m_tip_opacity = m_current_brush->m_dual_opacity;
        dual_brush->m_tip_flipx = m_current_brush->m_dual_flipx;
        dual_brush->m_tip_flipy = m_current_brush->m_dual_flipy;
        dual_brush->m_tip_invert = m_current_brush->m_dual_invert;
        dual_brush->m_blend_mode = m_current_brush->m_dual_blend_mode;
        dual_brush->m_tip_randflipx = m_current_brush->m_dual_randflip;
        dual_brush->m_tip_randflipy = m_current_brush->m_dual_randflip;
        dual_brush->m_tip_size = m_current_brush->m_dual_size * m_current_brush->m_tip_size;
        dual_brush->m_tip_spacing = m_current_brush->m_dual_spacing;
        dual_brush->m_jitter_scatter = m_current_brush->m_dual_scatter;
        dual_brush->m_jitter_scatter_bothaxis = m_current_brush->m_dual_scatter_bothaxis;
        dual_brush->m_jitter_angle = m_current_brush->m_dual_rotate;
        dual_brush->m_tip_texture = m_current_brush->m_dual_texture;
        dual_brush->m_tip_aspect = m_current_brush->m_dual_aspect;
        m_dual_stroke = std::make_unique<Stroke>();
        m_dual_stroke->m_camera.rot = m_cam_rot;
        m_dual_stroke->m_camera.fov = m_cam_fov;
        m_dual_stroke->start(dual_brush);
        m_dual_stroke->add_point(point, pressure);
    }

    auto const& l = m_layers[m_current_layer_idx];
    for (int i = 0; i < 6; i++)
    {
        m_dirty_box[i] = glm::vec4(m_width, m_height, 0, 0); // reset bounding box
        m_dirty_face[i] = false;
        
        m_tmp[i].bindFramebuffer();
        m_tmp[i].clear({ 0, 0, 0, 0 });
        m_tmp[i].unbindFramebuffer();

        if (m_current_brush->m_dual_enabled)
        {
            m_tmp_dual[i].bindFramebuffer();
            m_tmp_dual[i].clear({ 0, 0, 0, 0 });
            m_tmp_dual[i].unbindFramebuffer();
        }
    }
    m_mixer.bindFramebuffer();
    m_mixer.clear();
    m_mixer.unbindFramebuffer();
    m_show_tmp = true;
}
void Canvas::layer_add(std::string name, std::shared_ptr<Layer> layer /*= nullptr*/, int index /*= 0*/)
{
    LOG("canvas layer_add %s", name.c_str());
    int idx = (int)m_layers.size();
    if (layer)
    {
        m_layers.insert(m_layers.begin() + index, layer);
    }
    else
    {
        m_layers.insert(m_layers.begin() + index, std::make_unique<Layer>());
        m_layers[index]->create(m_width, m_height, name);
    }
    m_current_layer_idx = index;
}

std::shared_ptr<Layer> Canvas::layer_with_id(uint32_t id)
{
    auto layer = std::find_if(m_layers.begin(), m_layers.end(), 
        [id](const auto& l) { return l->id == id; });
    if (layer != m_layers.end())
        return *layer;
    return nullptr;
}

void Canvas::layer_remove(int idx) // m_order index
{
    LOG("canvas layer_remove %d", idx);
    //m_layers[n]->destroy();
    m_layers.erase(m_layers.begin() + idx);
    m_current_layer_idx = std::min<int>((int)m_layers.size() - 1, idx);
}
void Canvas::layer_order(int idx, int pos) // m_order index
{
    auto from = m_layers.begin() + idx;
    auto to = m_layers.begin() + pos;

    if (pos < idx)
        std::rotate(to, from, from + 1);
    if (pos > idx)
        std::rotate(from, from + 1, to + 1);

    if (m_current_layer_idx == idx)
        m_current_layer_idx = pos;
    else if (m_current_layer_idx == pos)
        m_current_layer_idx = idx;
}
void Canvas::layer_merge(int source_idx, int dest_idx) // m_layer index
{
    m_dirty = false;

    App::I->render_task([&]
    {
        // prepare common states
        apply_canvas_viewport(0, 0, m_width, m_height);
        apply_canvas_capability(blend_state(), false);

        for (int i = 0; i < 6; i++)
        {
            if (!m_layers[source_idx]->face(i))
                continue; // no stroke on this face, skip it

            m_layers[dest_idx]->rtt(i).bindFramebuffer();

            auto& lbox = m_layers[dest_idx]->box(i);
            lbox = glm::vec4(
                glm::min(xy(m_layers[source_idx]->box(i)), xy(lbox)),
                glm::max(zw(m_layers[source_idx]->box(i)), zw(lbox))
            );
            m_layers[dest_idx]->face(i) = true;

            // copy to tmp2 for layer blending
            set_active_texture_unit(0);
            m_tex2[i].bind();
            copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height);
            m_tex2[i].unbind();

            m_sampler.bind(0);
            m_sampler_nearest.bind(1);
            {
                pp::panopainter::setup_legacy_stroke_composite_shader(
                    pp::panopainter::LegacyStrokeCompositeUniforms {
                        .resolution = m_size,
                        .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
                        .layer_alpha = m_layers[source_idx]->m_opacity,
                        .alpha_lock = false,
                        .mask_enabled = false,
                        .use_fragcoord = false,
                        .blend_mode = m_layers[source_idx]->m_blend_mode,
                        .use_dual = false,
                        .dual_alpha = 0.0f,
                        .use_pattern = false,
                    });

                set_active_texture_unit(0);
                m_tex2[i].bind();
                set_active_texture_unit(1);
                m_layers[source_idx]->rtt(i).bindTexture();
                m_plane.draw_fill();
                m_layers[source_idx]->rtt(i).unbindTexture();
                set_active_texture_unit(0);
                m_tex2[i].unbind();
            }

            m_layers[dest_idx]->rtt(i).unbindFramebuffer();
        }
    });
}

int Canvas::anim_duration() const noexcept
{
    int frames = 1;
    for (auto& l : m_layers)
        frames = glm::max(frames, l->total_duration());
    return frames;
}

void Canvas::anim_update() noexcept
{
    for (auto& l : m_layers)
        l->goto_frame(m_anim_frame);
}

void Canvas::anim_goto_frame(int frame) noexcept
{
    m_anim_frame = frame;
    for (auto& l : m_layers)
        l->goto_frame(frame);
}

void Canvas::anim_goto_next() noexcept
{
    anim_goto_frame((m_anim_frame + 1) % anim_duration());
}

void Canvas::anim_goto_prev() noexcept
{
    int k = anim_duration();
    anim_goto_frame((m_anim_frame - 1 + k) % k);
}

void Canvas::flood_fill(int layer, int plane, std::vector<glm::ivec2> pos, FloodData& plane_data,
    float threshold, glm::vec4 dest_color, std::unique_ptr<glm::vec4>& source_color)
{
    struct adj_t
    {
        int plane;
        bool flipx;
        bool flipy;
        bool flipcoord;
        adj_t(int plane, bool flipx, bool flipy, int flipcoord) :
            plane(plane), flipx(flipx), flipy(flipy), flipcoord(flipcoord) { }
        glm::ivec2 compute(glm::ivec2 p, glm::ivec2 sz) const
        {
            glm::ivec2 ret;
            ret[flipcoord] = flipx ? sz.x - p.x : p.x;
            ret[1 - flipcoord] = flipy ? sz.y - p.y : p.y;
            return ret;
        }
    };

    LOG("flood_fill plane %d", plane);

    auto& rtt = m_layers[layer]->rtt(plane);
    auto sz = rtt.getSize();
    
    if (!plane_data.mask[plane])
    {
        plane_data.mask[plane] = std::make_unique<bool[]>((size_t)sz.x * sz.y);
        plane_data.rgb[plane] = std::unique_ptr<glm::u8vec4[]>(
            reinterpret_cast<glm::u8vec4*>(m_layers[layer]->rtt(plane).readTextureData()));
        plane_data.bb[plane] = { sz.x, sz.y, 0, 0 };
        plane_data.dirty[plane] = false;
        plane_data.layer = m_layers[layer];
    }
    auto& mask = plane_data.mask[plane];
    auto& rgb = plane_data.rgb[plane];

    if (!source_color)
        source_color = std::make_unique<glm::vec4>(rgb[pos.back().y * sz.x + pos.back().x]);
    const glm::vec4 c = *source_color;

    std::array<std::vector<glm::ivec2>, 4> edges;
    static const std::array<adj_t, 4> adj[6] = {
        // front
        {
            adj_t(3, 1, 0, 0),
            adj_t(4, 1, 1, 0),
            adj_t(1, 0, 0, 0),
            adj_t(5, 1, 0, 0),
        },
        // right
        {
            adj_t(0, 1, 0, 0),
            adj_t(4, 1, 0, 1),
            adj_t(2, 0, 0, 0),
            adj_t(5, 0, 0, 1),
        },
        // back
        {
            adj_t(1, 1, 0, 0),
            adj_t(4, 0, 0, 0),
            adj_t(3, 0, 0, 0),
            adj_t(5, 0, 1, 0),
        },
        // left
        {
            adj_t(2, 1, 0, 0),
            adj_t(4, 0, 1, 1),
            adj_t(0, 0, 0, 0),
            adj_t(5, 1, 1, 1),
        },
        // top
        {
            adj_t(1, 1, 1, 1),
            adj_t(0, 1, 1, 0),
            adj_t(3, 1, 0, 1),
            adj_t(2, 0, 1, 0),
        },
        // bottom
        {
            adj_t(1, 0, 0, 1),
            adj_t(2, 0, 0, 0),
            adj_t(3, 0, 1, 1),
            adj_t(0, 1, 0, 0),
        },
    };

    auto test = [&](glm::ivec2 p, bool set_color) -> bool
    {
        int i = p.y * sz.x + p.x;
        if (p.x < 0)
        {
            edges[0].push_back(adj[plane][0].compute({ -p.x, p.y }, sz));
            return false;
        }
        else if (p.x >= sz.x)
        {
            edges[2].push_back(adj[plane][2].compute({ sz.x - p.x + 1, p.y }, sz));
            return false;
        }
        else if (p.y < 0)
        {
            edges[3].push_back(adj[plane][3].compute({ p.x, -p.y }, sz));
            return false;
        }
        else if (p.y >= sz.y)
        {
            edges[1].push_back(adj[plane][1].compute({  p.x, sz.y - p.y + 1 }, sz));
            return false;
        }
        if (!mask[i])
        {
            if (c.a == 0 && glm::abs(rgb[i].a - c.a) < threshold || 
                c.a > 0 && rgb[i].a > 0 && glm::distance(glm::vec3(c), glm::vec3(rgb[i])) < threshold)
            {
                if (set_color)
                {
                    mask[i] = true;
                    rgb[i] = dest_color * 255.f;
                    plane_data.dirty[plane] = true;
                    glm::vec2 bb_min = glm::min((glm::vec2)p, xy(plane_data.bb[plane]));
                    // add 1 pixel to the end because 1 pixel has min(0) and max(1)
                    glm::vec2 bb_max = glm::max((glm::vec2)p + glm::vec2(1), zw(plane_data.bb[plane]));
                    plane_data.bb[plane] = { bb_min, bb_max };
                }
                pos.push_back(p);
            }
            return true;
        }
        return false;
    };

    while (!pos.empty())
    {
        auto p = pos.back();
        pos.pop_back();
        if(!test(p + glm::ivec2( 0, 0), true))
            continue;
        test(p + glm::ivec2(-1, 0), false);
        test(p + glm::ivec2(1, 0), false);
        test(p + glm::ivec2(0, 1), false);
        test(p + glm::ivec2(0, -1), false);
        //for (int x = -1; x <= 1; x++)
        //    for (int y = -1; y <= 1; y++)
        //        if (x != 0 && y != 0)
        //            test(p + glm::ivec2(x, y), false);
    }

    for (int i = 0; i < 4; i++)
    {
        if (!edges[i].empty())
        {
            flood_fill(layer, adj[plane][i].plane, edges[i], plane_data, threshold, dest_color, source_color);
            //LOG("continue to plane %d -> %d", plane, adj[plane][i].plane);
        }
    }
}

void Canvas::FloodData::apply()
{
    for (int plane = 0; plane < 6; plane++)
    {
        if (!dirty[plane])
            continue;
        auto& rtt = layer->rtt(plane);
        App::I->render_task([&]
        {
            rtt.updateRgba8(0, 0, rtt.getWidth(), rtt.getHeight(), rgb[plane].get());
        });
        layer->face(plane) = true;
        layer->box(plane) = box_union(layer->box(plane), bb[plane]);
    }
    Canvas::I->m_unsaved = true;
}

void Canvas::resize(int width, int height)
{
    m_width = width;
    m_height = height;
    m_size = { width, height };
    for (int i = 0; i < 6; i++)
    {
        const auto stroke_format = current_canvas_stroke_internal_format();
        m_tmp[i].create(width, height, -1, stroke_format);
        m_tmp_dual[i].create(width, height, -1, stroke_format);
        m_tex[i].create(width, height, rgba8_internal_format());
        m_tex2[i].create(width, height, rgba8_internal_format());
    }
    for (auto& l : m_layers)
        l->resize(width, height);
    m_smask.create(width, height, "mask");
    m_layers_merge.resize(width, height);
    m_merge_rtt.create(width, height);
    m_merge_tex.create(width, height);
    m_unsaved = true;
}

void Canvas::destroy()
{
    for (int i = 0; i < 6; i++)
    {
        m_tmp[i].destroy();
        m_tmp_dual[i].destroy();
        m_tex[i].destroy();
        m_tex2[i].destroy();
    }
    for (auto& l : m_layers)
        l->destroy();
    m_smask.destroy();
    m_mixer.destroy();
    m_layers_merge.destroy();
    m_merge_rtt.destroy();
    m_merge_tex.destroy();
}

bool Canvas::create(int width, int height)
{
    m_width = width;
    m_height = height;
    m_size = { width, height };
    for (int i = 0; i < 6; i++)
    {
        const auto stroke_format = current_canvas_stroke_internal_format();
        m_tmp[i].create(width, height, -1, stroke_format);
        m_tmp_dual[i].create(width, height, -1, stroke_format);
        m_tex[i].create(width, height, rgba8_internal_format());
        m_tex2[i].create(width, height, rgba8_internal_format());
    }
#if defined(__GLES__)
    m_sampler_brush.create();
#else
    m_sampler_brush.create(texture_filter_linear(), texture_wrap_clamp_to_border());
#endif
    m_sampler.create(texture_filter_linear());
    m_sampler_nearest.create(texture_filter_nearest());
    m_sampler_brush.set_filter(texture_filter_linear_mipmap_linear(), texture_filter_linear());
    m_sampler_brush.set_border({ 1, 1, 1, 1 });
    m_sampler_stencil.create(texture_filter_linear(), texture_wrap_repeat());
    m_sampler_mix.create(texture_filter_nearest(), texture_wrap_repeat());
    m_sampler_linear.create();
    m_plane.create<1>(1, 1);
    m_plane_brush.create<1>(1, 1);
    m_brush_shape.create();
    for (auto& l : m_layers)
        l->create(width, height, "");
    m_smask.create(width, height, "mask");
    //m_smask.clear({1, 1, 1, 1});
    m_layers_merge.create(width, height, "merge");
    m_merge_rtt.create(width, height);
    m_merge_tex.create(width, height);
    m_unsaved = true;

    timelapse_reset_encoder();

    return true;
}

void Canvas::snapshot_save()
{
    LOG("SAVE SNAPSHOT");
    m_layers_snapshot.clear();
    m_layers_snapshot.resize(m_layers.size());
    for (int i = 0; i < m_layers.size(); i++)
        m_layers_snapshot[i] = m_layers[i]->snapshot();
}

void Canvas::snapshot_restore()
{
    LOG("RESTORE SNAPSHOT");
    for (int i = 0; i < m_layers.size(); i++)
        m_layers[i]->restore(m_layers_snapshot[i]);
    m_layers_snapshot.clear();
}

void Canvas::clear_context()
{
    LOG("Canvas CLEAR CONTEXT");
    for (auto& layer : m_layers)
        layer->destroy();
    for (int i = 0; i < 6; i++)
    {
        m_tmp[i].destroy();
        m_tex[i].destroy();
        m_tex2[i].destroy();
    }
};

void Canvas::import_equirectangular(std::string file_path, std::shared_ptr<Layer> layer /*= nullptr*/)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            import_equirectangular_thread(file_path, layer);
        });
        t.detach();
    }
}

void Canvas::import_equirectangular_thread(std::string file_path, std::shared_ptr<Layer> layer /*= nullptr*/, int frame /*= -1*/)
{
    Image img;
    if (!img.load_file(file_path))
        return;

    if (!layer)
        layer = m_layers[m_current_layer_idx];

    if (frame == -1)
        frame = layer->m_frame_index;
    
    auto a = new ActionImportEquirect;
    a->m_layer = layer;
    a->m_frame = frame;
    a->m_snap = std::make_shared<LayerFrame::Snapshot>(layer->snapshot(frame));
    a->m_path = file_path;
    ActionManager::add(a);

    m_unsaved = true;

    if (img.width == img.height / 6)
    {
        Texture2D tex;
        static const GLint indices[] = { 5, 0, 4, 1, 2, 3 };
        const auto texture_format = texture_format_for_image_channels(img.comp);
        tex.create(
            img.width,
            img.width,
            static_cast<GLint>(texture_format.internal_format),
            static_cast<GLint>(texture_format.pixel_format));
        int stride = img.width * img.width * img.comp;
        Plane plane;
        plane.create<1>(2, 2);
        draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) {
            apply_canvas_capability(depth_test_state(), false);
            tex.update(img.m_data.get() + indices[i] * stride);
            m_sampler.bind(0);
            set_active_texture_unit(0);
            tex.bind();
            pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader(
                pp::panopainter::LegacyCanvasDrawMergeTextureUniforms {
                    .mvp = glm::scale(glm::vec3(-1, -1, 1)),
                    .texture_slot = 0,
                });
            plane.draw_fill();
            tex.unbind();
            m_sampler.unbind();
        }, frame, false);
        plane.destroy();
    }
    else
    {
        Texture2D tex;
        tex.load_file(file_path);
        Sphere sphere;
        sphere.create<64, 64>(2.f);
        draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) {
            apply_canvas_capability(depth_test_state(), false);
            m_sampler.bind(0);
            set_active_texture_unit(0);
            tex.bind();
            pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader(
                pp::panopainter::LegacyCanvasDrawMergeTextureUniforms {
                    .mvp = proj * camera *
                        glm::eulerAngleY(glm::radians(180.f)) * glm::scale(glm::vec3(1, -1, 1)),
                    .texture_slot = 0,
                });
            sphere.draw_fill();
            tex.unbind();
            m_sampler.unbind();
        }, frame, false);
        sphere.destroy();
    }
    for (int i = 0; i < 6; i++)
    {
        layer->box(i) = glm::vec4(0, 0, m_width, m_height);
        layer->face(i) = true;
    }
}

void Canvas::export_equirectangular(std::string file_path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_equirectangular_thread(file_path);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_equirectangular_thread(std::string file_path)
{
    Image data;
    
    App::I->render_task([&]
    {
        draw_merge(false);
        Texture2D equirect = m_layers_merge.gen_equirect();
        data = equirect.get_image();
    });

    LOG("writing %s", file_path.c_str());
    if (file_path.substr(file_path.size() - 4) == ".jpg")
    {
        data.save_jpg(file_path, 100);
        inject_xmp(file_path);
    }
    else if (file_path.substr(file_path.size() - 4) == ".png")
    {
        data.save_png(file_path);
    }

    App::I->publish_exported_image(file_path);
}

void Canvas::inject_xmp(std::string jpg_path)
{
    static const char xmp[] = 
"http://ns.adobe.com/xap/1.0/\0" R"(<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
  <x:xmpmeta xmlns:x="adobe:ns:meta/" xmptk="SAMSUNG 360CAM">
    <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about="" xmlns:GPano="http://ns.google.com/photos/1.0/panorama/">
        <GPano:ProjectionType>equirectangular</GPano:ProjectionType>
        <GPano:UsePanoramaViewer>True</GPano:UsePanoramaViewer>
        <GPano:CroppedAreaLeftPixels>0</GPano:CroppedAreaLeftPixels>
        <GPano:CroppedAreaTopPixels>0</GPano:CroppedAreaTopPixels>
        <GPano:PoseHeadingDegrees>0</GPano:PoseHeadingDegrees>
        <GPano:PosePitchDegrees>0</GPano:PosePitchDegrees>
        <GPano:PoseRollDegrees>0</GPano:PoseRollDegrees>
        <GPano:StitchingSoftware>PanoPainter</GPano:StitchingSoftware>
      </rdf:Description>
    </rdf:RDF>
  </x:xmpmeta>
<?xpacket end="r"?>)";

    FILE* fp = fopen(jpg_path.c_str(), "rb");
    fseek(fp, 0, SEEK_END);
    long len = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    unsigned char* jpeg_data = (unsigned char*)malloc(len);
    fread(jpeg_data, len, 1, fp);
    fclose(fp);

    fp = fopen(jpg_path.c_str(), "wb");

    int i = 0;
    while (i < len && !(jpeg_data[i] == 0xff && jpeg_data[i + 1] == 0xd8)) i++;
    i += 2;

    unsigned char* xmp_section = (unsigned char*)malloc(sizeof(xmp) + 4);
    xmp_section[0] = 0xff;
    xmp_section[1] = 0xe1;
    xmp_section[2] = ((int)sizeof(xmp) + 2) >> 8;
    xmp_section[3] = ((int)sizeof(xmp) + 2) >> 0;
    memcpy(xmp_section + 4, xmp, sizeof(xmp));

    fwrite(jpeg_data, 1, i, fp);
    fwrite(xmp_section, 1, sizeof(xmp) + 4, fp);
    fwrite(jpeg_data + i, 1, len - i, fp);
    fclose(fp);

}

void Canvas::export_depth(std::string file_name, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_depth_thread(file_name);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_depth_thread(std::string file_name)
{
    RTT rtt;
    rtt.create(1024, 1024);

    glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)rtt.getWidth() / (float)rtt.getHeight(), 0.1f, 100.f);
    glm::mat4 camera = m_cam_rot;

    App::I->render_task([&]
    {
        draw_merge(false);

        rtt.bindFramebuffer();
        rtt.clear({ 0, 0, 0, 1 });
        apply_canvas_capability(blend_state(), true);
        apply_canvas_capability(depth_test_state(), false);
        apply_canvas_viewport(0, 0, rtt.getWidth(), rtt.getHeight());
        for (int plane_index = 0; plane_index < 6; plane_index++)
        {
            auto plane_mvp_z = proj * camera *
                m_plane_transform[plane_index] *
                glm::translate(glm::vec3(0, 0, -1)) *
                glm::scale(glm::vec3(2));

            m_sampler.bind(0);
            pp::panopainter::setup_legacy_canvas_draw_merge_texture_alpha_shader(
                pp::panopainter::LegacyCanvasDrawMergeTextureAlphaUniforms {
                    .mvp = plane_mvp_z,
                    .texture_slot = 0,
                    .alpha = 1.f,
                    .highlight = false,
                });

            set_active_texture_unit(0);
            m_layers_merge.rtt(plane_index).bindTexture();
            m_plane.draw_fill();
            m_layers_merge.rtt(plane_index).unbindTexture();
        }
        rtt.unbindFramebuffer();
    });

    uint8_t* rgba_data = rtt.readTextureData();
    stbi_flip_vertically_on_write(true);
    std::string path_rgba = App::I->work_path + "/" + file_name + ".png";
    stbi_write_jpg(path_rgba.c_str(), rtt.getWidth(), rtt.getHeight(), 4, rgba_data, 100);
    delete rgba_data;

    App::I->render_task([&]
    {
        rtt.bindFramebuffer();
        rtt.clear({ 0, 0, 0, 1 });
        apply_canvas_capability(blend_state(), true);
        apply_canvas_capability(depth_test_state(), false);
        apply_canvas_viewport(0, 0, rtt.getWidth(), rtt.getHeight());
        for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
        {
            for (int plane_index = 0; plane_index < 6; plane_index++)
            {
                if ((!m_layers[layer_index]->m_visible ||
                    m_layers[layer_index]->m_opacity == .0f ||
                    !m_layers[layer_index]->face(plane_index)))
                    continue;

                auto plane_mvp_z = proj * camera *
                    m_plane_transform[plane_index] *
                    glm::translate(glm::vec3(0, 0, -1)) *
                    glm::scale(glm::vec3(2));

                m_sampler.bind(0);
                pp::panopainter::setup_legacy_canvas_draw_merge_texture_colorize_shader(
                    pp::panopainter::LegacyCanvasDrawMergeTextureColorizeUniforms {
                        .mvp = plane_mvp_z,
                        .texture_slot = 0,
                        .color = { glm::vec3((float)(layer_index + 1) / (float)(m_layers.size() + 1)), 1.f },
                    });

                set_active_texture_unit(0);
                m_layers[layer_index]->rtt(plane_index).bindTexture();
                m_plane.draw_fill();
                m_layers[layer_index]->rtt(plane_index).unbindTexture();
            }
        }
        rtt.unbindFramebuffer();
    });

    uint8_t* depth_data = rtt.readTextureData();
    std::string path_depth = App::I->work_path + "/" + file_name + "_depth.png";
    stbi_write_jpg(path_depth.c_str(), rtt.getWidth(), rtt.getHeight(), 4, depth_data, 100);
    delete depth_data;
    stbi_flip_vertically_on_write(false);

    rtt.destroy();
}

void Canvas::export_layers(std::string path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_layers_thread(path);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_layers_thread(std::string path)
{
    auto pb = App::I->show_progress("Export Layers", m_layers.size());
    for (int i = 0; i < m_layers.size(); i++)
    {
        auto l = m_layers[i];
        Image img = l->gen_equirect().get_image();
        img.save_png(fmt::format("{}-layer{:02d}-{}.png", path, i, l->m_name));
        pb->increment();
    }
    pp::panopainter::close_legacy_dialog_node(*pb);
}

void Canvas::export_anim_frames(std::string path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_anim_frames_thread(path);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_anim_frames_thread(std::string path)
{
    auto pb = App::I->show_progress("Export Frames", anim_duration());
    for (int i = 0; i < anim_duration(); i++)
    {
        anim_goto_frame(i);
        export_equirectangular_thread(fmt::format("{}-{:02d}.png", path, i));
        pb->increment();
    }
    pp::panopainter::close_legacy_dialog_node(*pb);
}

void Canvas::export_anim_mp4(std::string path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_anim_mp4_thread(path);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_anim_mp4_thread(std::string path)
{
    auto pb = App::I->show_progress("Export Animation", anim_duration());
    int fps = App::I->animation->get_fps();
    MP4Encoder mp4;
    int res = std::min<int>(1024, m_width);
    mp4.init(res * 4, res * 2, 30, 2 << 20);
    for (int i = 0; i < anim_duration(); i++)
    {
        Image data;
        App::I->render_task([&]
        {
            anim_goto_frame(i);
            draw_merge(false);
            Texture2D equirect = m_layers_merge.gen_equirect({ res, res });
            data = equirect.get_image();
        });
        for (int j = 0; j < 30/fps; j++)
            mp4.encode(data);
        pb->increment();
    }
    mp4.write_mp4(path);
    pp::panopainter::close_legacy_dialog_node(*pb);
}

void Canvas::export_cube_faces(std::string file_name, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            export_cube_faces_thread(file_name);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_cube_faces_thread(std::string file_name)
{
#ifdef __OBJC__
    NSMutableArray* files = [NSMutableArray array];
#endif
    static std::array<const char*, 6> plane_names{ "front", "right", "back", "left", "top", "bottom" };

    auto pb = App::I->show_progress("Export Cube Faces", 7);
    
    App::I->render_task([this] {
        draw_merge(false);
    });
    pb->increment();

    for (int i = 0; i < 6; i++)
    {
        Image face = m_layers_merge.rtt(i).get_image();
        std::string path = fmt::format("{}/{}-{}.png", App::I->work_path, file_name, plane_names[i]);
        face.save_png(path);
        pb->increment();

        App::I->publish_exported_image(path);
#ifdef __OBJC__
        [files addObject : [NSString stringWithUTF8String:path.c_str()] ];
#endif
    }

    pp::panopainter::close_legacy_dialog_node(*pb);

#ifdef __OBJC__
    static char name[128];
    sprintf(name, "%s.zip", App::I->work_path.c_str());
    auto zip_path = [NSString stringWithUTF8String:name];
    //[SSZipArchive createZipFileAtPath:zip_path withFilesAtPaths:files];
    //for (NSString* f : files)
    //    [[NSFileManager defaultManager] removeItemAtPath:f error:nil];
#endif
}

void Canvas::project_save(std::function<void(bool)> on_complete)
{
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            bool ret = project_save_thread(App::I->doc_path, true);
            if (on_complete)
                on_complete(ret);
        });
        t.detach();
    }
}

void Canvas::project_save(std::string file_path, std::function<void(bool)> on_complete)
{
    LOG("saving %s", file_path.c_str());
    if (App::I->check_license())
    {
        std::thread t([=] {
            BT_SetTerminate();
            bool ret = project_save_thread(file_path, true);
            if (on_complete)
                on_complete(ret);
        });
        t.detach();
    }
    else
    {
        LOG("no license, no save");
    }
}

bool Canvas::project_save_thread(std::string file_path, bool show_progress)
{
    // already saved, nothing to do
    if (!m_unsaved && file_path == App::I->doc_path)
    {
        LOG("already saved");
        return true;
    }

//     static char name[128];
//     sprintf(name, "%s/latlong.ppi", data_path.c_str());
    FILE* fp = nullptr;
    
    const auto save_target = pp::app::plan_document_canvas_project_save_target(App::I->data_path, file_path);
    if (!save_target) {
        LOG("cannot plan project save target for %s: %s", file_path.c_str(), save_target.status().message);
        return false;
    }
    const auto& save_paths = save_target.value();
    const std::string& file_name = save_paths.file_name;
    const std::string& tmp_path = save_paths.temporary_path;
    const std::string& lapse_path = save_paths.timelapse_path;
    
    LOG("file name %s", file_name.c_str());
    LOG("tmp path %s", tmp_path.c_str());

    bool target_exists = false;
    if ((fp = fopen(file_path.c_str(), "rb"))) {
        fclose(fp);
        fp = nullptr;
        target_exists = true;
    }

    const auto write_plan = pp::app::plan_document_canvas_project_save_write(save_paths, target_exists);
    if (!write_plan) {
        LOG("cannot plan project save write for %s: %s", file_path.c_str(), write_plan.status().message);
        return false;
    }

    bool use_tmp = write_plan.value().uses_temporary;
    if (write_plan.value().uses_temporary)
    {
        LOG("use tmp file");
        fp = fopen(write_plan.value().write_path.c_str(), "wb");
        if (!fp)
        {
            LOG("cannot write tmp project to %s", tmp_path.c_str());
            use_tmp = false;
        }
    }

    LOG("save first time");
    
    if (!fp)
    {
        // write directly to the new file
        if (!(fp = fopen(file_path.c_str(), "wb")))
        {
            LOG("cannot write project to %s", file_path.c_str());
            return false;
        }
        LOG("unsafe mode saving directly to %s", file_path.c_str());
    }

    PPIHeader ppi_header;
    fwrite(&ppi_header, sizeof(PPIHeader), 1, fp);

    // load thumbnail
    Image thumb = thumbnail_generate(ppi_header.thumb_header.width, ppi_header.thumb_header.height);
    
    std::shared_ptr<NodeProgressBar> pb;
    if (show_progress)
        pb = App::I->show_progress("Saving Pano Project");
    
    thumb.flip();
    fwrite(thumb.data(), thumb.size(), 1, fp);

    fwrite(&m_width, sizeof(int), 1, fp);
    fwrite(&m_height, sizeof(int), 1, fp);
    
    int n_layers = (int)m_layers.size();
    fwrite(&n_layers, sizeof(int), 1, fp);
    
    int n_frames = std::accumulate(m_layers.begin(), m_layers.end(), 0, 
        [](int tot, auto& l) { return tot + l->frames_count(); });
    if (ppi_header.doc_version.minor >= 3)
        fwrite(&n_frames, sizeof(int), 1, fp);

    int progress = 0;
    int total = n_frames * 6;
    
    for (int i = 0; i < (int)m_layers.size(); i++)
    {
        int n_order = i;
        fwrite(&n_order, sizeof(int), 1, fp);

        float layer_alpha = m_layers[i]->m_opacity;
        fwrite(&layer_alpha, sizeof(float), 1, fp);

        int name_len = (int)m_layers[i]->m_name.size();
        fwrite(&name_len, sizeof(int), 1, fp);
        fwrite(m_layers[i]->m_name.data(), name_len, 1, fp);

        if (ppi_header.doc_version.minor >= 2)
        {
            fwrite(&m_layers[i]->m_blend_mode, sizeof(int), 1, fp);
            fwrite(&m_layers[i]->m_alpha_locked, sizeof(bool), 1, fp);
            fwrite(&m_layers[i]->m_visible, sizeof(bool), 1, fp);
        }

        int frames = 1;
        if (ppi_header.doc_version.minor >= 3)
        {
            frames = (int)m_layers[i]->frames_count();
            fwrite(&frames, sizeof(int), 1, fp);
        }
        
        for (int fi = 0; fi < frames; fi++)
        {
            if (ppi_header.doc_version.minor >= 3)
            {
                int duration = m_layers[i]->frame_duration(fi);
                fwrite(&duration, sizeof(int), 1, fp);
            }
            bool gpu = m_layers[i]->frame(fi).gpu_load();
            m_layers[i]->optimize(fi);
            auto snap = m_layers[i]->snapshot(fi);
            for (int plane_index = 0; plane_index < 6; plane_index++)
            {
                int has_data = snap.m_dirty_face[plane_index] ? 1 : 0;
                fwrite(&has_data, sizeof(int), 1, fp);
                if (has_data)
                {
                    glm::ivec4 b = snap.m_dirty_box[plane_index];
                    glm::vec2 sz = zw(b) - xy(b);
                    int box[4] = { b.x, b.y, b.z, b.w };
                    fwrite(&box, sizeof(box), 1, fp);

                    std::vector<uint8_t> compressed;
                    auto callback = [](void* context, void* data, int size)
                    {
                        std::vector<uint8_t>* buffer = static_cast<std::vector<uint8_t>*>(context);
                        buffer->insert(buffer->end(), (uint8_t*)data, (uint8_t*)data + size);
                    };
                    int ret = stbi_write_png_to_func(callback, &compressed, sz.x, sz.y, 4, snap.image[plane_index].get(), sz.x * 4);

                    int data_size = (int)compressed.size();
                    fwrite(&data_size, sizeof(int), 1, fp);

                    fwrite(compressed.data(), 1, compressed.size(), fp);
                }
                progress++;
                float p = (float)progress / total * 100.f;
                if (show_progress)
                    pb->m_progress->SetWidthP(p);
                LOG("progress: %f", p);
            }
            if (!gpu)
                m_layers[i]->frame(fi).gpu_unload();
        }
    }

    if (ppi_header.doc_version.minor >= 4)
    {
        BinaryStreamWriter sw;
        sw.init(BinaryStream::ByteOrder::LittleEndian);

        Serializer::Descriptor info;
        info.class_id = "ppi_info";
        info.name = L"info header";
        //info.props["has_encoder"] = std::make_shared<Serializer::Boolean>(m_encoder != nullptr);
        sw << info;
        //if (m_encoder != nullptr)
        //    sw << *m_encoder;

        int bytes = sw.m_data.size();
        fwrite(&bytes, sizeof(int), 1, fp);
        fwrite((char*)sw.m_data.data(), sw.m_data.size(), 1, fp);
    }

    fclose(fp);

    bool target_remove_attempted = false;
    bool target_remove_succeeded = false;
    bool temporary_rename_attempted = false;
    bool temporary_rename_succeeded = false;
    if (use_tmp)
    {
        LOG("project saved tmp to %s", tmp_path.c_str());
        LOG("swapping to %s", file_path.c_str());
        target_remove_attempted = true;
        target_remove_succeeded = std::remove(file_path.c_str()) == 0;
        if (target_remove_succeeded)
        {
            temporary_rename_attempted = true;
            temporary_rename_succeeded = std::rename(tmp_path.c_str(), file_path.c_str()) == 0;
        }
    }

    const auto commit_plan = pp::app::plan_document_canvas_project_save_commit(
        pp::app::DocumentCanvasProjectSaveCommitInput {
            .used_temporary = use_tmp,
            .target_remove_attempted = target_remove_attempted,
            .target_remove_succeeded = target_remove_succeeded,
            .temporary_rename_attempted = temporary_rename_attempted,
            .temporary_rename_succeeded = temporary_rename_succeeded,
        });
    const bool success = commit_plan.saved;
    if (commit_plan.saved && commit_plan.temporary_renamed) {
        LOG("tmp file swapped succesfully");
    } else if (!commit_plan.saved && commit_plan.target_may_be_missing) {
        LOG("tmp file NOT swapped, original removed");
    } else if (!commit_plan.saved && commit_plan.used_temporary) {
        LOG("could not remove %s", file_path.c_str());
    } else if (commit_plan.saved) {
        LOG("project saved to %s", file_path.c_str());
    }

    const auto post_commit_plan = pp::app::plan_document_canvas_project_save_post_commit(
        pp::app::DocumentCanvasProjectSavePostCommitInput {
            .save_succeeded = success,
            .timelapse_encoder_available = Canvas::I->m_encoder != nullptr,
            .progress_ui_visible = show_progress,
        });

    if (post_commit_plan.marks_document_clean)
    {
        m_unsaved = false;
    }
    if (post_commit_plan.marks_new_document_committed)
    {
        m_newdoc = false;
    }

    if (post_commit_plan.saves_timelapse_sidecar)
    {
        BinaryStreamWriter sw;
        sw.init(BinaryStream::ByteOrder::LittleEndian);
        Serializer::Descriptor info;
        info.class_id = "tracks-info";
        info.name = L"Timelapse Tracks";
        info.props["has-track-360"] = std::make_shared<Serializer::Boolean>(true);
        info.props["version"] = std::make_shared<Serializer::Integer>(1);
        sw << info;
        sw << *Canvas::I->m_encoder;
        if (!sw.save(lapse_path))
            LOG("cannot save timelase to %s", lapse_path.c_str());
    }

    if (post_commit_plan.flushes_platform_storage)
    {
        App::I->flush_platform_storage();
    }

    if (post_commit_plan.dismisses_progress_ui)
    {
        pp::panopainter::close_legacy_dialog_node(*pb);
    }
    if (post_commit_plan.updates_title)
    {
        App::I->title_update();
    }
    
    return success;
}

void Canvas::project_open(std::string file_path, std::function<void(bool)> on_complete)
{
    std::thread t([=] {
        BT_SetTerminate();
        bool result = project_open_thread(file_path);
        if (on_complete)
            on_complete(result);
    });
    t.detach();
}

bool Canvas::project_open_thread(std::string file_path)
{
    FILE* fp = fopen(file_path.c_str(), "rb");
    if (!fp)
    {
        LOG("cannot write project to %s", file_path.c_str());
        return false; // should probably return a bool
    }

    PPIHeader ppi_header;
    fread(&ppi_header, sizeof(PPIHeader), 1, fp);

    if (!ppi_header.valid())
    {
        LOG("INVALID PPI HEADER");
        return false;
    }
    
    std::shared_ptr<NodeProgressBar> pb;
    if (App::I->layout.m_loaded)
    {
        pb = std::make_shared<NodeProgressBar>();
        pb->set_manager(&App::I->layout);
        pb->init();
        pb->create();
        pb->loaded();
        pb->m_progress->SetWidthP(0);
        pb->m_title->set_text("Opening Pano Project");
        App::I->layout[App::I->main_id]->add_child(pb);
    }

    // skip thumbnail
    Image thumb;
    thumb.width = ppi_header.thumb_header.width;
    thumb.height = ppi_header.thumb_header.height;
    thumb.comp = ppi_header.thumb_header.comp;
    fseek(fp, thumb.size(), SEEK_CUR);

    fread(&m_width, sizeof(int), 1, fp);
    fread(&m_height, sizeof(int), 1, fp);
    
    int n_layers = 0;
    fread(&n_layers, sizeof(int), 1, fp);
    int n_frames = 1;
    if (ppi_header.doc_version.minor >= 3)
        fread(&n_frames, sizeof(int), 1, fp);

    const int bytes = m_width * m_height * 4;
    LayerFrame::Snapshot snap;
    snap.create(m_width, m_height); // allocate single data, no box should be bigger

    int progress = 0;
    int total = n_frames * 6;
    
    for (auto& l : m_layers)
        l->destroy();
    m_layers.clear();
    //clear_all();
    resize(m_width, m_height);

    std::vector<std::shared_ptr<Layer>> tmp_layers(n_layers);

    for (int i = 0; i < n_layers; i++)
    {
        int n_order;
        fread(&n_order, sizeof(int), 1, fp);

        //if (ppi_header.doc_version.minor > 1)
        //    n_order = i;

        tmp_layers[n_order] = std::make_unique<Layer>();
        auto& layer = tmp_layers[n_order];

        fread(&layer->m_opacity, sizeof(float), 1, fp);

        int name_len;
        fread(&name_len, sizeof(int), 1, fp);
        std::string name(name_len, '\0');
        fread((char*)name.data(), name_len, 1, fp);

        if (ppi_header.doc_version.minor >= 2)
        {
            fread(&layer->m_blend_mode, sizeof(int), 1, fp);
            fread(&layer->m_alpha_locked, sizeof(bool), 1, fp);
            fread(&layer->m_visible, sizeof(bool), 1, fp);
        }

        int frames = 1;
        if (ppi_header.doc_version.minor >= 3)
            fread(&frames, sizeof(int), 1, fp);

        layer->create(m_width, m_height, name.c_str());

        for (int fi = 0; fi < frames; fi++)
        {
            if (fi > 0)
                layer->add_frame();
            if (ppi_header.doc_version.minor >= 3)
            {
                int duration = layer->frame_duration(fi);
                fread(&duration, sizeof(int), 1, fp);
            }
            snap.clear();
            for (int plane_index = 0; plane_index < 6; plane_index++)
            {
                int has_data;
                fread(&has_data, sizeof(int), 1, fp);
                snap.m_dirty_face[plane_index] = has_data;
                if (has_data)
                {
                    int b[4];
                    fread(&b, sizeof(b), 1, fp);
                    snap.m_dirty_box[plane_index] = glm::vec4(b[0], b[1], b[2], b[3]);
                    glm::vec2 sz = zw(snap.m_dirty_box[plane_index]) - xy(snap.m_dirty_box[plane_index]);

                    int data_size;
                    fread(&data_size, sizeof(int), 1, fp);
                    std::vector<uint8_t> compressed(data_size);

                    fread(compressed.data(), 1, data_size, fp);
                    int imgw, imgh, imgc;
                    uint8_t* rgba = stbi_load_from_memory(compressed.data(), data_size, &imgw, &imgh, &imgc, 4);
                    if (rgba)
                    {
                        std::copy(rgba, rgba + (imgw * imgh * 4), snap.image[plane_index].get());
                        delete rgba;
                    }
                }

                progress++;
                float p = (float)progress / total * 100.f;
                LOG("progress: %f", p);

                if (App::I->layout.m_loaded)
                {
                    pb->m_progress->SetWidthP(p);
                }
            }
            layer->restore(snap, fi);
        }
    }

    std::swap(tmp_layers, m_layers);

    if (ppi_header.doc_version.minor >= 4)
    {
        int bytes = 0;
        fread(&bytes, sizeof(int), 1, fp);
        std::vector<uint8_t> data(bytes);
        fread(data.data(), bytes, 1, fp);
        BinaryStreamReader sr;
        sr.init(data.data(), data.size(), BinaryStream::ByteOrder::LittleEndian);
        Serializer::Descriptor info;
        sr >> info;
        //if (info.value<Serializer::Boolean>("has_encoder"))
        //{
        //    m_encoder = std::make_unique<MP4Encoder>();
        //    sr >> *m_encoder;
        //    m_encoder->init();
        //}
        //else
        //{
        //    timelapse_reset_encoder();
        //}
    }
    //else
    //{
    //    timelapse_reset_encoder();
    //}

    fclose(fp);
    LOG("project restore from %s", file_path.c_str());

    auto start = file_path.rfind('/') + 1;
    std::string file_name = file_path.substr(start, file_path.length() - start - strlen(".ppi"));
    std::string lapse_path = App::I->data_path + '/' + file_name + ".pptl";
    if (Asset::exist(lapse_path))
    {
        BinaryStreamReader sr;
        sr.load(lapse_path, BinaryStream::ByteOrder::LittleEndian);
        Serializer::Descriptor info;
        sr >> info;
        if (info.value<Serializer::Boolean>("has-track-360"))
        {
            m_encoder = std::make_unique<MP4Encoder>();
            sr >> *m_encoder;
            m_encoder->init();
        }
    }
    else
    {
        timelapse_reset_encoder();
    }
    
    m_current_layer_idx = 0;
    m_current_stroke = nullptr;
    m_dual_stroke = nullptr;
    m_show_tmp = false;
    m_smask_active = false;
    m_smask_mode = 0;
    m_dirty = false;
    m_commit_delayed = false;
    m_dirty_stroke = false;
    memset(m_dirty_face, 0, sizeof(bool) * 6);
    memset(m_pick_ready, 0, sizeof(bool) * 6);
    m_unsaved = false;
    m_newdoc = false;
    if (App::I->layout.m_loaded)
    {
        pp::panopainter::close_legacy_dialog_node(*pb);
        App::I->ui_task([] {
            App::I->title_update();
            App::I->update_rec_frames();
            Canvas::I->anim_update();
            App::I->animation->load_layers();
        });
    }
    return true;
}

Image Canvas::thumbnail_generate(int w, int h)
{
    Image image;
    image.create(w, h);

    App::I->render_task([this, w, h, &image]
    {
        // save viewport and clear color states
        const auto vp = query_canvas_viewport();
        const auto cc = query_canvas_clear_color();
        auto blend = query_canvas_capability(blend_state());

        // prepare common states
        apply_canvas_viewport(0, 0, w, h);

        RTT fb;
        fb.create(w, h);
        fb.bindFramebuffer();
        Plane m_face_plane;
        m_face_plane.create<1>(2, 2);
        Texture2D blendtex;
        blendtex.create(w, h);
        const auto layer_feedback = canvas_destination_feedback_plan(w, h);
        const bool copy_layer_destination = !layer_feedback.reads_destination_color;

        // recalculate because of different aspect ratio than the m_proj matrix
        glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)w / (float)h, 0.1f, 1000.f);

        fb.clear({ 1, 1, 1, 0 });
        for (int i = 0; i < 6; i++)
        {
            apply_canvas_capability(blend_state(), false);
            auto plane_mvp = proj * m_mv * m_plane_transform[i] * glm::translate(glm::vec3(0, 0, -1));

            if (copy_layer_destination)
            {
                set_active_texture_unit(2);
                blendtex.bind();
                m_sampler_nearest.bind(2);
            }
            m_sampler_nearest.bind(0);   // nearest
            for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
            {
                if (!m_layers[layer_index]->m_visible ||
                    m_layers[layer_index]->m_opacity == 0.f ||
                    !m_layers[layer_index]->face(i))
                    continue;
                if (copy_layer_destination)
                {
                    set_active_texture_unit(2);
                    copy_framebuffer_to_texture_2d(0, 0, 0, 0, w, h);
                }
                pp::panopainter::setup_legacy_canvas_draw_merge_texture_blend_shader(
                    pp::panopainter::LegacyCanvasDrawMergeTextureBlendUniforms {
                        .mvp = plane_mvp,
                        .texture_slot = 0,
                        .destination_texture_slot = 2,
                        .use_destination_texture = copy_layer_destination,
                        .blend_mode = m_layers[layer_index]->m_blend_mode,
                        .alpha = m_layers[layer_index]->m_opacity,
                    });
                set_active_texture_unit(0);
                m_layers[layer_index]->rtt(i).bindTexture();
                m_face_plane.draw_fill();
                m_layers[layer_index]->rtt(i).unbindTexture();
            }

            if (copy_layer_destination)
            {
                set_active_texture_unit(2);
                blendtex.unbind();
            }

            set_active_texture_unit(0);
            blendtex.bind();
            // copy the content of the fb before drawing the grid
            copy_framebuffer_to_texture_2d(0, 0, 0, 0, w, h);

            // draw the grid
            pp::panopainter::setup_legacy_canvas_draw_merge_checkerboard_shader(
                pp::panopainter::LegacyCanvasDrawMergeCheckerboardUniforms {
                    .mvp = plane_mvp,
                });
            m_face_plane.draw_fill();

            // now blend with the background
            apply_canvas_capability(blend_state(), true);
            pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader(
                pp::panopainter::LegacyCanvasDrawMergeTextureUniforms {
                    .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
                    .texture_slot = 0,
                });
            m_sampler.bind(0); // linear
            m_plane.draw_fill();

            blendtex.unbind();
        }

        fb.unbindFramebuffer();

        // read the rendered image
        fb.readTextureData((uint8_t*)image.data());

        fb.destroy();
        blendtex.destroy();

        // restore viewport and clear color states
        blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false);
        apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
        apply_canvas_clear_color(cc);
        set_active_texture_unit(0);
    });

    return image;
}

Image Canvas::thumbnail_read(std::string file_path)
{
//     static char name[128];
//     sprintf(name, "%s/latlong.ppi", data_path.c_str());
    FILE* fp = fopen(file_path.c_str(), "rb");
    if (!fp)
    {
        LOG("cannot read project %s", file_path.c_str());
        return {}; // return empty image
    }
    PPIHeader ppi_header;
    fread(&ppi_header, sizeof(PPIHeader), 1, fp);

    if (!ppi_header.valid())
        return {};

    Image thumb;
    thumb.width = ppi_header.thumb_header.width;
    thumb.height = ppi_header.thumb_header.height;
    thumb.comp = ppi_header.thumb_header.comp;
    thumb.create();
    fread((uint8_t*)thumb.data(), thumb.size(), 1, fp);
    fclose(fp);
    LOG("project thumbnail read from %s", file_path.c_str());
    return thumb;
}

void Canvas::draw_objects_direct(std::function<void(const glm::mat4& camera, const glm::mat4& proj, int i)> observer, Layer& layer, int frame)
{
    App::I->render_task([&]
    {
        // save viewport and clear color states
        const auto vp = query_canvas_viewport();
        const auto cc = query_canvas_clear_color();
        auto blend = query_canvas_capability(blend_state());

        // prepare common states
        apply_canvas_viewport(0, 0, layer.w, layer.h);
        apply_canvas_capability(blend_state(), false);

        GLuint rboID = allocate_canvas_depth_renderbuffer(layer.w, layer.h);

        glm::mat4 proj = glm::perspective(glm::radians(90.f), 1.f, .01f, 1000.f);
        for (int i = 0; i < 6; i++)
        {
            glm::mat4 plane_camera = glm::lookAt(glm::vec3(0), m_plane_origin[i], m_plane_tangent[i]);
            layer.rtt(i, frame).bindFramebuffer();
            attach_canvas_depth_renderbuffer(rboID);

            observer(plane_camera, proj, i);

            attach_canvas_depth_renderbuffer(0);
            layer.rtt(i, frame).unbindFramebuffer();

            layer.face(i, frame) = true;
            layer.box(i, frame) = { 0, 0, layer.w, layer.h };
        }

        delete_canvas_renderbuffer(rboID);

        // restore viewport and clear color states
        blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false);
        apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
        apply_canvas_clear_color(cc);
        set_active_texture_unit(0);
    });
}

void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm::mat4& proj, int i)> observer, Layer& layer, int frame, bool save_history)
{
    App::I->render_task([&]
    {
        // save viewport and clear color states
        const auto vp = query_canvas_viewport();
        const auto cc = query_canvas_clear_color();
        auto blend = query_canvas_capability(blend_state());

        // prepare common states
        apply_canvas_viewport(0, 0, layer.w, layer.h);
        apply_canvas_capability(blend_state(), false);

        GLuint rboID = allocate_canvas_depth_renderbuffer(layer.w, layer.h);

        RTT rtt;
        rtt.create(layer.w, layer.h);
        rtt.bindFramebuffer();
        attach_canvas_depth_renderbuffer(rboID);
        rtt.unbindFramebuffer();

        // allocate action to add to history
        ActionStroke* action;
        
        if (save_history)
        {
            action = new ActionStroke;
            action->was_saved = !m_unsaved;
        }

        glm::mat4 proj = glm::perspective(glm::radians(90.f), 1.f, .01f, 1000.f);
        for (int i = 0; i < 6; i++)
        {
            glm::mat4 plane_camera = glm::lookAt(glm::vec3(0), m_plane_origin[i], m_plane_tangent[i]);
            rtt.bindFramebuffer();
            rtt.clear({ 1, 1, 1, 0 });
            observer(plane_camera, proj, i);
            rtt.unbindFramebuffer();

            glm::vec4 bounds = rtt.calc_bounds();

            layer.rtt(i, frame).bindFramebuffer();

            glm::vec2 box_sz = zw(bounds) - xy(bounds);
            bool has_data = box_sz.x > 0 && box_sz.y > 0;

            if (save_history)
            {
                // save image before commit
                if (has_data)
                {
                    action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
                    layer.rtt(i, frame).readPixelsRgba8(
                        static_cast<int>(bounds.x),
                        static_cast<int>(bounds.y),
                        static_cast<int>(box_sz.x),
                        static_cast<int>(box_sz.y),
                        action->m_image[i].get());
                    action->m_box[i] = bounds;
                }
                action->m_old_box[i] = layer.box(i, frame);
                action->m_old_dirty[i] = layer.face(i, frame);
            }

            // draw the tmp layer into the actual layer
            if (has_data)
            {
                pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader(
                    pp::panopainter::LegacyCanvasDrawMergeTextureUniforms {
                        .mvp = glm::ortho(-0.5f, 0.5f, -0.5f, 0.5f),
                        .texture_slot = 0,
                    });
                set_active_texture_unit(0);
                m_sampler_nearest.bind(0);
                rtt.bindTexture();
                m_plane.draw_fill();
                rtt.unbindTexture();

                layer.face(i, frame) = true;
                layer.box(i, frame) = { glm::min(xy(layer.box(i, frame)), xy(bounds)), glm::max(zw(layer.box(i, frame)), zw(bounds)) };
            }

            layer.rtt(i, frame).unbindFramebuffer();
        }

        if (save_history)
        {
            // save history
            action->m_layer_idx = m_current_layer_idx;
            action->m_frame_idx = frame;
            action->m_canvas = this;
            //action->m_stroke = std::move(m_current_stroke);
            ActionManager::add(action);
        }

        delete_canvas_renderbuffer(rboID);
        rtt.destroy();

        // restore viewport and clear color states
        blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false);
        apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
        apply_canvas_clear_color(cc);
        set_active_texture_unit(0);
    });
}

void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm::mat4& proj, int i)> observer, int frame, bool save_history)
{
    draw_objects(observer, layer(), frame, save_history);
}

void Canvas::project2Dpoints(std::vector<vertex_t>& vertices)
{
    for (auto& p : vertices)
    {
        glm::vec3 ro, rd, hit_o, hit_d;
        glm::vec2 hit_fb;
        int plane_id;
        if (point_trace(p.pos, ro, rd, hit_o, hit_fb, hit_d, plane_id))
            p.pos = glm::vec4(hit_o, 1);
    }
}

glm::vec3 Canvas::project2Dpoint(glm::vec2 pt)
{
    glm::vec3 ro, rd, hit_o, hit_d;
    glm::vec2 hit_fb;
    int plane_id;
    if (point_trace(pt, ro, rd, hit_o, hit_fb, hit_d, plane_id))
        return glm::vec4(hit_o, 1);
    return glm::vec3(0);
}



// return the 2d shape of the faces based on the current camera
// this can be used for screen space shapes clipping
std::vector<glm::vec2> Canvas::face_to_shape2D(int plane_index)
{
    static std::array<glm::vec4, 4> corners{
        glm::vec4(-1.f, +1.f, -1.f, 1.f), // A top-left
        glm::vec4(+1.f, +1.f, -1.f, 1.f), // B top-right
        glm::vec4(+1.f, -1.f, -1.f, 1.f), // C bottom-right
        glm::vec4(-1.f, -1.f, -1.f, 1.f), // D bottom-left
    };

    // compute points in camera space
    std::vector<glm::vec3> pt_cam;
    for (auto c : corners)
    {
        auto pt_world = m_plane_transform[plane_index] * c;
        pt_cam.push_back(m_mv * pt_world);
    }

    // clip at near plane
    pt_cam = poly_clip_near(pt_cam, 0.01);

    // compute windows space
    std::vector<glm::vec2> points;
    for (auto p : pt_cam)
    {
        auto pt_clip = m_proj * glm::vec4(p, 1);
        pt_clip = pt_clip / pt_clip.w;
        glm::vec2 pt_screen = (glm::vec2(pt_clip) * 0.5f + 0.5f) * zw(m_vp);
        pt_screen.y = m_vp.w - pt_screen.y - 1;
        points.push_back(pt_screen);
    }
    return points;
}

void Canvas::push_camera()
{
    m_camera_stack.push(get_camera());
}

void Canvas::pop_camera()
{
    if (!m_camera_stack.empty())
    {
        set_camera(m_camera_stack.top());
        m_camera_stack.pop();
    }
}

CameraData Canvas::get_camera()
{
    CameraData c;
    c.m_box = m_box;
    c.m_mv = m_mv;
    c.m_pan = m_pan;
    std::copy_n(m_plane_dir, 6, c.m_plane_dir);
    std::copy_n(m_plane_unproject, 6, c.m_plane_unproject);
    c.m_proj = m_proj;
    c.m_vp = m_vp;
    return c;
}

void Canvas::set_camera(const CameraData& c)
{
    m_box = c.m_box;
    m_mv = c.m_mv;
    m_pan = c.m_pan;
    std::copy_n(c.m_plane_dir, 6, m_plane_dir);
    std::copy_n(c.m_plane_unproject, 6, m_plane_unproject);
    m_proj = c.m_proj;
    m_vp = c.m_vp;
}

void Canvas::timelapse_reset_encoder() noexcept
{
    m_encoder = std::make_unique<MP4Encoder>();
    int res = glm::min<int>(m_width / 2, 1024);
    m_encoder->init(res * 4, res * 2, 30, 2000 << 10);
}
